Java JNI

Willemers Informatik-Ecke

2013-05-15

Über das Java Native Interface kann Java Bibliotheken anderer Programmiersprachen, insbesondere C und C++ aufrufen. Da diese in der Regel nicht plattformunabhängig sind, wird ein Programm, das JNI verwendet, nicht mehr plattformunabhängig sein.

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_
#include 

#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT void JNICALL Java_Main_rufeCpp(JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif

#endif /* JNICALLED_H_ */
Daraus entwickelt man das Hauptprogramm der Shared Library, das nicht viel tut, außer Männchen zu machen.
#include "JniCalled.h"
#include 
using namespace std;

JNIEXPORT void JNICALL Java_Main_rufeCpp(JNIEnv *, jclass)
{
	cout << "C++ antwortet." << endl;
}
Der 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.

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
Dieser Aufruf erzeugt im Verzeichnis von JniCall eine Datei JniCalled.h. Dahinter steckt die Verwendung des Header-Generators javah. Die Ausführung entspricht den beiden Befehlen:
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/include
Unter GCC C++ Linker wird der Pfad der Library eingebunden
/usr/lib/jvm/java-7-openjdk-i386/lib
C++-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: 24
Offensichtlich 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 */
#include 
/* 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
Daraus übernehmen wir wieder die Funktionsrümpfe in die Datei JniCalled.cpp.

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.
Im Quelltext sieht das dann so aus:
#include "JniCalled.h"
#include 
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;
}
Beim Aufruf von GetFieldID werden zwei String-Konstanten übergeben. Die erste ist der Name des Attributs und die zweite stellt die JNI-Typkennung dar.

TypKennung 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.
Der erste Schritt ist schon bekannt. Nun müssen die nächsten beiden Schritte durchgeführt werden.
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;)V
Daraus 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.Main
Darauf entsteht die folgende JniCalled.h:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include 
/* 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
Der Package-Name fließt also in den generierten Funktionsnamen ein. Der Grund ist klar. C beherrscht keine Packages oder Namensräume.


Homepage (C) Copyright 2013 Arnold Willemer