19.04. Zugriff von C nach Java
Mit JNI können Sie nicht nur nativen Code in Java ausführen, es ist auch der umgekehrte Weg möglich: Sie können in einer nativen Bibliothek Methoden von Klassen und Objekten aufrufen und Attribute auslesen oder setzen.
Unser Ziel ist eine kleine Klasse (CToJavaTest
), die folgende Methoden bereitstellt:
public List<String> generateList()
– Erzeugt eine beliebige Listepublic static void printList(List<String> toPrint)
– Gibt eineList<String>
auspublic native void callJNI()
– Aufruf unserer JNI Bibliothek
Außerdem beinhaltet Sie noch folgende Felder:
public static String staticStringValue
– Ein statischesString
-Attributprivate int intValue
– Eine gewöhnliche Objektvariable
Die zugehörige native Bibliothek wird Methoden aufrufen, Rückgabewerte verarbeiten und Attribute manipulieren.
Über Sinn und Unsinn dieser Klasse lässt sich natürlich streiten. Sie ist jedoch ideal um Ihnen die Lerninhalte dieses Kapitels zu vermitteln. Werfen wir einen Blick auf diese Klasse:
package de.jbb.jni; import java.util.*; public class CToJavaTest { static { System.loadLibrary("ctojava"); } public static String staticStringValue = "I'm a String!"; private int intValue = 5; public List<String> generateList() { List<String> ret = new ArrayList<String>(intValue); for (int i = 0; i < intValue; i++) { ret.add("Java: Eintrag: " + (i + 1)); } return ret; } public static void printList(List<String> toPrint) { System.out.println("Java: printList"); for (int i = 0; i < toPrint.size(); i++) { System.out.println("\tJava: " + toPrint.get(i)); } } public native void callJNI(); public static void main(String[] args) { CToJavaTest ctj = new CToJavaTest(); ctj.callJNI(); System.out.println("Java: " + CToJavaTest.staticStringValue); } }
Wir legen gleichzeitig noch eine Main-Methode an, in der wir uns ein neues Objekt dieser Klasse erzeugen, die native Methode aufrufen, und letztendlich das statische String
-Attribut staticStringValue
ausgeben. Sie ahnen es bereits: Wir werden diese Variable in unserer nativen Bibliothek manipulieren.
Der Java Code sollte für Sie keine Probleme darstellen. Widmen wir uns deshalb unserer C-Bibliothek zu. Nachfolgenden finden Sie den kompletten Quellcode, welchen ich Ihnen anschließend erläutern werde.
#include <jni.h> #include <stdio.h> #include "de_jbb_jni_CToJavaTest.h" JNIEXPORT void JNICALL Java_de_jbb_jni_CToJavaTest_callJNI(JNIEnv *env, jobject obj) { // CToJavaTest-Klasse auslesen jclass cls = (*env)->GetObjectClass(env, obj); // jstring mit Text initialisieren const char* text = "Ich wurde von C gesetzt!"; jstring jstr = (*env)->NewStringUTF(env, text); // Adresse eines statichen Attributs auslesen jfieldID staticStringField = (*env)->GetStaticFieldID(env, cls, "staticStringValue", "Ljava/lang/String;"); // statisches Attribut setzen (*env)->SetStaticObjectField(env, cls, staticStringField, jstr); // Adresse des Objekt-Attributs auslesen jfieldID intField = (*env)->GetFieldID(env, cls, "intValue", "I"); // Wert des Attributs auslesen (funktioniert auch mit private Attributen) int intValue = (*env)->GetIntField(env, obj, intField); // Adresse einer Objekt-Methode auslesen jmethodID generateListMethod = (*env)->GetMethodID(env, cls, "generateList", "()Ljava/util/List;"); // Objekt-Methode aufrufen und Rückgabewert in eine Variable speichern jobject list = (*env)->CallObjectMethod(env, obj, generateListMethod); // statische Methode auslesen jmethodID printList = (*env)->GetStaticMethodID(env, cls, "printList", "(Ljava/util/List;)V"); // statische Methode mit Parametern aufrufen (*env)->CallStaticVoidMethod(env, cls, printList, list); // Größe der Liste auslesen jclass listClass = (*env)->GetObjectClass(env, list); jmethodID sizeMethod = (*env)->GetMethodID(env, listClass, "size", "()I"); int listSize = (*env)->CallIntMethod(env, list, sizeMethod); // Get-Methode zur Abfrage der Liste ermitteln jmethodID getMethod = (*env)->GetMethodID(env, listClass, "get", "(I)Ljava/lang/Object;"); // Ausgabe printf("C: intValue: %d\n", intValue); printf("C: printList reverse\n"); for (listSize = listSize - 1; listSize > -1; listSize--) { // jobject in jstring casten jstring cur = (jstring)(*env)->CallObjectMethod(env, list, getMethod, listSize); // jstring nach C konvertieren const char *c_cur = (*env)->GetStringUTFChars(env, cur, 0); printf("\tC: %s\n", c_cur); } return; }
Die ersten Zeilen unterscheiden sich nicht von bereits Bekanntem. Wie Sie sicherlich noch aus dem Einstiegs Kapitel zu JNI wissen, wird jeder Methode die aufrufende Klasse (bei statischen Methoden) bzw. das aufrufende Objekt (bei Objektmethoden) übergeben. Das ist in diesem Fall die Variable obj
vom Typ jobject
und repräsentiert hier ein Objekt der Klasse CToJavaTest
. Zusätzlich benötigen wir in dieser Methode auch noch die Klasse des Objekts. Diese erhalten wir mit mit dem Aufruf GetObjectClass
der JNI Umgebung:
jclass cls = (*env)->GetObjectClass(env, obj);
Auch hier wird, wieder zuerst die JNI Umgebung übergeben, gefolgt von dem jobject
, dessen Klasse ermittelt werden soll. Das Resultat wird als jclass
in eine Variable geschrieben.
Jetzt können wir uns ein wenig „austoben“. Zuerst verändern wir das statische String
Attribut der Klasse CToJavaTest
. Hierzu benötigen Sie einen jstring
. Dieser kann über die Methode NewStringUTF
mit einer C-Zeichenkette erzeugt werden. Zusätzlich muss auch hier mal wieder die JNI-Umgebung (im restlichen Kapitel wird auf die explizite Erwähnung dieses Parameters bei Funktionsaufrufen verzichtet) übergeben werden.
const char* text = "Ich wurde von C gesetzt!"; jstring jstr = (*env)->NewStringUTF(env, text);
Um das Attribut zu setzen, wird (ähnlich wie bei Reflection) zuerst eine Referenz auf das Attribut benötigt. Dies funktioniert bei statischen Attributen mit dem Aufruf GetStaticFieldID
. Möchten Sie auf ein nicht statisches Attribut zugreifen, muss stattdessen die Funktion GetFieldID
verwendet werden. Als Parameter wird die Klasse gesetzt, in der das Attribut zu finden ist, der Name des Attributs, und von welchem Typ (int
, String
, List
, …) das Attribut ist. Dieser Typ wird in Form einer eindeutigen Signatur übergeben.
jfieldID staticStringField = (*env)->GetStaticFieldID(env, cls, "staticStringValue", "Ljava/lang/String;");
Handelt es sich beim Typen des Attributs um einen primitiven Datentyp, wird eine der folgenden Abkürzungen verwendet:
Java | Abkürzung/Signatur |
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
void | V |
Ist das Attribut kein primitiver Datentyp, wird ein „L“, gefolgt vom vollständigen Klassennamen (inkl. Package) und einem abschließendem Semikolon gesetzt. Anstelle des Punktes, um Packages und Klassen voneinander zu trennen, wird hier ein normaler Schrägstrich verwendet. Bei Arrays wird vor die Signatur noch eine geöffnete, eckige Klammer geschrieben. Wenn Sie nun also einen int
ansprechen möchten, schreiben Sie als letzten Parameter ein „I“, bei einem long
-Array wäre es ein „[J“ und bei einem Object
ein „Ljava/lang/Object;“ (das Semikolon am Ende ist wichtig!).
Den Datentyp „void“ gibt es natürlich nicht als Attribut. Sehr wohl aber als Rückgabewert einer Methode. Aber hierzu später mehr.
Beim Setzen des statischen Attributs, wird die Methode SetStaticObjectField
mit der Klasse, in der das Attribut gesetzt werden soll, der Attribut-ID, und dem neuen Wert aufgerufen. Möchten Sie einen primitiven Datentyp setzen, muss im Funktionsnamen das Object durch den entsprechenden Typen ersetzt werden. Um einen int
zu setzen, rufen Sie bspw. die Methode SetStaticIntField
auf. Oder für einen boolean
die Methode SetStaticBooleanField
. Objektattribute werden ohne Static (bspw. SetObjectField
oder SetIntField
) im Methodennamen gesetzt. Außerdem wird das Objekt, in welchem das Attribut gesetzt werden soll, anstelle der Klasse übergeben.
(*env)->SetStaticObjectField(env, cls, staticStringField, jstr);
Als nächstes soll ein Objektattribut vom Typ int
ausgelesen werden. Auch hier benötigen Sie zuerst wieder die ID. Wie das funktioniert, wissen Sie bereits.
jfieldID intField = (*env)->GetFieldID(env, cls, "intValue", "I");
Anschließend können Sie über GetIntField
den Wert direkt als C-int
ermitteln. Auch hier ist wieder das Int im Methodennamen durch bspw. Object oder Boolean austauschbar. Bei statischen Attributen muss wieder ein Static in den Aufruf eingeschoben werden (bsp.: GetStaticObject
). Die Parameter sind identisch mit der zugehörenden Set-Methode. Lediglich der neu zu setzende Wert fällt selbstverständlich als letzter Parameter weg.
Werfen Sie noch einmal einen Blick in den Java Quellcode. Es fällt auf, dass das Attribut intValue private
ist. Sie können also auch private Attribute abfragen.
int intValue = (*env)->GetIntField(env, obj, intField);
Beim Aufruf einer Methode ist das Vorgehen recht ähnlich. Zuerst muss die Methoden-ID über GetMethodID
bzw. GetStaticMethodID
ermittelt werden. Die Parameter sind identisch mit denen von GetFieldID
. Lediglich die Signatur wird anders aufgebaut. Sie hat die Form „(Übergabeparameter ohne Leerzeichen aneinander gereiht)Rückgabewert„. Ein paar Beispiele:
public void doSomething()
– Signatur: „()V“
public void doSomething(int i)
– Signatur: „(I)V“
public void doSomething(int i, boolean b)
– Signatur: „(IZ)V“
public int getSomething(int i, boolean b, String str, double d)
– Signatur: „(IZLjava/lang/String;D)I“
Damit Sie die ID der Methode generateList
erhalten, ist folgender Code notwendig:
jmethodID generateListMethod = (*env)->GetMethodID(env, cls, "generateList", "()Ljava/util/List;");
Um die Methode aufzurufen, wird die Funktion CallObjectMethod
verwendet (bzw. wieder mit einem Static für statische Methoden (CallStaticObjectMethod
) oder einem primitiven Rückgabetyp (CallIntMethod
)). Je nachdem, ob es sich um eine statische Methode oder nicht handelt, wird die Klasse oder das Objekt der Methode übergeben. Anschließend folgt die Methoden-ID. Falls die Methode noch Parameter erwarten sollte, werden diese hinten als optionale Parameter angehängt. Da dies für die generateList
Methode nicht notwendig ist, lautet der Aufruf wie folgt:
jobject list = (*env)->CallObjectMethod(env, obj, generateListMethod);
In diesem jobject
befindet sich nun die Liste, die die Methode generateList
zurücklieferte.
Zum besseren Verständnis rufen wir jetzt noch die statische Methode printList
mit einem Übergabeparameter auf.
jmethodID printList = (*env)->GetStaticMethodID(env, cls, "printList", "(Ljava/util/List;)V"); (*env)->CallStaticVoidMethod(env, cls, printList, list);
Die restlichen Codezeilen in der Klasse sind nun nur noch Wiederholungen und sollten ohne Probleme verstanden werden. Hierbei ist es das Ziel, die ermittelten Daten (intValue
und der Inhalt unserer Liste) in C auszugeben. Damit dieses Ziel erreicht werden kann, müssen jedoch einige Vorbereitungen getroffen werden.
Um über die Liste zu iterieren, wird die Größe der Liste benötigt. Diese kann über den Methodenaufruf size()
der Liste ermittelt werden. Sie holen sich also zuerst wieder die Klasse der Liste, anschließend die Methoden-ID und rufen zum Schluss diese Methode auf. Der Rückgabewert ist die Größe der Liste.
jclass listClass = (*env)->GetObjectClass(env, list); jmethodID sizeMethod = (*env)->GetMethodID(env, listClass, "size", "()I"); int listSize = (*env)->CallIntMethod(env, list, sizeMethod);
Um an ein Element der Liste zu gelangen, benötigen Sie weiterhin die Methoden-ID der get(int i)
-Methode:
jmethodID getMethod = (*env)->GetMethodID(env, listClass, "get", "(I)Ljava/lang/Object;");
Sie haben alles beisammen, was Sie zur Ausgabe benötigen. Geben wir zuerst über printf
die intValue
aus.
printf("C: intValue: %d\n", intValue);
Um die Liste auszugeben benötigen Sie eine Schleife, die so oft durchlaufen wird, wie die Liste Elemente besitzt. In dieser Schleife rufen Sie dann die get
-Methode mit der aktuellen Position in der Schleife auf. Den Rückgabewert casten Sie in einen jstring
. Anschließend wird er über die Methode GetStringUTFChars
, die Sie bereits aus dem letzten Kapitel kennen, in eine C-Zeichenkette konvertiert. Diese kann dann über printf
ausgegeben werden.
printf("C: printList reverse\n"); for (listSize = listSize - 1; listSize > -1; listSize--) { jstring cur = (jstring)(*env)->CallObjectMethod(env, list, getMethod, listSize); const char *c_cur = (*env)->GetStringUTFChars(env, cur, 0); printf("\tC: %s\n", c_cur); }
Wenn Sie das Programm kompiliert und ausgeführt haben, sollten Sie folgende Ausgabe auf der Konsole erhalten:
Java: printList
Java: Eintrag: 1
Java: Eintrag: 2
Java: Eintrag: 3
Java: Eintrag: 4
Java: Eintrag: 5
C: intValue: 5
C: printList reverse
C: Eintrag: 5
C: Eintrag: 4
C: Eintrag: 3
C: Eintrag: 2
C: Eintrag: 1
Java: Ich wurde von C gesetzt!