06. Juni 2009
Dieser Artikel bietet Ihnen eine Einführung in JNI und stellt den grundsätzlichen Entwicklungsprozess vor, den Sie durchlaufen, um JNI-Schnittstellen zu erstellen. Sie erhalten einen Überblick über die verschiedenen Themen, die JNI abdeckt, und lernen im Detail zahlreiche in der Praxis nützliche JNI-Funktionen kennen. Es wird Ihnen gezeigt, welche primitiven Datentypen in C denen in Java gegenüberstehen und welche speziellen JNI-Funktionen es gibt, um Strings und Arrays zu verarbeiten. Sie erfahren, wie auf Eigenschaften und Methoden zugegriffen wird und Ausnahmen in nativem Code geworfen werden. Abschließend sehen Sie, wie die Java-VM in einem C-Programm geladen und in dieses eingebettet werden kann.
Dieser Inhalt ist unter einer Creative Commons-Lizenz lizensiert.
JNI, kurz für Java Native Interface, heißt die Technologie, mit deren Hilfe von Java- auf C-Code und umgekehrt zugegriffen werden kann. So kann mit JNI ein Java-Programm C-Funktionen direkt aufrufen als auch ein in C entwickeltes Programm auf Java-Objekte zugreifen.
Da JNI die Schnittstelle zwischen der Java- und C-Welt ist, ist es notwendig, beide Programmiersprachen zu kennen. Ohne Kenntnisse in Java und in C wird es schwierig, JNI-Schnittstellen zu verstehen und selbst zu entwickeln.
Eine Technologie wie JNI, mit der Schnittstellen zwischen Programmen erstellt werden können, die in unterschiedlichen Programmiersprachen entwickelt sind, muss mehrere Probleme lösen:
Java und C basieren auf unterschiedlichen Paradigmen: Während Java objektorientiert ist, ist C eine prozedurale Programmiersprache. So gibt es in Java ausschließlich Methoden - also Funktionen, die zu Klassen gehören - während in C Funktionen immer freistehend sind. JNI muss also Java-Programmen ermöglich, auf C-Funktionen zuzugreifen, obwohl Java genaugenommen gar keine Funktionen kennt. C-Programme wiederum müssen mit Java-Objekten arbeiten, obwohl C gar keine Klassen kennt und es in C genaugenommen gar keine Objekte gibt.
Primitive Datentypen wie char
und int
gibt es sowohl in Java als auch in C. Sie belegen jedoch nicht zwangsläufig gleich viel Speicher. Während in Java eine char
-Variable immer genau zwei Byte groß ist, ist eine char
-Variable in C üblicherweise ein Byte groß. JNI muss also irgendwie zwischen primitiven Datentypen vermitteln.
In Java werden Objekte über Referenzen verfolgt und automatisch vom Garbage Collector freigegeben, wenn keine Referenz mehr auf ein Objekt verweist. Die Speicherverwaltung in Java erfolgt demnach automatisch. So können auch Objekte im Speicher verschoben werden, ohne dass dies einem Java-Programm auffallen würde. In C hingegen müssen Ressourcen vom Programmierer verwaltet werden. Sie müssen freigegeben werden, wenn sie nicht mehr benötigt werden, und verändern nicht automatisch ihre Position im RAM. Wenn ein C-Programm auf ein Java-Objekt zugreift, muss JNI sicherstellen, dass die automatische Speicherverwaltung in Java die Position des Objekts nicht verändert. In C wird nicht über Referenzen, sondern über Zeiger auf Java-Objekte zugegriffen - und diese Zeiger können Objekte nicht automatisch verfolgen, wenn sie im RAM an eine andere Position verschoben werden würden.
Java bietet Schlüsselwörter wie synchronized
an, die die Entwicklung eines Java-Programms vereinfachen können. So kann mit synchronized
der Zugriff von mehreren Threads auf gemeinsam genutzte Daten synchronisiert werden. Eine Schnittstelle wie JNI sollte versuchen, derartige Hilfsmittel in der anderen Programmiersprache verfügbar zu machen. So kennt C zwar kein Schlüsselwort synchronized
. Idealerweise kann aber mit anderen Hilfsmitteln in C der gleiche Effekt erzielt werden, um den Zugriff auf Daten mit in Java entwickelten Methoden zu synchronisieren.
Dieser Artikel ist lediglich eine Einführung in JNI. Sie werden erste eigene Schritte in der Entwicklung von JNI-Schnittstellen machen und einen guten Überblick über JNI erhalten. Für einen vollständigen und detaillierten Überblick wird die Dokumentation The Java Native Interface empfohlen, die unter anderem eine Referenz zu den rund 230 Funktionen enthält, die die JNI-Spezifikation definiert.
In diesem Abschnitt lernen Sie den grundsätzlichen Entwicklungsprozess kennen, um native Funktionen von Java aus aufzurufen. Native Funktionen bezeichnen dabei Funktionen, die als Maschinencode für den jeweiligen Prozessor vorliegen. Da die Systemprogrammiersprache vieler Betriebssysteme C ist, werden native Funktionen oft mit in C entwickelten Funktionen gleichgesetzt. Sie können aber auch Funktionen aufrufen, die in anderen Programmiersprachen entwickelt sind - immer vorausgesetzt, es stehen Compiler zur Verfügung, mit denen der entsprechende Quellcode in Maschinencode umgewandelt werden kann. In diesem Artikel sind die nativen Funktionen in den verschiedenen Beispielen vorwiegend in C und teilweise in C++ entwickelt.
Damit ein Java-Programm eine native Funktion aufrufen kann, muss die Funktion in Java deklariert werden. Da Java keine Funktionen kennt, sondern nur Methoden - also Funktionen, die zu Klassen gehören - muss diese Deklaration innerhalb einer Klasse erfolgen. Dabei wird auf das Schlüsselwort native
zugegriffen. Sehen Sie sich dazu folgende Java-Anwendung an.
class HelloWorld { native static void print(); public static void main(String[] args) { print(); } }
In obiger Beispielanwendung wird eine native Methode print()
deklariert. Dies geschieht, indem ausschließlich der Methodenkopf angegeben wird und vor diesen das Schlüsselwort native
gesetzt wird. Auf diese Weise wird dem Java-Compiler mitgeteilt, dass das Programm eine Methode print()
aufrufen wird, die keinen Parameter erwartet und keinen Rückgabewert besitzt und nicht in Java, sondern außerhalb des Programms entwickelt ist und in Maschinencode vorliegt.
Beachten Sie, dass im obigen Beispiel die Methode print()
statisch ist. Die Angabe von static
ist nur deswegen notwendig, weil main()
statisch ist. Wenn Sie in main()
die Klasse HelloWorld
instantiieren und dann für das Objekt print()
aufrufen, muss print()
natürlich nicht statisch sein. Dass print()
im obigen Beispiel statisch ist, hat also nichts damit zu tun, dass es sich um eine native Funktion handelt.
Wenn Sie obigen Code in einer Datei HelloWorld.java
speichern und die Datei mit javac HelloWorld.java
kompilieren, erhalten Sie wie gewohnt eine Binärdatei HelloWorld.class
. Sie können das Programm aber vorerst nicht ausführen. Schließlich wird eine native Funktion aufgerufen, die erst noch entwickelt und in irgendeiner Form dem Java-Programm zur Verfügung gestellt werden muss.
Während die Methode in Java einfach nur print()
heißt und keine Parameter erwartet, sieht die Signatur der nativen Funktion etwas anders aus. So muss der entsprechende Funktionsname Java_HelloWorld_print()
lauten. Außerdem muss diese Funktion zwei Parameter erwarten - einen vom Typ JNIEnv*
und einen anderen vom Typ jclass
. Es findet demnach eine Übersetzung statt, die nach ganz bestimmten Regeln erfolgt. So kann man zum Beispiel erkennen, dass der Name der nativen Funktion mit Hilfe des Klassen- und Methodennamens in Java gebildet und ein Java_ vorangestellt wird.
Glücklicherweise gibt eine einfache Möglichkeit, die Signatur von nativen Funktionen zu erhalten. Im Java SDK befindet sich ein Programm javah, das C-Funktionen deklariert und diese Deklarationen in einer Headerdatei ablegt. Dem Programm muss dazu als einziger Parameter der Name der Klasse übergeben werden, die mit native
deklarierte Methoden enthält. Die Klasse muss dabei in kompilierter Form vorliegen - javah sucht nach der entsprechenden class
-Datei. Wenn Sie obiges Beispiel kompiliert haben, erhalten Sie mit javah HelloWorld
eine Headerdatei HelloWorld.h
.
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class HelloWorld */ #ifndef _Included_HelloWorld #define _Included_HelloWorld #ifdef __cplusplus extern "C" { #endif /* * Class: HelloWorld * Method: print * Signature: ()V */ JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif
Die Headerdatei enthält neben der Deklaration der Funktion Java_HelloWorld_print()
ein paar Präprozessoranweisungen und Kommentare. Die einzige Besonderheit ist, dass in der zweiten Zeile eine Headerdatei jni.h
eingebunden wird, die aus dem Java SDK stammt. Diese Headerdatei enthält alle Typdefinitionen und deklariert alle JNI-Funktionen, die die JNI-Spezifikation kennt. Wenn Sie sich einen Überblick über JNI verschaffen möchten, können Sie diese Headerdatei in einem Texteditor öffnen und sich ansehen.
Nachdem Sie die Headerdatei erhalten haben, können Sie die Funktion Java_HelloWorld_print()
implementieren. Erstellen Sie zum Beispiel eine neue Datei HelloWorld.c
, in der Sie Java_HelloWorld_print()
definieren. Wenn Sie unter Windows arbeiten, greifen Sie beispielsweise auf die Funktion MessageBox()
zu, mit der unter Windows sehr einfach eine Meldung in einem Dialogfenster angezeigt werden kann. Da einfach nur eine Meldung angezeigt werden soll, werden die beiden Parameter von Java_HelloWorld_print()
nicht benötigt und können für den Moment ignoriert werden.
#include "HelloWorld.h" #include <windows.h> JNIEXPORT void JNICALL Java_HelloWorld_print(JNIEnv *env, jclass cl) { MessageBox(NULL, "Hello, world!", "JNI Message", MB_OK); }
Der nächste Schritt ist, die Datei HelloWorld.c
zu kompilieren. Verwenden Sie dazu einen beliebigen C-Compiler. So können Sie obige Datei zum Beispiel mit der von Microsoft kostenlos angebotenen Entwicklungsumgebung Visual C++ 2008 Express kompilieren.
Wichtig ist, dass Sie beim Linken eine dynamische Bibliothek erstellen. Java setzt voraus, dass beim Aufruf der Methode print()
die Funktion Java_HelloWorld_print()
in einer dynamischen Bibliothek gefunden wird. Unter Windows tragen diese Dateien die Endung dll
. Unter Linux und Unix sind sie typischerweise an der Endung so
zu erkennen.
Unter Windows müssen Sie also nach dem Kompilieren und Linken von HelloWorld.c
eine Datei HelloWorld.dll
erhalten. Welche Parameter Sie dazu an den Compiler übergeben müssen, hängt vom verwendeten Compiler ab. Wenn Sie mit der Entwicklungsumgebung Visual C++ 2008 Express arbeiten, erstellen Sie einfach ein neues Projekt vom Typ DLL. Es wird dann automatisch beim Erstellen des Projekts eine dll
-Datei erstellt.
Wenn Sie die dynamische Bibliothek erstellt haben, kopieren Sie die Datei in das Verzeichnis, in dem sich Ihr Java-Programm befindet. Bevor Sie das Programm starten, müssen Sie den Quellcode um eine Zeile ergänzen und ihn erneut kompilieren.
class HelloWorld { native static void print(); public static void main(String[] args) { System.loadLibrary("HelloWorld"); print(); } }
Im obigen Programm wird nun zusätzlich mit loadLibrary()
auf eine Methode der Klasse java.lang.System
zugegriffen. Dies ist notwendig, um die dynamische Bibliothek zu laden, die die Implementation der Methode print()
enthält. Ansonsten weiß Java nicht, wo es nach der entsprechenden Funktion Java_HelloWorld_print()
suchen soll. Java durchsucht also lediglich dynamische Bibliotheken, die explizit geladen wurden.
Beachten Sie, dass Sie keine Dateiendung angeben, wenn Sie mit loadLibrary()
eine dynamische Bibliothek laden. Java ergänzt die Dateiendung automatisch. So können Sie die Funktion Java_HelloWorld_print()
zum Beispiel auch unter Linux oder Unix in einer dynamischen Bibliothek HelloWorld.so
zur Verfügung stellen, ohne Ihr Java-Programm anpassen zu müssen.
Beachten Sie außerdem, dass Sie keinen Pfad auf die dynamische Bibliothek angeben dürfen, sondern ausschließlich den Dateinamen ohne Endung. Möchten Sie einen Pfad angeben, müssen Sie die Methode load()
verwenden, die ebenfalls von java.lang.System
angeboten wird. In diesem Fall müssen Sie die Dateiendung angeben.
Nachdem Sie den Quellcode neukompiliert haben, können Sie Ihr Java-Programm mit java HelloWorld
ausführen. Unter Umständen startet Ihr Programm nicht, und Sie erhalten folgende Fehlermeldung:
Exception in thread "main" java.lang.UnsatisfiedLinkError: no HelloWorld in java.library.path at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1709) at java.lang.Runtime.loadLibrary0(Runtime.java:823) at java.lang.System.loadLibrary(System.java:1030) at HelloWorld.main(HelloWorld.java:7)
In diesem Fall müssen Sie Java mitteilen, in welchem Verzeichnis es die dynamische Bibliothek suchen soll, die Sie mit loadLibrary()
laden. Standardmäßig sucht Java ausschließlich in den Verzeichnissen, die in der Systemeigenschaft java.library.path
angegeben sind. Sie können diese Systemeigenschaft aber temporär auf einen neuen Wert setzen, indem Sie einen zusätzlichen Parameter an java übergeben. Befindet sich die dynamische Bibliothek im aktuellen Verzeichnis, können Sie Ihr Java-Programm mit java -Djava.library.path=. HelloWorld
starten. Auf diese Weise geben Sie über die Kommandozeilenoption -D
an, dass die Systemeigenschaft java.library.path
temporär auf das aktuelle Verzeichnis verweisen soll.
Im vorherigen Abschnitt haben Sie den Entwicklungsprozess kennengelernt, der typischerweise durchlaufen wird, um von Java auf in C entwickelte Funktionen zuzugreifen. Im Folgenden soll nun die Methode print()
derart geändert werden, dass sie einen Parameter vom Typ int
erwartet, über den das Dialogfenster konfiguriert werden kann.
class HelloWorld { native static void print(int type); public static void main(String[] args) { System.loadLibrary("HelloWorld"); print(0x30); } }
In einem ersten Schritt wird die Deklaration der Methode print()
angepasst und um einen Parameter vom Typ int
ergänzt. Natürlich muss dann auch beim Aufruf ein int
-Wert übergeben werden. Im obigen Fall ist das 0x30
. Dieser Wert entspricht dem Windows-Makro MB_ICONEXCLAMATION
, mit dem einem Dialogfenster ein Ausrufezeichen hinzugefügt werden kann. Da das Windows-Makro MB_ICONEXCLAMATION
im Java-Code nicht verwendet werden kann, wird der Wert 0x30
direkt übergeben.
Im nächsten Schritt wird, nachdem der Quellcode kompiliert wurde und die Datei HelloWorld.class
vorliegt, mit Hilfe von javah eine neue Headerdatei erstellt.
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class HelloWorld */ #ifndef _Included_HelloWorld #define _Included_HelloWorld #ifdef __cplusplus extern "C" { #endif /* * Class: HelloWorld * Method: print * Signature: (I)V */ JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *, jclass, jint); #ifdef __cplusplus } #endif #endif
Der int
-Parameter der Methode print()
taucht als dritter Parameter vom Typ jint
in der Funktion Java_HelloWorld_print()
auf. Der Datentyp jint
in C entspricht also dem Datentyp int
in Java.
Die JNI-Spezifikation definiert für alle primitiven Datentypen in Java entsprechende Datentypen in C. Diese Datentypen heißen grundsätzlich genauso wie die Datentypen in Java - mit dem Unterschied, dass ihnen der Buchstabe j vorangestellt wird. Alle diese mit j beginnenden Datentypen sind lediglich Typdefinitionen, die auf primitiven Datentypen in C basieren. Folgende Tabelle gibt Ihnen einen Überblick, wie die primitiven Datentypen in Java den primitiven Datentypen in C entsprechen.
Java-Datentyp | C-Datentyp | Typdefinition in C |
---|---|---|
boolean |
jboolean |
unsigned char |
char |
jchar |
unsigned short |
byte |
jbyte |
signed char |
short |
jshort |
short |
int |
jint |
long |
long |
jlong |
__int64 |
float |
jfloat |
float |
double |
jdouble |
double |
Beachten Sie, dass die Typdefinition von jchar
auf short
und nicht auf char
basiert. Der Java-Datentyp char
ist ein 2-Byte-Typ, so dass die Typdefinition in C auf den 2-Byte-Typ short
zugreift.
Beachten Sie außerdem, dass einige Typdefinitionen compilerspezifisch sind. So ist in obiger Tabelle angegeben, dass jlong
als __int64
definiert ist. Bei __int64
handelt es sich um einen Datentyp, der ausschließlich von Microsoft-Compilern unterstützt wird. Für andere Compiler ist jlong
entsprechend anders definiert. So lautet die Typdefinition von jlong
für GCC unter Linux und Unix typischerweise long long
.
Wie Sie anhand obiger Tabelle erkennen können, ist jint
eine Typdefinition für long
. Da der vierte Parameter der Windows-Funktion MessageBox()
den Datentyp unsigned int
besitzt, kann der Parameter type mit dem Makro MB_OK
ODER-verknüpft werden, ohne dass eine explizite Typumwandlung notwendig ist. Indem im vierten Parameter, der an MessageBox()
übergeben wird, verschiedene Bits gesetzt werden, kann MessageBox()
angewiesen werden, zum Beispiel eine -Schaltfläche und ein Ausrufezeichen anzuzeigen.
#include "HelloWorld.h" #include <windows.h> JNIEXPORT void JNICALL Java_HelloWorld_print(JNIEnv *env, jclass cl, jint type) { MessageBox(NULL, "Hello, World!", "Java Message", MB_OK | type); }
Wenn Sie den Code kompilieren und die dynamische Bibliothek neu erstellen und dann das Java-Programm ausführen, sehen Sie ein Dialogfenster mit einem Ausrufezeichen.
Während primitiven Datentypen in Java primitive Datentypen in C gegenüberstehen, muss der Zugriff auf Objekte anders gelöst sein. Denn während es eine überschaubare und fixe Anzahl an primitiven Datentypen gibt, können jederzeit neue Klassen in Java erstellt werden, die in irgendeiner Weise in C abgebildet werden müssten - etwas, was auch dadurch erschwert wird, dass C keine objektorientierte Programmiersprache ist und Klassen nicht unterstützt.
Strings sind in Java basierend auf der Klasse java.lang.String
implementiert. Es handelt sich dabei um eine von unzähligen Klassen, die es in der Java-Standardbibliothek gibt. Obwohl Strings also ganz normale Objekte sind, besitzen sie eine Sonderstellung in Java. So können Strings initialisiert werden, ohne dass mit new
explizit ein neues Objekt erstellt werden muss. Außerdem ist es möglich, zwei Strings mit einem Pluszeichen zu verknüpfen. Da Strings so häufig verwendet werden, ist es nur sinnvoll, wenn Strings einfacher verarbeitet werden können.
Auch in der JNI-Spezifikation werden Strings bevorzugt. So werden Java-Strings in C durch ihren eigenen Datentypen jstring
identifiziert. Außerdem existieren verschiedene JNI-Funktionen, die ausschließlich auf Strings angewandt werden können. Um diese Funktionen kennenzulernen, soll die oben entwickelte Anwendung nun derart geändert werden, dass die Meldung, die im Dialogfenster ausgegeben werden soll, als Parameter an print()
übergeben wird.
class HelloWorld { native static void print(String s); public static void main(String[] args) { System.loadLibrary("HelloWorld"); print("Good morning!"); } }
Im ersten Schritt wird die Deklaration von print()
geändert, so dass beim Aufruf der Methode ein Parameter vom Typ java.lang.String
übergeben werden kann. Die Datei HelloWorld.java
muss daraufhin kompiliert werden, um mit javah die neue Headerdatei zu erhalten.
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class HelloWorld */ #ifndef _Included_HelloWorld #define _Included_HelloWorld #ifdef __cplusplus extern "C" { #endif /* * Class: HelloWorld * Method: print * Signature: (Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *, jclass, jstring); #ifdef __cplusplus } #endif #endif
Wie Sie anhand der Headerdatei erkennen, lautet der für java.lang.String
entsprechende Datentyp in C jstring
. JNI stellt nun verschiedene Funktionen bereit, um Variablen vom Typ jstring
zu verarbeiten. Eine dieser Funktion ist GetStringUTFChars()
, die es ermöglicht, für einen Java-String einen Zeiger vom Typ const char*
zu erhalten. Da in C Strings üblicherweise genau diesen Datentyp haben, ist GetStringUTFChars()
eine sehr häufig verwendete Funktion, um einen String in C weiterverarbeiten zu können.
In der vorliegenden Beispielanwendung soll der String, der als Parameter an Java_HelloWorld_print()
übergeben wurde, einfach nur im Dialogfenster angezeigt werden. Da der zweite Parameter von MessageBox()
den Datentyp const char*
besitzt, ist GetStringUTFChars()
die einzige Funktion, die wir brauchen.
#include "HelloWorld.h" #include <windows.h> JNIEXPORT void JNICALL Java_HelloWorld_print(JNIEnv *env, jclass cl, jstring s) { const char *c = (*env)->GetStringUTFChars(env, s, NULL); MessageBox(NULL, c, "JNI Message", MB_OK); (*env)->ReleaseStringUTFChars(env, s, c); }
Im obigen Beispielcode wird nun zum ersten Mal auf eine JNI-Funktion zugegriffen. Wie sehen erfolgt dies auf eine etwas komplizierte Art und Weise: GetStringUTFChars()
ist keine freistehende Funktion, sondern muss über die sogenannte JNI-Umgebung aufgerufen werden. Sie erhalten über den ersten Parameter von Java_HelloWorld_print()
, der den Datentyp JNIEnv*
hat, Zugriff auf diese JNI-Umgebung. Da alle JNI-Funktionen ausschließlich über diese JNI-Umgebung aufgerufen werden können, erhalten Sie in Ihren C-Funktionen immer als ersten Parameter einen Zeiger auf die Umgebung.
Wenn Sie eine JNI-Funktion wie GetStringUTFChars()
über die JNI-Umgebung aufrufen, müssen Sie außerdem immer als ersten Parameter an diese Funktion den Zeiger auf die JNI-Umgebung übergeben. Sie sehen das anhand des obigen Beispielcodes, in dem env als erster Parameter an GetStringUTFChars()
weitergereicht wird. Der Grund ist, dass JNI-Funktionen möglicherweise auch auf die JNI-Umgebung zugreifen wollen und deswegen einen entsprechenden Zeiger erwarten.
GetStringUTFChars()
gibt wie bereits erwähnt für einen Java-String vom Typ jstring
einen Zeiger vom Typ const char*
zurück. GetStringUTFChars()
erwartet neben dem Java-String jedoch einen weiteren Parameter vom Typ jboolean*
, den Sie so wie oben geschehen grundsätzlich auf NULL
setzen können.
Beachten Sie, dass einem Aufruf von GetStringUTFChars()
immer ein Aufruf von ReleaseStringUTFChars()
folgen muss. Das ist notwendig, weil beim Aufruf von GetStringUTFChars()
Ressourcen reserviert werden, die mit ReleaseStringUTFChars()
freigegeben werden. Wenn der Aufruf von ReleaseStringUTFChars()
vergessen wird, kann das langfristig zu einer Beeinträchtigung der Performance führen, weil Ressourcen, die eigentlich gar nicht mehr benötigt werden, blockiert sind.
Neben GetStringUTFChars()
bietet JNI weitere Funktionen zur Verarbeitung von Strings an: Während GetStringChars()
einen Zeiger vom Typ const jchar*
zurückgibt, erwarten GetStringUTFRegion()
und GetStringRegion()
einen Puffer vom Typ char*
bzw. jchar*
, um den String in diesen Puffer zu kopieren.
Wenn Sie C++ verwenden, kann Ihre Implementation von Java_HelloWorld_print()
vereinfacht werden. So können Sie zum einen einfacher auf JNI-Funktionen zugreifen und müssen diesen außerdem nicht zusätzlich einen Zeiger auf die JNI-Umgebung übergeben. Zum anderen können Sie sich des RAII-Idioms bedienen und Klassen entwickeln, die automatisch im Destruktor eine Ressource freigeben, so dass dies nicht vergessen werden kann. Sehen Sie sich dazu folgenden C++-Code an.
#include "HelloWorld.h" #include <windows.h> class utf_chars { public: utf_chars(JNIEnv *env, jstring s) : env_(env), s_(s), c_(env->GetStringUTFChars(s, NULL)) { } ~utf_chars() { env_->ReleaseStringUTFChars(s_, c_); } const char *get() { return c_; } private: JNIEnv *env_; jstring s_; const char *c_; }; JNIEXPORT void JNICALL Java_HelloWorld_print(JNIEnv *env, jclass cl, jstring s) { utf_chars utf(env, s); MessageBox(NULL, utf.get(), "JNI Message", MB_OK); }
Objekte vom Typ utf_chars
werden mit dem Zeiger auf die JNI-Umgebung und einem Java-String initialisiert. Im Konstruktor wird auf diese beiden Parameter zugegriffen, um GetStringUTFChars()
aufzurufen. Der Rückgabewert wird in einer Eigenschaft gespeichert. Damit auf diese Eigenschaft jederzeit zugegriffen werden kann, um den String vom Typ const char*
zu verarbeiten und zum Beispiel an MessageBox()
zu übergeben, ist eine Methode get()
definiert.
Der Vorteil, den die Klasse utf_chars
bietet, ist im Destruktor begründet: Dort wird automatisch ReleaseStringUTFChars()
aufgerufen. Weil ein Destruktor immer garantiert ausgeführt wird, wenn der Gültigkeitsbereich des entsprechenden Objekts endet, kann der Aufruf von ReleaseStringUTFChars()
nicht mehr vergessen werden.
Klassen wie utf_chars
sind auch dann wichtig, wenn auf Funktionen zugegriffen wird, die im Fehlerfall Ausnahmen werfen. Damit diese Ausnahmen nicht dazu führen, dass eine Funktion wie Java_HelloWorld_print()
vorzeitig abgebrochen wird und der Aufruf von ReleaseStringUTFChars()
am Ende der Funktion übersprungen wird, bietet sich das RAII-Idiom an, auf dem die Klasse utf_chars
basiert. Im Artikel über C++ Best Practices können Sie mehr über RAII erfahren.
Nachdem Sie die speziellen JNI-Funktionen für Strings kennengelernt haben, sehen Sie, wie Arrays verarbeitet werden. Ähnlich wie bei Strings handelt es sich grundsätzlich auch bei Arrays um ganz gewöhnliche Objekte. Da Arrays aber eine Aneinanderreihung von Objekten sind und in Java über einen Index auf einzelne Elemente in einem Array zugegriffen werden kann, stehen verschiedene JNI-Funktionen zur Verfügung, um einen derartigen Zugriff auch in C-Code zu bewerkstelligen.
Die Beispielanwendung wird nun dahingehend geändert, dass die Kommandozeilenparameter, die beim Programmstart angegeben und an main()
übergeben werden, im Dialogfenster angezeigt werden sollen.
class HelloWorld { native static void print(String[] a); public static void main(String[] args) { System.loadLibrary("HelloWorld"); print(args); } }
Wie üblich wird in einem ersten Schritt der Java-Code angepasst, um ihn dann zu kompilieren und mit javah die Headerdatei zu erhalten.
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class HelloWorld */ #ifndef _Included_HelloWorld #define _Included_HelloWorld #ifdef __cplusplus extern "C" { #endif /* * Class: HelloWorld * Method: print * Signature: ([Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *, jclass, jobjectArray); #ifdef __cplusplus } #endif #endif
Der Datentyp eines jeden Java-Arrays in C ist grundsätzlich jarray
. Enthält das entsprechende Array Objekte - dazu gehören auch Strings - wird der Datentyp mit jobjectArray
spezifiziert. Das ist hier der Fall. Würde es sich um ein Array handeln, das int
-Werte speichert, wäre der entsprechende Datentyp in C jintArray
. Der Grund, warum es verschiedene Array-Datentypen gibt, ist natürlich wieder der, dass es verschiedene JNI-Funktionen gibt, die auf die unterschiedlichen Array-Datentypen angewandt werden können.
#include "HelloWorld.h" #include <string.h> #include <windows.h> JNIEXPORT void JNICALL Java_HelloWorld_print(JNIEnv *env, jclass cl, jobjectArray a) { char buffer[1024] = { 0 }; int size = (*env)->GetArrayLength(env, a); int i; for (i = 0; i < size; ++i) { jobject ob = (*env)->GetObjectArrayElement(env, a, i); const char *c = (*env)->GetStringUTFChars(env, ob, NULL); strcat(buffer, c); (*env)->ReleaseStringUTFChars(env, ob, c); } MessageBox(NULL, buffer, "JNI Message", MB_OK); }
Die Größe des Arrays wird mit Hilfe der Funktion GetArrayLength()
ermittelt. Um auf ein Element in einem Array zuzugreifen, das Objekte enthält, wird GetObjectArrayElement()
aufgerufen. Dieser Funktion muss entsprechend ein Index übergeben werden.
Die beiden anderen JNI-Funktionen, die im obigen Code-Beispiel aufgerufen werden, kennen Sie bereits aus dem vorherigen Abschnitt zur String-Verarbeitung.
Beachten Sie, dass der Rückgabewert von GetObjectArrayElement()
den Datentyp jobject
hat, der Rückgabewert aber dennoch problemlos an die Funktion GetStringUTFChars()
übergeben werden kann, die einen Parameter vom Typ jstring
erwartet. Das Programm funktioniert problemlos, weil wir wissen, dass ein String-Array im Java-Programm an die C-Funktion übergeben wird. Da Strings auch Objekte sind, verwenden wir die JNI-Funktion GetObjectArrayElement()
, um auf einen String im Array zuzugreifen. Wir erhalten zwar einen Rückgabewert vom Typ jobject
- so ist GetObjectArrayElement()
definiert. Aber weil wir wissen, dass es sich bei diesem Objekt um einen String handelt, können wir den Rückgabewert als Parameter an GetStringUTFChars()
übergeben.
Rufen Sie die Java-Anwendung zum Beispiel mit java HelloWorld Hello!
auf, wird im Dialogfenster "Hello!" angezeigt.
Neben GetArrayLength()
und GetObjectArrayElement()
stehen weitere JNI-Funktionen zur Verfügung, um Arrays zu verarbeiten. So gibt zum Beispiel GetIntArrayElements()
einen Zeiger vom Typ jint*
zurück, über den auf alle Elemente in einem int
-Array zugegriffen werden kann - also auf ein Array vom Typ jintArray
. Für Arrays, in denen Werte anderer primitiver Datentypen gespeichert sind, stehen ähnliche Funktionen zur Verfügung, die GetByteArrayElements()
, GetShortArrayElements()
, GetDoubleArrayElements()
etc. heißen. Greifen Sie auf diese Funktionen zu, müssen Sie ähnlich wie bei Strings entsprechende Release-Funktionen verwenden, um Ressourcen wieder freizugeben. Die entsprechende Funktion zu GetIntArrayElements()
heißt zum Beispiel ReleaseIntArrayElements()
. Wenn Sie in C++ programmieren, empfiehlt sich die Entwicklung von Klassen basierend auf dem RAII-Idiom. Sehen Sie sich dazu das Code-Beispiel im Abschnitt zur String-Verarbeitung an.
Sie haben bisher gesehen, welche JNI-Funktionen Ihnen zur Verfügung stehen, um mit Objekten vom Typ String und Array zu arbeiten. Wenn Sie mit Objekten arbeiten, die auf anderen Klassen basieren, stehen keine speziellen JNI-Funktionen zur Verfügung. Um mit diesen Objekten zu arbeiten, müssen Sie auf Eigenschaften und Methoden zugreifen, die die Objekte anbieten.
Eigenschaften und Methoden werden in JNI über ID-Werte identifiziert, die vom Typ jfieldID
oder jmethodID
sind. Um auf eine Eigenschaft oder Methode zuzugreifen, muss demnach zuerst ihr ID-Wert ermittelt werden. Dazu muss auf die entsprechende Klasse zugegriffen werden, um sie nach der Eigenschaft oder Methode zu durchsuchen.
Das bisher entwickelte Java-Programm soll nun insofern geändert werden, als dass der String, der im Dialogfenster ausgegeben wird, als Parameter an die C-Funktion übergeben und dann mit Hilfe der Methode toUpperCase()
, die von java.lang.String
angeboten wird, in Großbuchstaben umgewandelt werden soll.
Wie üblich ist der erste Schritt, die Deklaration von print()
im Java-Code anzupassen.
class HelloWorld { native static void print(String s); public static void main(String[] args) { System.loadLibrary("HelloWorld"); print("Good morning!"); } }
Nachdem print()
geändert wurde und einen Parameter vom Typ java.lang.String
erwartet, wird der Code kompiliert. Im Anschluß wird mit javah die Headerdatei erzeugt.
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class HelloWorld */ #ifndef _Included_HelloWorld #define _Included_HelloWorld #ifdef __cplusplus extern "C" { #endif /* * Class: HelloWorld * Method: print * Signature: (Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *, jclass, jstring); #ifdef __cplusplus } #endif #endif
Nachdem Sie die Headerdatei erstellt haben, kann die Funktion Java_HelloWorld_print()
implementiert werden.
#include "HelloWorld.h" #include <windows.h> JNIEXPORT void JNICALL Java_HelloWorld_print(JNIEnv *env, jclass cl, jstring s) { jclass cl_string = (*env)->FindClass(env, "java/lang/String"); jmethodID id_uppercase = (*env)->GetMethodID(env, cl_string, "toUpperCase", "()Ljava/lang/String;"); jobject ob_uppercase = (*env)->CallObjectMethod(env, s, id_uppercase); const char *c = (*env)->GetStringUTFChars(env, ob_uppercase, NULL); MessageBox(NULL, c, "JNI Message", MB_OK); (*env)->ReleaseStringUTFChars(env, ob_uppercase, c); }
Die C-Funktion soll wie bereits erwähnt den String in Großbuchstaben umwandeln, bevor er im Dialogfenster ausgegeben wird. Die Klasse java.lang.String
bietet hierzu praktischerweise die Methode toUpperCase()
an. Um nun diese Methode aufzurufen, muss ihr ID-Wert ermittelt werden. Dazu muss zuerst ein Zugriff auf die entsprechende Klasse erfolgen, die die benötigte Methode definiert - in diesem Fall java.lang.String
.
In der JNI-Spezifikation sind zahlreiche Funktionen definiert, die den Zugriff auf Klassen erlauben und sie nach Eigenschaften und Methoden durchsuchen. Die beiden Funktionen, die in diesem Fall benötigt werden, sind FindClass()
und GetMethodID()
. FindClass()
erwartet als Parameter einen vollständig qualifizierten Klassennamen, wobei die Pakete und der Klassenname durch Schrägstriche getrennt werden müssen. Wenn also die Klasse java.lang.String
benötigt wird, muss als Parameter "java/lang/String" übergeben werden.
Der Rückgabewert von FindClass()
hat den Typ jclass
. Es handelt sich um einen Datentypen, der eine Java-Klasse beschreibt. Sie verwenden den Rückgabewert, um ihn zum Beispiel an die Funktion GetMethodID()
zu übergeben. Mit dieser Funktion suchen Sie nach einer Methode in der entsprechenden Klasse, um ihren ID-Wert zu erhalten.
Wie Sie anhand des obigen Beispiels sehen, wird GetMethodID()
nicht nur der Name der gesuchten Methode übergeben, sondern auch die Methodensignatur in einer etwas merkwürdigen Schreibweise. Der Grund, warum die Methodensignatur angegeben werden muss, ist, dass Methoden überladen sein können. Eine Klasse kann also eine Methode mehrfach definieren, wobei sich die Definitionen im Datentypen oder in der Anzahl der Parameter unterscheiden. Damit in einem derartigen Fall der ID-Wert der gewünschten Methode ermittelt werden kann, erwartet die Funktion GetMethodID()
in einem weiteren Parameter die Methodensignatur.
Die Kodierung der Methodensignatur erfolgt nach ganz bestimmten Regeln, die Sie jedoch nicht auswendig lernen müssen. Ein weiteres Hilfsprogramm im Java SDK namens javap ermittelt die kodierten Methodensignaturen automatisch. So rufen Sie das Programm mit javap -s java.lang.String
auf, um die kodierten Methodensignaturen für die Klasse java.lang.String
zu erhalten. Vergessen Sie nicht, beim Aufruf -s
anzugeben - ohne diese Kommandozeilenoption erhalten Sie eine Liste der von java.lang.String
angeboten Methoden, aber nicht in kodierter Form.
Da java.lang.String
sehr viele Methoden besitzt, sehen Sie im Folgenden lediglich einen Auszug aus der Liste.
Compiled from "String.java" public final class java.lang.String extends java.lang.Object implements java.io. Serializable,java.lang.Comparable,java.lang.CharSequence{ ... public java.lang.String toUpperCase(); Signature: ()Ljava/lang/String; ... }
Mit Hilfe von javap lässt sich leicht die kodierte Methodensignatur für toUpperCase()
ermitteln, die in diesem Fall ()Ljava/lang/String;
lautet.
Wenn Sie von GetMethodID()
den ID-Wert erhalten haben, können Sie zum Beispiel mit Hilfe der Funktion CallObjectMethod()
die entsprechende Methode aufrufen. Sie müssen dieser Funktion dabei die Referenz auf das Objekt übergeben und natürlich den ID-Wert der Methode.
Die im obigen Beispiel verwendeten JNI-Funktionen stehen stellvertretend für verschiedene Kategorien. So erhalten Sie auch dann eine Klassenbeschreibung vom Typ jclass
, wenn Sie die Funktion GetObjectClass()
verwenden und ihr als Parameter einen Wert vom Typ jobject
übergeben. Folgende Tabelle gibt Ihnen einen Überblick über die JNI-Funktionen, mit denen eine Klassenbeschreibung vom Typ jclass
erhalten werden kann.
Funktion | Beschreibung |
---|---|
FindClass() |
Erwartet einen String in der Form "Paket/Unterpaket/Klassename". |
GetObjectClass() |
Erwartet einen Parameter vom Typ jobject und sollte daher dann verwendet werden, wenn ein entsprechendes Objekt existiert und nicht mit FindClass() nach der entsprechenden Klasse gesucht werden muss. |
GetSuperclass() |
Erwartet eine Klassenbeschreibung vom Typ jclass und gibt die Elternklasse zurück. |
Neben GetMethodID()
existieren weitere Funktionen, um unter anderem die ID-Werte für Eigenschaften zu erhalten.
Funktion | Beschreibung |
---|---|
GetMethodID() |
Erwartet eine Klassenbeschreibung, den Namen der gesuchten Methode und ihre Signatur in kodierter Form. |
GetFieldID() |
Erwartet eine Klassenbeschreibung, den Namen der gesuchten Eigenschaft und seinen Datentyp in kodierter Form. |
GetStaticMethodID() |
Erwartet eine Klassenbeschreibung, den Namen der gesuchten statischen Methode und ihre Signatur in kodierter Form. |
GetStaticFieldID() |
Erwartet eine Klassenbeschreibung, den Namen der gesuchten statischen Eigenschaft und seinen Datentyp in kodierter Form. |
Mit den ID-Werten kann eine Funktion wie CallObjectMethod()
aufgerufen werden. Diese Funktion heißt deswegen so, weil sie als Ergebnis einen Wert vom Typ jobject
zurückgibt. Der Funktionsname enthält demnach den Datentyp des Rückgabewertes. Da Methoden auch Ergebnisse anderer Datentypen zurückgegeben können, stehen neben CallObjectMethod()
entsprechende weitere Funktionen zur Verfügung.
Funktion | Beschreibung |
---|---|
CallObjectMethod() |
Ruft eine Methode mit einem Rückgabewert vom Typ jobject auf. |
CallBooleanMethod() |
Ruft eine Methode mit einem Rückgabewert vom Typ jboolean auf. |
CallByteMethod() |
Ruft eine Methode mit einem Rückgabewert vom Typ jbyte auf. |
CallCharMethod() |
Ruft eine Methode mit einem Rückgabewert vom Typ jchar auf. |
CallShortMethod() |
Ruft eine Methode mit einem Rückgabewert vom Typ jshort auf. |
CallIntMethod() |
Ruft eine Methode mit einem Rückgabewert vom Typ jint auf. |
CallLongMethod() |
Ruft eine Methode mit einem Rückgabewert vom Typ jlong auf. |
CallFloatMethod() |
Ruft eine Methode mit einem Rückgabewert vom Typ jfloat auf. |
CallDoubleMethod() |
Ruft eine Methode mit einem Rückgabewert vom Typ jdouble auf. |
CallVoidMethod() |
Ruft eine Methode ohne Rückgabewert auf. |
Alle in obiger Tabelle aufgelisteten Funktionen können unabhängig von der Anzahl der von einer Methode erwarteten Parameter aufgerufen werden. So bietet zum Beispiel die Klasse java.lang.String
die Methode toUpperCase()
zweimal an - einmal ohne Parameter, einmal mit einem Parameter. Da in beiden Fällen der Rückgabewert den Datentypen jobject
hat, würde jedesmal auf die Funktion CallObjectMethod()
zugegriffen werden. Die Funktionen in obiger Tabelle unterstützen eine beliebige Anzahl an Parametern, so dass Sie egal, welche der beiden toUpperCase()
-Methoden Sie verwenden, auf die gleiche Funktion CallObjectMethod()
zugreifen können. Sie müssen also nur anhand des Datentypen des Rückgabewertes entscheiden, welche der obigen Funktionen Sie aufrufen. Sie müssen keine Entscheidung treffen bezüglich der Anzahl der Parameter, die eine Methode erwartet.
Die Funktionen in obiger Tabelle dürfen Sie nur verwenden, wenn Sie auf eine Methode eines Objekts zugreifen. Möchten Sie auf eine Eigenschaft zugreifen oder auf eine statische Methode oder Eigenschaft einer Klasse, müssen Sie Funktionen einer anderen Kategorie verwenden. Da die Kategorien jeweils identisch sind, werden sie nicht zusätzlich tabellarisch aufgelistet. Es reicht zu wissen, dass der Zugriff auf eine Eigenschaft vom Typ jobject
über eine Funktion GetObjectField()
erfolgt, auf eine statische Methode mit einem Rückgabewert vom Typ jobject
über CallStaticObjectMethod()
und auf eine statische Eigenschaft vom Typ jobject
über GetStaticObjectField()
. Da Eigenschaften nicht nur gelesen, sondern auch gesetzt werden können, stehen außerdem Funktionen wie SetObjectField()
und SetStaticObjectField()
zur Verfügung.
Viele JNI-Funktionen geben als Ergebnis Zeiger zurück, von denen wir in den bisherigen Beispielen davon ausgingen, dass sie immer gültig sind. JNI-Funktionen können jedoch auch fehlschlagen. Sie geben dann einen NULL-Zeiger zurück, so dass die Rückgabewerte von JNI-Funktionen immer überprüft werden sollten.
Wenn eine JNI-Funktion fehlschlägt, wird nicht nur ein NULL-Zeiger zurückgegeben, sondern auch eine Ausnahme geworfen - aber erst dann, wenn die native Funktion beendet wird und sich die Programmausführung wieder im Java-Code befindet. Solange die native Funktion ausgeführt wird, wird von einer schwebenden Ausnahme gesprochen. Liegt eine schwebende Ausnahme vor, darf bis auf wenige Ausnahmen keine JNI-Funktion mehr aufgerufen werden. Zu diesen Ausnahmen zählen zum Beispiel die verschiedenen Release-Funktionen, um Ressourcen freizugeben.
Schlägt also eine JNI-Funktion fehl, liegt eine schwebende Ausnahme vor. Da bis auf wenige Ausnahmen keine JNI-Funktionen mehr aufgerufen werden dürfen, sollte eine C-Funktion so schnell wie möglich beendet werden.
Das Beispiel aus dem Abschnitt zur String-Verarbeitung wird im Folgenden um eine Fehlerbehandlung ergänzt.
#include "HelloWorld.h" #include <windows.h> JNIEXPORT void JNICALL Java_HelloWorld_print(JNIEnv *env, jclass cl, jstring s) { const char *c = (*env)->GetStringUTFChars(env, s, NULL); if (c) { MessageBox(NULL, c, "JNI Message", MB_OK); (*env)->ReleaseStringUTFChars(env, s, c); } }
Wie Sie sehen wird lediglich überprüft, ob c gültig ist. Ist in c nach dem Aufruf von GetStringUTFChars()
ein NULL-Zeiger gespeichert, wird der Aufruf von MessageBox()
übersprungen und auch nicht ReleaseStringUTFChars()
aufgerufen. Wenn die Funktion Java_HelloWorld_print()
beendet wird und sich die Programmausführung im Java-Code befindet, wird automatisch eine Ausnahme geworfen. Sie müssen also nicht selbst eine Ausnahme werfen, wenn eine JNI-Funktion fehlschlägt.
Nicht immer ist es möglich, anhand des Rückgabewertes zu erkennen, ob es einen Fehler gab und eine schwebende Ausnahme vorliegt. Wenn Sie zum Beispiel für einen String die Methode charAt()
aufrufen und ihr einen ungültigen Index übergeben, wirft charAt()
eine Ausnahme. Sie können also nicht anhand des Rückgabewertes erkennen, ob ein Fehler vorliegt.
#include "HelloWorld.h" #include <windows.h> JNIEXPORT void JNICALL Java_HelloWorld_print(JNIEnv *env, jclass cl, jstring s) { jclass cl_string; jmethodID id_charat; jchar ch; jthrowable ex; cl_string = (*env)->FindClass(env, "java/lang/String"); if (!cl_string) return; id_charat = (*env)->GetMethodID(env, cl_string, "charAt", "(I)C"); if (!id_charat) return; ch = (*env)->CallCharMethod(env, s, id_charat, 999); ex = (*env)->ExceptionOccurred(env); if (ex) return; }
Da charAt()
im Fehlerfall eine Ausnahme wirft, müssen Sie die JNI-Funktion ExceptionOccurred()
verwenden, um zu überprüfen, ob eine schwebende Ausnahme vorliegt. Diese Funktion besitzt einen Rückgabewert vom Typ jthrowable
. Nachdem es sich dabei um einen Zeiger handelt, überprüfen Sie, ob er gültig oder auf NULL
gesetzt ist. Handelt es sich nicht um einen NULL-Zeiger, wissen Sie, dass eine schwebende Ausnahme vorliegt.
Sie können eine schwebende Ausnahme auch löschen, um dann zum Beispiel Ihre eigene Ausnahme zu werfen.
#include "HelloWorld.h" #include <windows.h> JNIEXPORT void JNICALL Java_HelloWorld_print(JNIEnv *env, jclass cl, jstring s) { jclass cl_string; jmethodID id_charat; jchar ch; jthrowable ex; cl_string = (*env)->FindClass(env, "java/lang/String"); if (!cl_string) return; id_charat = (*env)->GetMethodID(env, cl_string, "charAt", "(I)C"); if (!id_charat) return; ch = (*env)->CallCharMethod(env, s, id_charat, 999); ex = (*env)->ExceptionOccurred(env); if (ex) { (*env)->ExceptionClear(env); ex = (*env)->FindClass(env, "java/lang/RuntimeException"); if (!ex) return; (*env)->ThrowNew(env, ex, "Error in native code!"); } }
Die JNI-Funktion ExceptionClear()
ermöglicht es, eine schwebende Ausnahme zu löschen. Mit ThrowNew()
wiederum kann jederzeit selbst eine Ausnahme geworfen werden.
In allen Beispielen in diesem Artikel wurde bisher ein Java-Programm gestartet, das dann auf eine in einer dynamischen Bibliothek zur Verfügung gestellte C-Funktion zugriff. Es ist jedoch auch möglich, die Java-VM in ein C-Programm einzubetten. Sehen Sie sich dazu folgendes Beispiel an.
#include <jni.h> int main() { JavaVM *jvm; JNIEnv *env; JavaVMInitArgs jvmargs; jint r; jvmargs.nOptions = 0; jvmargs.version = JNI_VERSION_1_6; r = JNI_CreateJavaVM(&jvm, (void**)&env, &jvmargs); if (r < 0) return -1; (*jvm)->DestroyJavaVM(jvm); }
Mit der Funktion JNI_CreateJavaVM()
wird die virtuelle Java-Maschine geladen. Dieser Funktion müssen drei Parameter übergeben werden: Ein Zeiger auf JavaVM*
, über den auf die virtuelle Maschine zugegriffen werden kann, ein Zeiger auf JNIEnv*
, der die JNI-Umgebung darstellt und Zugriff auf JNI-Funktionen ermöglicht, und ein Zeiger auf eine Struktur namens JavaVMInitArgs
, um die Java-VM zu initialisieren.
Über den Zeiger vom Typ JavaVM*
kann auf nur wenige Funktionen der Java-VM zugegriffen werden. Eine dieser Funktionen ist DestroyJavaVM()
, mit der die Java-VM zerstört werden kann, so wie es im obigen Beispiel am Ende der Funktion main()
geschieht.
Mit Hilfe der Struktur JavaVMInitArgs
wird eine Java-VM initialisiert. Es müssen mindestens die beiden Eigenschaften nOptions und version gesetzt werden. Während nOptions auf 0 gesetzt werden kann, muss version auf eine gültige Versionsnummer gesetzt werden. In der Headerdatei jni.h
sind dazu verschiedene Makros definiert, unter anderem auch JNI_VERSION_1_6
. Über nOptions und eine weitere Eigenschaft options, die den Datentyp JavaVMOption*
hat, ist es möglich, die Java-VM zu konfigurieren.
Wenn der Rückgabewert von JNI_CreateJavaVM()
0 ist, konnte die Java-VM erfolgreich geladen und initialisiert werden. Es kann dann auf Java-Klassen zugegriffen werden, um Objekte zu erstellen und mit Eigenschaften und Methoden zu arbeiten.
Wenn Sie obigen Code kompilieren und versuchen, das C-Programm auszuführen, erhalten Sie möglicherweise eine Fehlermeldung - jedenfalls dann, wenn Sie die Java Runtime von Sun einsetzen. Das Problem ist, dass ein Programm, in dem die Java-VM eingebettet ist, von einer dynamischen Bibliothek abhängt. Unter Windows heißt diese Datei jvm.dll
. Diese Bibliothek muss gefunden und geladen werden, damit das C-Programm ausgeführt werden kann. Normalerweise lädt ein Betriebssystem dynamische Bibliotheken automatisch. Dabei werden jedoch nur ganz bestimmte Verzeichnisse nach den benötigten Bibliotheken durchsucht. Und leider wird bei der Installation der Java Runtime von Sun unter Windows das entsprechende Verzeichnis, in dem sich die Bibliothek befindet, nicht zu dieser Liste hinzugefügt. Sie können das nachholen, indem Sie das Verzeichnis - es sollte ähnlich wie C:\Programme\Java\jre6\bin\client
lauten - zur Systemvariablen PATH
hinzufügen.
Im Folgenden wird das Beispiel derart erweitert, dass die C-Anwendung mit Hilfe der Klasse javax.swing.JDialog
ein Dialogfenster anzeigt.
#include <jni.h> int main() { JavaVM *jvm; JNIEnv *env; JavaVMInitArgs jvmargs; jint r; jclass cl; jmethodID m; jobject ob; jstring s; const char *c = "Hello, world!"; jvmargs.nOptions = 0; jvmargs.version = JNI_VERSION_1_6; r = JNI_CreateJavaVM(&jvm, (void**)&env, &jvmargs); if (r < 0) return -1; cl = (*env)->FindClass(env, "javax/swing/JDialog"); if (!cl) return -1; m = (*env)->GetMethodID(env, cl, "<init>", "(Ljava/awt/Frame;Ljava/lang/String;)V"); if (!m) return -1; s = (*env)->NewStringUTF(env, c); if (!s) return -1; ob = (*env)->NewObject(env, cl, m, 0, s); (*env)->ReleaseStringUTFChars(env, s, c); if (!ob) return -1; m = (*env)->GetMethodID(env, cl, "setDefaultCloseOperation", "(I)V"); if (!m) return -1; (*env)->CallVoidMethod(env, ob, m, 2); m = (*env)->GetMethodID(env, cl, "show", "()V"); if (!m) return -1; (*env)->CallVoidMethod(env, ob, m); (*jvm)->DestroyJavaVM(jvm); }
Wenn Sie sich obigen Code ansehen, sollten Ihnen die meisten JNI-Funktionen bekannt vorkommen. Lediglich die Funktionen NewStringUTF()
und NewObject()
sind neu: Mit diesen Funktionen wird ein neuer String vom Typ jstring
bzw. ein neues Objekt vom Typ jobject
erstellt. NewStringUTF()
wird benötigt, um dem Dialogfenster den Titel "Hello, world!" zu geben. Und auf NewObject()
wird zugegriffen, um ein neues Objekt vom Typ javax.swing.JDialog
zu erstellen.
Beachten Sie außerdem, dass Sie "<init>" an GetMethodID()
übergeben müssen, um den ID-Wert für den Konstruktor einer Klasse zu erhalten.
Copyright © 2009 Boris Schäling