Umgebung
Die aufgerufenen Bibliotheken müssen als dynamische Library zur Verfügung stehen. Das wären DLL unter Windows bzw. shared librarys unter den UNIX-Derivaten wie Linux oder Mac.Logischerweise brauchen wir also eine Java-Entwicklungsumgebung und einen C++-Compiler, der in der Lage ist, eine Shared Library zu generieren.
Grundsätzlich können alle möglichen Kombinationen verwendet werden. Im Beispiel wird Eclipse unter Linux verwendet. Eclipse hat mit dem CDT-Modul den Charme, dass es auch die C++-Übersetzung machen kann. Unter Linux steht mit dem GNU-C++-Compiler eine sehr gute Umgebung zur Verfügung.
Wer unter Windows arbeiten muss, kann aber auch mit NetBeans und dem Microsoft Visual C++ arbeiten. Gegebenenfalls muss man einige Schritte von Hand machen und einige Pfade suchen und festlegen, wenn es unter Windows dafür keinen Standard gibt.
Eclipse
Eclipse setze ich als installiert voraus. Das Paket eclipse-cdt enthält die C/C++-Einbindung unter Linux. Sie wird je nach Distribution über Synaptic, das Software-Center oder Muon aus dem Repository installiert.Für den Aufrufer wird ein gewöhnliches Java-Projekt namens JniCall angelegt. Sie können natürlich einen anderen Namen verwenden.
Der Java-Aufrufer JniCall
Die Klasse enthält natürlich eine Methode main, die ich in einer Klasse Main verborgen habe. Ungewöhnlich ist nur die C-Deklaration mit dem Schlüsselwort native.public class Main { public static native void rufeCpp(); // Deklaration der C-Methode public static void main(String[] args) { System.loadLibrary("JniCalled"); // laden der dynamischen lib System.out.println("Java ruft C++"); // Java arbeitet rufeCpp(); // rufe C++ } }
In der Hauptmethode wird die Shared Library mit dem Systemaufruf System.loadLibrary aufgerufen. Der Parameter ist der Name der Library. Unter Windows muss sie ein ".dll" angehängt bekommen. Unter UNIX-Ablegern wird lib davorgestellt und .so angehängt. Im Beispiel heißen die Shared Librarys also JniCalled.dll bzw. libJniCalled.so.
Dann macht das Java-Programm eine Bildschirmausgabe, um gleich anschließend die C++-Funktion rufeCpp aufzurufen, die sich in der Library JniCalled befindet.
Die aufgerufene C++-Bibliothek JniCalled
Für die Bibliothek wird ein neues C/C++-Projekt angelegt, das eine DLL bzw. eine shared object generiert. Unter Eclipse erreicht man das über File - New - Project. Hier C/C++. Als Project name verwenden wir JniCalled. Project type ist Shared Library - Empty Project.Ein neuer Header wird angelegt, in dem folgender Source steht: Entweder Sie tippen ihn ein oder schauen unten, wie er mit dem Programm javah generiert werden kann.
#ifndef JNICALLED_H_ #define JNICALLED_H_ #includeDaraus entwickelt man das Hauptprogramm der Shared Library, das nicht viel tut, außer Männchen zu machen.#ifdef __cplusplus extern "C" { #endif JNIEXPORT void JNICALL Java_Main_rufeCpp(JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif /* JNICALLED_H_ */
#include "JniCalled.h" #includeDer Header kann auch durch javah erzeugt werden. Von Eclipse kann man dieses als External Tool einbinden. Dazu ruft man über das Menü Run - External Tools - External Tools Configurations.using namespace std; JNIEXPORT void JNICALL Java_Main_rufeCpp(JNIEnv *, jclass) { cout << "C++ antwortet." << endl; }
In der linken Spalte Program doppelt anklicken und damit ein neues Tool erzeugen. Als Parameter geben wir folgendes ein:
- Name: JNI Header Creator (oder einfach javah oder etwas ganz anderes)
- Location: /usr/bin/javah
- Working Directory: ${workspace_loc:/JniCall/bin}
- Arguments: -jni -o ${workspace_loc:/JniCalled/JniCalled.h} Main
cd $HOME/workspace/JniCall/bin javah -jni -o $HOME/workspace/JniCalled/JniCalled.h Main
C++-Compiler auf JNI vorbereiten
Der C++-Compiler muss nun darauf vorbereitet werden, dass er mit JNI zusammenarbeitet und beispielsweise die Datei jni.h findet, die zum JDK gehört. Dazu klickt man das Projekt mit der rechten Maustaste an. In dem Menü wählt man die Eigenschaften Properties und gibt in Settings unter C/C++ Build - Settings im Dialog Tool Settings - GCC C++ Compiler Includes den folgenden Pfad an:/usr/lib/jvm/java-7-openjdk-i386/includeUnter GCC C++ Linker wird der Pfad der Library eingebunden
/usr/lib/jvm/java-7-openjdk-i386/libC++-Projekte werden nicht automatisch übersetzt, sondern müssen mit Project - Build Project von Hand ausgelöst werden. Auch ungewohnt ist, dass der C++-Compiler nur das übersetzt, was gesichert wurde. Änderungen, die im Editor vorgenommen wurden, aber noch nicht gespeichert sind, nimmt der Compiler nicht zur Kenntnis. Die Datei libJniCalled.so sollte nun entstehen.
Aufruf der Shared Library von Java aus
Die dynamische Library muss nun in einen Pfad, in dem sie Java findet. Auf die harte Tour kann man dies erzwingen, indem man die Shared Library in das bin-Verzeichnis der Eclipse-Projekt-Umgebung kopiert und dann den aktuellen Pfad mit einem Konsolenaufruf in den Java-Pfad zwingt:java -Djava.library.path="." Main Java ruft C++ C++ antwortet.Immerhin erscheint nun das Ergebnis. Wir haben das meiste richtig gemacht.
Um das Ganze zu automatisieren, kann man zumindest nach dem erfolgreichen Lauf des C++-Compilers die Shared Library in das Verzeichnis umkopieren lassen. Dazu findet man unter den Projekt-Eigenschaften unter C/C++ Build und Settings auf der rechten Seite eine Lasche Build Steps. Dort kann man unter Post-build steps den folgenden Befehl eintragen:
cp $(PWD)/lib*.so $(PWD)/../../JniCall/ressources/Im Java-Projekt JniCall wird nun unter Run - Run Configurations den Dialog Java Applications aufklappen. Unter Main steht im Dialog rechts eine Lasche (x)= Arguments. Unter VM arguments Folgendes eintragen:
-Djava.library.path="${workspace_loc:/JniCall/ressources}"Nun kann das Programm auch über Eclipse aufgerufen werden.
Parameter und Rückgabewerte
Rufen können wir nun. Jetzt sollen Daten fließen. Wir ändern zunächst den Prototyp des Aufrufs.public static native long rufeCpp(double wert, String text);Dann rufen wir javah über Run - External Tools. Automatisch wird eine neue Datei JniCalled.h erzeugt, die wir uns im C++-Projekt anschauen. Der Prototyp hat sich leicht verändert.
JNIEXPORT jlong JNICALL Java_Main_rufeCpp (JNIEnv *, jclass, jdouble, jstring);Diese Funktionsdeklaration kopieren wir in die Datei JniCalled.cpp und erweitern sie zu einer Funktion, die einfach nur die Zahl 24 zurückgibt.
JNIEXPORT jlong JNICALL Java_Main_rufeCpp(JNIEnv *, jclass, jdouble, jstring) { return 24; }Das Projekt wird mit Project - Build Project übersetzt. Dabei wird die dynamische Library automatisch umkopiert und wir können das Java-Projekt starten. Im Ausgabefenster erscheint, was wir erhofften:
Java ruft C++ zurück kam: 24Offensichtlich ist es einfach möglich, die im Java-Sprachgebrauch als primitiv bezeichneten Typen direkt zwischen C++ und Java untereinander zuzuweisen. Die Übergabetypen heißen jboolean, jbyte, jchar, jshort, jint, jlong, jfloat und jdouble und entstehen aus den ähnlich lautenden Java-Typen. Sie sind zu den analogen C++-Typen kompatibel.
Der String muss allerdings konvertiert werden. Dazu liefert JNI eine Funktion GetStringUTFChars, die über den Environment-Zeiger erreichbar ist.
JNIEXPORT jlong JNICALL Java_Main_rufeCpp(JNIEnv *jenv, jclass, jdouble pWert, jstring jStr) { double wert = pWert; const char *str = jenv->GetStringUTFChars(jStr, 0); cout << "Übergebener String: " << str << endl; return wert * 4; }
Objekte übergeben
Daten fließen nun. Nun sollen Objekte übergeben werden. Dazu definieren wir im Aufrufer eine Klasse namens Datum.class Datum { int tag, monat, jahr; } public class Main { public static native long rufeCpp(double wert, String text); // Deklaration der C-Methode public static native long rufeCpp(Datum datum); // Nun geht ein Objekt raus public static void main(String[] args) { System.loadLibrary("JniCalled"); System.out.println("Java ruft C++"); long zurueck = rufeCpp(12.5, "Nimm dies!"); System.out.println("zurück kam: " + zurueck); Datum gebtag = new Datum(); gebtag.tag = 24; gebtag.monat = 4; gebtag.jahr = 1909; zurueck = rufeCpp(gebtag); System.out.println("Tag: "+zurueck); } }Nun haben wir zwei native Methoden deklariert, die sich gegenseitig überladen. Dadurch ergibt sich beim Erzeugen der Headerdatei etwas Neues. Die Parameter fließen in den Funktionsnamen ein, da C im Gegensatz zu C++ kein Überladen kennt.
javah erzeugt nun die folgende Headerdatei JniCaller.h:
/* DO NOT EDIT THIS FILE - it is machine generated */ #includeDaraus übernehmen wir wieder die Funktionsrümpfe in die Datei JniCalled.cpp./* Header for class Main */ #ifndef _Included_Main #define _Included_Main #ifdef __cplusplus extern "C" { #endif /* * Class: Main * Method: rufeCpp * Signature: (DLjava/lang/String;)J */ JNIEXPORT jlong JNICALL Java_Main_rufeCpp__DLjava_lang_String_2 (JNIEnv *, jclass, jdouble, jstring); /* * Class: Main * Method: rufeCpp * Signature: (LDatum;)J */ JNIEXPORT jlong JNICALL Java_Main_rufeCpp__LDatum_2 (JNIEnv *, jclass, jobject); #ifdef __cplusplus } #endif #endif
Um nun auf das Attribut tag der Klasse zuzugreifen, verwenden wir unter C++ drei Schritte.
- Hole das Handle für die Klasse
- Hole mit dem Klassen-Handle das Handle für das Attribut
- Hole mit dem Attribut-Handle den Dateninhalt.
#include "JniCalled.h" #includeBeim Aufruf von GetFieldID werden zwei String-Konstanten übergeben. Die erste ist der Name des Attributs und die zweite stellt die JNI-Typkennung dar.using namespace std; JNIEXPORT jlong JNICALL Java_Main_rufeCpp__DLjava_lang_String_2 (JNIEnv *jenv, jclass, jdouble jWert, jstring jStr) { double wert = jWert; const char *str = jenv->GetStringUTFChars(jStr, 0); cout << "Übergebener String: " << str << endl; return wert * 4; } JNIEXPORT jlong JNICALL Java_Main_rufeCpp__LDatum_2 (JNIEnv *jenv, jclass, jobject jObj) { jclass jClass = jenv->GetObjectClass(jObj); jfieldID jField = jenv->GetFieldID(jClass, "tag", "I"); int tag = jenv->GetIntField(jObj, jField); return tag; }
Typ | Kennung | get-Funktion |
---|---|---|
boolean | "Z" | GetBooleanField() |
byte | "B" | GetByteField() |
char | "C" | GetCharField() |
short | "S" | GetShortField() |
int | "I" | GetIntField() |
long | "J" | GetLongField() |
float | "F" | GetFloatField() |
double | "D" | GetDoubleField() |
void | "V" | - |
Es gibt natürlich auch eine passende Set-Funktion, um ein Objekt-Attribut zu setzen.
jenv->SetIntField(jObj, jField, 23);
Aufruf einer Java-Methode von C++
Wir können von C++ nicht nur auf die Attribute einer Java-Klasse, sondern auch deren Methoden aufrufen. Der Ablauf ist wieder ganz ähnlich.- Hole das Handle für die Klasse
- Hole mit dem Klassen-Handle das Handle für die Methode
- Rufe mit dem Attribut-Handle die Methode auf.
JNIEXPORT jlong JNICALL Java_Main_rufeCpp__LDatum_2 (JNIEnv *jenv, jclass, jobject jObj) { jclass jClass = jenv->GetObjectClass(jObj); ... jmethodID jMethod = jenv->GetMethodID(jClass, "setJahr", "(I)I"); int jahr = jenv->CallIntMethod(jObj, jMethod, 1960); return jahr; }Bei GetMethodID muss der Name der Methode und die Signatur der Methode als String übergeben werden. Aber woher bekommen wir die Signatur? Hier können wir das Programm javap einsetzen. Auf der Konsole wechseln wir in das Verzeichnis bin des JniCall-Projekts. Dort rufen wir es für die Datumsklasse auf:
javap -s Datum.class class Datum { ... int setJahr(int); Signature: (I)I }Falls Sie eine Methode mit einem String als Parameter aufrufen wollen, sieht der Aufruf so aus:
class Datum { int tag, monat, jahr; String name; int setJahr(int pJahr) {jahr=pJahr; return jahr;} void setNamenstag(String pName) {name = pName;} }Nun liefert der Aufruf von javap:
javap -s Datum.class ... java.lang.String setNamenstag(java.lang.String); Signature: (Ljava/lang/String;)VDaraus ergibt sich dann Folgendes:
JNIEXPORT jlong JNICALL Java_Main_rufeCpp__LDatum_2 (JNIEnv *jenv, jclass, jobject jObj) { jclass jClass = jenv->GetObjectClass(jObj); ... jMethod = jenv->GetMethodID(jClass, "setNamenstag", "(Ljava/lang/String;)V"); jstring jStr = jenv->NewStringUTF("Grzimek"); jenv->CallVoidMethod(jObj, jMethod, jStr); }
Packages
javah -jni -o JniCalled.h de.beispiel.jni.MainDarauf entsteht die folgende JniCalled.h:
/* DO NOT EDIT THIS FILE - it is machine generated */ #includeDer Package-Name fließt also in den generierten Funktionsnamen ein. Der Grund ist klar. C beherrscht keine Packages oder Namensräume./* Header for class de_beispiel_jni_Main */ #ifndef _Included_de_beispiel_jni_Main #define _Included_de_beispiel_jni_Main #ifdef __cplusplus extern "C" { #endif /* * Class: de_beispiel_jni_Main * Method: rufeCpp * Signature: (DLjava/lang/String;)J */ JNIEXPORT jlong JNICALL Java_de_beispiel_jni_Main_rufeCpp (JNIEnv *, jclass, jdouble, jstring); #ifdef __cplusplus } #endif #endif