Boris Schäling

06. Juni 2009


JNI: Java Native Interface

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.


Inhaltsverzeichnis


1. Allgemeines

Schnittstelle zwischen der Java-Welt und nativem Code

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:

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.


2. Aufruf nativer Funktionen

Von Java auf C-Funktionen zugreifen

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.

Windows-Dialogfenster

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.


3. Konvertierung primitiver Datentypen

Primitive Datentypen in Java und in C

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.

Tabelle 1. Primitive Datentypen in Java und C
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.

Windows-Dialogfenster mit Ausrufezeichen

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 OK-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.


4. String-Verarbeitung

Spezielle JNI-Funktionen für Strings

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.

Windows-Dialogfenster mit variablen Text

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.


5. Verarbeiten von Arrays

Spezielle JNI-Funktionen für Arrays

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.

Windows-Dialogfenster mit variablen Text

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.


6. Zugriff auf Eigenschaften und Methoden

Klassen und Objekte

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.

Windows-Dialogfenster mit variablen Text

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.

Tabelle 2. JNI-Funktionen für Klassenbeschreibungen
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.

Tabelle 3. JNI-Funktionen, um ID-Werte 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.

Tabelle 4. JNI-Funktionen, um Methoden aufzurufen
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.


7. Fehlerbehandlung

Ausnahmen fangen und werfen

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.


8. Java-VM in C-Programm einbetten

Von einem C-Programm auf Java-Klassen zugreifen

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.