Unter native Code versteht man Programme, die in einer bestimmten (von Java verschiedenen) Programmiersprache bereits implementiert sind. Oftmals wird eine Schnittstelle zwischen native Code (bspw. C-Programme) und Java benötigt, da bereits bestehende Programme in Java integrierbar sein sollten. Hierzu kann das Java Native Interface (JNI) verwendet werden. Java-Programme, die in JNI geschrieben sind, sind nach wie vor plattformunabhängig. Mittels JNI kann ein Java-Programm, das in einer Java Virtual Machine (VM) ausgeführt wird, mit anderen Anwendungen (bspw. Bibliotheken, die in C, C++ oder Assembler-Sprachen geschrieben sind) zusammenarbeiten. Umgekehrt kann mittels des Invocation API eine Java Virtual Machine in Anwendungen von native Code integriert werden. Java kann daher sowohl zur Steuerung anderer Anwendungen als auch als Teil derartiger Programme verwendet werden. Abb. 3.14: Java Native Interface (JNI) Während es oftmals praktisch ist, eine derartige Schnittstelle zur Verfügung zu haben, existieren auch Anwendungsfälle, in denen eine Anwendung nicht in Java geschrieben werden kann und eine Kooperation über JNI daher unerlässlich ist: die Java-Klassenbibliothek kann eventuell plattformabhängige Eigenschaften nicht unterstützen, die von einer Anwendung benötigt werden. Eine Anwendung oder Bibliothek existiert bereits und soll nicht in Java reimplementiert werden. Zeitkritischer Code soll in einer maschinennahen Sprache implementiert werden und aus Java heraus aufgerufen werden. In JNI können Methoden in native Code in derselben Art und Weise verwendet werden, wie Java-Code derartige Objekte benutzen würde. Eine solche Methode kann Java-Objekte erzeugen und anschließend auf diesen operieren, wobei die Java-Objekte gleichzeitig im Java-Programm zur Verfügung stehen. Mittels JNI können daher Objekte zwischen Java und anderen Programmiersprachen ausgetauscht werden. Umgekehrt können Methoden in native Code auch Java-Methoden aufrufen, die bspw. in Form einer Java-Bibliothek vorhanden sind. Derartige Aufrufe beinhalten auch die Übergabe von Parametern. Durch JNI können insbesondere die Vorteile von Java hervorragend ausgenutzt werden, bspw. indem Ausnahmen von Methoden in native Code aus Java heraus abgefangen werden. Das in Abb. 3-14 dargestellte Schaubild zeigt den Zusammenhang zwischen Methoden in native Code, Java und JNI nochmals auf. Programmierung mit JNI Im Folgenden wird erläutert, wie native Code in Java-Programme integriert werden kann. Hierzu sind die folgenden Schritte zu durchlaufen: Schreiben eines Java-Programms, das eine Klasse enthält, die eine Methode aus native Code in Form einer Signatur deklariert und diese in einer main-Methode aufruft. Übersetzen der Java-Klasse. Erzeugen einer Header-Datei der Methode aus native Code mittels javah mit der Option „-jni". Die Header-Datei enthält anschließend die formale Signatur der Methode. Implementierung der Methode in einer beliebigen Programmiersprache (bspw. C). Übersetzen der Header- und der Implementierungsdatei in eine Shared Library. Ausführen des Java-Programms. Diese Schritte werden im Folgenden detailliert betrachtet. Hierzu wird eine Anwendung entwickelt, die die Fakultät einer Zahl berechnet. Die eigentliche Berechnung wird in C implementiert. Schreiben des Java-Programms Das folgende Java-Programm definiert eine Klasse, in der eine Methode aus native Code deklariert wird: class JNIBeispiel { native int fakultaet(int wert); static { System.loadLibrary("fak"); } public static void main(String[] args) { int i = new JNIBeispiel().fakultaet(3); } } Zuerst muss die Methode aus native Code innerhalb einer Java-Klasse deklariert werden. Da die Methode in einer von Java verschiedenen Sprache implementiert werden soll, muss das Schlüsselwort native angegeben werden, wodurch dem Java-Compiler mitgeteilt wird, dass eine Funktion einer anderen Sprache verwendet werden soll. Die Methode wird lediglich als Signatur deklariert, die eigentliche Implementierung in einer anderen Programmiersprache wird in einer separaten Datei gespeichert. Die Methodendeklaration reflektiert weiterhin, dass die Methode eine public-Instanz ist, die als Argument eine Zahl akzeptiert, und die einen Integer-Wert zurückgibt. Die Klasse JNIBeispiel verwendet die Methode System.loadLibrary, um die in Schritt 5 erzeugte Shared Library zu laden, die erzeugt wird, wenn der Implementierungs-Code übersetzt wird. Diese Methode muss als static deklariert werden und erwartet als Argument den Namen der Shared Library, der beliebig wählbar ist. Das System konvertiert in diesem Fall den Namen der Bibliothek in einen Namen der native Library. In Windows-Systemen wird bspw. der Name fak.dll erzeugt. Die nun folgende statische Initialisierung lädt die Bibliothek fak. Das Laufzeitsystem führt diese Anweisung aus, wenn die Klasse geladen wird. Als Application enthält die Klasse JNIBeispiel eine main-Methode, in der die Klasse instantiiert wird und in der die Methode aufgerufen wird. Der Aufruf der Methode erfolgt in JNI in derselben Form wie der anderer Java-Methoden. Erzeugung der Header-Datei Nachdem das in Schritt 1 erzeugte Programm kompiliert wurde (javac JNIBeispiel.java), muss im dritten Schritt die Hilfsroutine javah dazu verwendet werden, eine Header-Datei aus der Datei JNIBeispiel.class zu erzeugen (javah JNIBeispiel). Die Header-Datei enthält die Signatur, die zur Implementierung der C-Methode notwendig ist. Der Name der Header-Datei ist in diesem Fall Beispiel.h, also der Klassenname mit angehängtem Suffix „.h". javah speichert diese Datei im selben Verzeichnis ab, in dem sich auch die .class-Datei der Java-Klasse befindet. Mittels der Option -h kann hier allerdings auch ein anderes Verzeichnis angegeben werden. In der Header-Datei befindet sich dann die Zeile: JNIEXPORT jint JNICALL Java_JNIBeispiel_fakultaet (JNIEnv *, jobject, jint); Die Funktion Java_JNIBeispiel_fakultaet realisiert die Implementierung der in der Java-Klasse definierten Methode aus native Code fakultaet (siehe nächster Schritt). Der Name der zu implementierenden Funktion ergibt sich immer dadurch, dass zuerst der Term „Java", anschließend der Klassenname (JNIBeispiel) und zum Schluss der Name der zu implementierenden Funktion getrennt durch Underscores angegeben wird. Diese Reihenfolge würde sich auch ergeben, wenn mehrere native Funktionen verwendet worden wären. Die Parameter, die an die Funktion übergeben werden, bestehen aus einem JNIEnv-Objekt, einem jobject und der Zahl, die an die Funktion übergeben wird (Typ jint für Integer). Die ersten beiden Parameter werden bei jeder JNI-Methode erzeugt. Der erste Parameter definiert einen Zeiger auf das JNIEnv-Interface, mit dessen Hilfe die Methode aus native Code auf Parameter und Objekte zugreift, die aus der Java-Anwendung heraus übergeben werden. Der zweite Parameter vom Typ jobject referenziert das derzeitige Objekt selbst, in der Art, wie dies in Java auch der this-Zeiger realisiert. Implementierung von native Code Die Funktion, die nun implementiert werden kann, muss dieselbe Signatur haben, wie die in der Header-Datei angegebene. Die C-Implementierung der Funktion, die in der Datei JNIBeispielImp.c gespeichert wird, sieht dann wie folgt aus: #include <JNIBeispiel.h> JNIEXPORT jint JNICALL Java_JNIBeispiel_fakultaet (JNIEnv *env, jobject obj, jint wert) { int i; i = fakultaet(wert); return i; } //Rekursive Berechnung der Fakultaet int fakultaet(int i) { int wert = i; if (wert > 2) wert = wert*fakultaet(i-1); return(wert); } Erzeugung einer Shared Library Das Java-Programm verwendet zum Laden von native Code die Anweisung System.load (siehe Schritt 1). System.load("fak"); Die im vorangehenden Schritt generierte Datei JNIBeispielImp.c muss nun in eine Shared Library namens fak übersetzt werden. Die notwendigen Befehle zur Generierung einer Shared Library sind stark vom System bzw. von der darauf installierten Software abhängig. Der Leser sollte daher feststellen, welchen C-Compiler er verwendet und wie hiermit eine derartige Bibliothek erzeugt werden kann. Ausführung des Programms Nachdem alle Schritte durchlaufen wurden, kann das Programm ausgeführt werden (java JNIBeispiel). Sollte hierbei eine Fehlermeldung auftreten, so sollte überprüft werden, ob der Library-Pfad, der unter anderem den Speicherort der verwendeten Shared Library enthält, richtig gesetzt ist. Unter UNIX kann dazu bspw. die Anweisung setenv LD_LIBRARY_PATH Name_des_Library_Pfades verwendet werden. Integration von Java und native Code JNI standardisiert die Namensgebung sowie die Aufrufstruktur, so dass die Java Virtual Machine Methoden aus native Code lokalisieren und aufrufen kann. Das hierzu notwendige Wissen ist Thema dieses Unterkapitels. Deklaration nativer Methoden Zur korrekten Deklaration einer Methode aus native Code sind die folgenden Regeln zu beachten: Methoden aus native Code müssen in Java mit dem Schlüsselwort native gekennzeichnet werden. Dieses Schlüsselwort teilt dem Compiler mit, dass die Implementierung der Methode in einer anderen Sprache vorliegt. Die Deklaration von Methoden aus native Code ist mit einem Semikolon abzuschließen, da die Implementierung der Methode nicht in der Java-Klasse erfolgt. Vor der Implementierung von native Code sollte die Header-Datei mit Hilfe von javah erzeugt werden, die die Funktionsprototypen der Methoden enthält. Die Funktionsdefinition der Implementierung muss sich exakt nach dem Namen richten, der in der Header-Datei enthalten ist. JNIEXPORT und JNICALL müssen in jeder Funktionssignatur vorkommen. JNIEXPORT und JNICALL bewirken, dass der Quellcode auch auf solchen Plattformen übersetzt werden kann, die spezielle Schlüsselwörter für Funktionen fordern, die aus dynamischen Link-Libraries (DLLs) exportiert werden. Eine Besonderheit ergibt sich, wenn überladene Methodennamen verwendet werden. In diesem Fall weicht die Namensgebung der Methode aus native Code von der bisherigen Regel, das Schlüsselwort „Java", einen Underscore, den Klassennamen, einen weiteren Underscore und den Funktionsnamen zu verwenden, ab. Logischerweise reicht diese Definition nicht mehr aus, da überladene Funktionen jeweils denselben Namen, aber unterschiedliche Argumentlisten haben. Aus genau diesem Grund wird die Namensgebung dahingehend erweitert, dass an den sonst auch verwendeten Namen zwei aufeinander folgende Underscores angehängt werden und zusätzlich die Argumentsignatur. Ein Beispiel für eine derartige Funktion ist im Folgenden angegeben: Java_JNIBeispiel_quadratzahl__jint_wert Java_JNIBeispiel_quadratzahl__jfloat_wert Beziehung zwischen Java-Typen und Typen in native Code Java-Typen müssen in Methoden von native Code referenziert werden, wenn auf Argumente zugegriffen werden soll, die von einer Java-Anwendung an eine Methode aus native Code übergeben werden, Java-Objekte in einer Methode aus native Code angelegt werden sollen oder eine Methode aus native Code ein Ergebnis zurückgeben soll. Methoden aus native Code können einfache Java-Datentypen, die als Argumente übergeben werden, direkt verwenden. Der Java-Typ int wurde in den Beispielen bereits auf den Typ jint abgebildet. In Tab. 3-12 ist die hierzu notwendige Abbildung angegeben. Java-Objekte werden mittels Call-by-Reference an JNI übergeben und auf den Typ jobject abgebildet. JNI implementiert bereits eine Menge von Typen, die von jobject abgeleitet sind: jobject bezeichnet alle Java-Objekte, jstring für String-Objekte, jclass für Klassenobjekte, jthrowable für Exception-Objekte und jarray für Array-Objekte. Abgeleitet von jarray sind die folgenden Typen: jbooleanArray , jbyteArray, jcharArray, jdoubleArray, jfloatArray, jintArray, jlongArray und jshortArray für Arrays der Typen boolean, byte, char, double, float, int, long und short und jobjectArray für Objekt-Arrays |
Java-Typ | Typ in native Code | Größe in bit
boolean | jboolean | 8, ohne Vorzeichen
byte | jbyte | 8
char | jchar | 16, ohne Vorzeichen
short | jshort | 16
int | jint | 32
long | jlong | 64
float | jfloat | 32
double | jdouble | 64
void | void | --
Tab. 3.12: Abbildung von Java-Typen auf native Code Java-Aufrufe aus native Code JNI bietet Standard-Interface-Funktionen an, mit deren Hilfe Java-Objekte aus native Code heraus erzeugt, verwendet und auch manipuliert werden können. Zugriff auf Java-Strings Eine Java-Anwendung übergibt einen String an eine Methode aus native Code in Form eines jstring-Objekts. Der Typ jstring differiert allerdings von dem in C verwendeten Typ String, weshalb derartige Objekte nicht direkt verwendbar sind. Native Code muss daher JNI-Funktionen benutzen, um Java-Strings in native Strings umzuwandeln. JNI unterstützt hierzu die Umwandlung von Unicode (der üblicherweise in Java verwendet wird) in UTF-8-Strings. UTF-8-Strings sind aufwärtskompatibel mit 7-bit ASCII. Zur Umwandlung eines Strings muss die Methode aus native Code die Methode GetStringUTFChars aufrufen, um einen String korrekt auszugeben. GetStringUTFChars konvertiert die Unicode-Repräsentation eines Java-Strings in einen UTF-8-String. Enthält ein String allerdings nur 7-bit ASCII-Zeichen, so kann der String direkt an ein native Code-Segment übergeben werden, bspw. folgendermaßen: JNIEXPORT JNICALL Java_JNIBsp_zeile (JNIEnv *env, jobject obj, jstring z){ const char *s = (*env)->GetStringUTFChars(env, z, 0); printf("%s\n", s); //Aufheben der Referenzierung (*env)->ReleaseStringUTFChars(env, z, s); } Ist die Bearbeitung des Strings abgeschlossen, so muss die Methode ReleaseString-UTFChars aufgerufen werden. ReleaseStringUTFChars teilt der Java-VM mit, dass die Methode aus native Code den String nicht länger verwendet und dass folglich der Speicher freigegeben werden kann. Im Gegenzug resultiert das Weglassen dieses Aufrufs in der Verschwendung von Speicherplatz. Eine Methode aus native Code kann auch einen neuen String anlegen, indem die Methode NewStringUTF aufgerufen wird. Dies ist im folgenden Beispiel angegeben. JNIEXPORT JNICALL Java_JNIBsp_zeile (JNIEnv *env, jobject obj, jstring z){ char puffer[128]; scan("%s", puffer); const char *s = (*env)->NewStringUTF(env, puffer); printf("%s\n", s); } Methoden aus native Code können auf Java-Objekte mit Hilfe des env-Zeigers zugreifen. Beispiele hierfür sind die Anwendungen der Funktionen GetStringUTF-Chars und ReleaseStringUTFChars in den vorangegangenen Beispielen. JNI stellt auch Möglichkeiten bereit, die Unicode-Repräsentation von Java-Strings zu erzeugen. Dies ist vor allem auf Systemen sinnvoll, die auch in native Code Unicode verwenden. Hierzu stehen die folgenden Funktionen zur Verfügung: GetStringChars akzeptiert als Argument einen Java-String und gibt einen Zeiger auf einen Array von Unicode-Zeichen zurück. ReleaseStringChars löscht diesen Zeiger wieder. NewString erzeugt einen neuen Java-String aus einem Array von Unicode-Zeichen. GetStringLength stellt die Länge eines Strings fest, der aus Unicode-Zeichen besteht. GetStringUTFLength stellt analog die Länge eines Strings fest, der im UTF-8-Format vorliegt. Zugriff auf Java-Arrays JNI verwendet den Typ jarray, um Java-Arrays zu referenzieren. Auch auf Objekte vom Typ jarray kann aus native Code nicht direkt zugegriffen werden. JNI stellt Methoden bereit, die diesen Zugriff erlauben (siehe Tab. 3-13). |
Funktion | Array-Typ
GetBooleanArrayElements | boolean
GetByteArrayElements | byte
GetCharArrayElements | char
GetShortArrayElements | short
GetIntArrayElements | int
GetLongArrayElements | long
GetFloatArrayElements | float
GetDoubleArrayElements | double
Tab. 3.13: Array-Zugriff in JNI Ähnlich wie bei Strings müssen Array-Objekte wieder freigegeben werden, wenn sie in native Code nicht weiter verwendet werden. Die hierzu notwendigen Methoden sind in Tab. 3-14 angegeben. |
Funktion | Array-Typ
ReleaseBooleanArrayElements | boolean
ReleaseByteArrayElements | byte
ReleaseCharArrayElements | char
ReleaseShortArrayElements | short
ReleaseIntArrayElements | int
ReleaseLongArrayElements | long
ReleaseFloatArrayElements | float
ReleaseDoubleArrayElements | double

Tab. 3.14: Array-Freigabe in JNI Die Anwendung dieser Methoden wird im folgenden Beispiel demonstriert. Hierbei wird ein Array an ein Programm in native Code übergeben. Zuerst wird die Länge des Arrays festgestellt, indem die JNI-Methode GetArrayLength aufgerufen wird. Im Gegensatz zu anderen Programmiersprachen merkt sich Java immer die Länge von Arrays. Anschließend wird ein Zeiger auf die Elemente des Arrays erzeugt. JNI enthält Methoden, die auf Arrays eines beliebigen einfachen Typs (boolean, byte, char, short, int, long, float und double) zugreifen können. Wie aus Tab. 3-14 ersichtlich, enthält das JNI eine Menge an Funktionen zur Erzeugung von Array-Zeigern. Hierbei wird in der Notation Get<Typ>ArrayElements jeweils der Typ so gesetzt, dass er den Elementen des Arrays entspricht. Zum Zugriff auf einen Array aus Double-Elementen muss dann bspw. die Methode GetDoubleArrayElements verwendet werden. Da das folgende Beispiel eine Liste von Integer-Werten enthält, muss die JNI-Methode GetIntArrayElements verwendet werden. Nachdem der Zeiger generiert wurde, kann der resultierende Integer-Array mit C-Kommandos verarbeitet werden. JNIEXPORT jint JNICALL Java_Beispiel_bspArray(JNIEnv *env, jobject obj, jintArray a) { int i, r; jsize laenge = (*env)->GetArrayLength(env, a); jint *zeiger = (*env)->GetIntArrayElements(env, a, 0); for (i=0; i<laenge; i++) { //Zugriff auf Elemente, bspw. mit zeiger[i]; r = zeiger[i]; } //Freigeben des Speichers (*env)->ReleaseIntArrayElements(env, a, zeiger, 0); return r; } Im Allgemeinen werden während der Garbage Collection Java-Arrays verschoben, um eine Speicherfragmentierung zu vermeiden. Dies ist problematisch, da in native Code mit Zeigern gearbeitet wird, die nach einer Verschiebung eines Arrays nicht mehr korrekt verwendet werden können. Aus diesem Grund garantiert die Java Virtual Machine, dass in JNI erzeugte Zeiger auf einen nicht verschiebbaren Array zeigen. Dies wird dadurch realisiert, dass der Speicherbereich entweder gesperrt wird oder dass eine Kopie des Arrays in einem nicht verschiebbaren Bereich erstellt wird. Aus diesem Grund muss aber JNI den belegten Speicher wieder freigeben. Ähnlich wie bei der Deklaration der Methoden Get<Typ>ArrayElements stellt das JNI eine Menge von Freigabemethoden bereit, die die Syntax Release<Typ>ArrayElements aufweisen. Im Beispiel wird daher die Methode ReleaseIntArrayElements aufgerufen, wenn die Bearbeitung des Arrays abgeschlossen ist. Mittels ReleaseIntArrayElements kann JNI den Array zurückkopieren, wenn eine Kopie des ursprünglichen Java-Arrays erstellt wurde, und den belegten Speicherbereich wieder freigeben. Durch die Kopieroperation erhält das aufrufende Java-Programm die eventuell in native Code modifizierten Werte des Arrays. Problematisch ist, dass durch Get<Typ>ArrayElements eventuell eine Kopie des gesamten Arrays angelegt wird. Dieser Vorgang verschwendet insbesondere bei sehr großen Arrays Ressourcen in einem nicht zu vernachlässigenden Umfang. Die Zahl der zu kopierenden Elemente kann aber begrenzt werden, indem die JNI-Methoden Get/Set<Typ>ArrayRegion verwendet werden, mit denen auf eine eingeschränkte Anzahl von Array-Elementen durch Kopieroperationen zugegriffen werden kann. Zum Zugriff auf Objekt-Arrays werden in JNI separate Methoden verwendet, mit deren Hilfe auf einzelne Elemente zugegriffen werden kann. Auf die Gesamtheit aller Elemente kann allerdings nicht in einer atomaren Operation zugegriffen werden. JNI beinhaltet hierzu die Funktionen: GetObjectArrayElement gibt das Objekt-Element an einem bestimmten Index zurück und SetObjectArrayElement setzt das Objekt-Element an einem bestimmten Index auf einen neuen Wert. Aufruf von Java-Methoden in native Code Zum Aufruf einer Java-Instanzmethode in native Code sind die folgenden drei Schritte zu durchlaufen: Die Methode aus native Code ruft die JNI-Funktion GetObjectClass auf, die das Java-Klassenobjekt zurückgibt. Die Methode ruft anschließend die JNI-Funktion GetMethodID auf, die die Bezeichnung (ID) einer Java-Methode in einer gegebenen Java-Klasse feststellt. Dieser Vorgang basiert auf dem Methodenname und auf der Methodensignatur. Sollte die Java-Methode nicht existieren, so liefert die Funktion GetMethodID den Wert 0 zurück. Wird die Methode in native Code an dieser Stelle unmittelbar beendet, so wird in Java eine NoSuchMethodError-Ausnahme generiert. Im letzten Schritt ruft die Methode in native Code die JNI-Funktion CallVoidMethod auf, die wiederum eine Instanzmethode aufruft, die den Rückgabetyp void hat. Die an CallVoidMethod zu übergebenden Argumente sind das Objekt, die Methoden-ID und die Argumente der Java-Methode. Das folgende Beispiel zeigt eine Anwendung der drei Schritte: JNIEXPORT void JNICALL Java_Beispiel_bspFkt(JNIEnv *env, jobject obj) { int r = 0; //Feststellen des Klassen-Objekts jclass klasse = (*env)->GetObjectClass(env, obj); //Feststellen der ID der Methode javafkt jmethodID mId = (*env)->GetMethodID(env, klasse, "javafkt", "(I)V"); if (mId == 0) { //Pech gehabt return; } //Aufruf der Methode (*env)->CallVoidMethod(env, obj, mId, r); } JNI stellt den Namen der Java-Methode aufgrund des Namens und der Signatur fest, wodurch gewährleistet werden kann, dass dieselbe Methode in native Code auch dann noch funktioniert, wenn der Java-Klasse neue Methoden hinzugefügt wurden. Soll ein Konstruktor einer Java-Klasse aufgerufen werden, so muss der Name in Kleiner-/Größerzeichen eingeschlossen werden, bspw. als "<Beispiel>". Die Methodensignatur wird in JNI dazu verwendet, den Rückgabetyp der Java-Methode anzugeben. Im obigen Beispiel wurde die Signatur (I)V angegeben, die eine Java-Methode bezeichnet, die ein Argument vom Typ Integer erwartet und die einen Rückgabetyp void hat. Die allgemeine Form dieses Arguments ist immer "(argument-Typen)rückgabe-typ". Die in JNI möglichen Typsignaturen sind in Tab. 3-15 zusammengefasst.

Signatur | Java-Typ
Z | boolean
B | byte
C | char
D | double
F | float
I | int
J | long
L voller Klassenname; Komponenten getrennt durch Schrägstriche. Beispiel: Ljava/lang | vollständige Java-Klasse
S | short
[typ | Typ[]
(Argumententypen) Rückgabetyp | Methodentyp

Tab. 3.15: JNI-Typsignaturen Die folgenden Beispiele für Signaturen verdeutlichen die Verwendung der Tabelle: (Ljava/lang/String;)Ljava/lang/String; ist die Signatur einer Methode, die als Argument einen String erwartet, und die einen String zurückgibt. ([S)V ist die Signatur einer Methode, die als Argument einen Array von short-Elementen erwartet und die ein void-Objekt zurückgibt. Der Array wird hier mit der geöffneten eckigen Klammer symbolisiert. Offensichtlich ist die Angabe derartiger Signaturen nicht gerade einfach und daher eine potentielle Fehlerquelle. Zur Vermeidung von Fehlern kann aber das Werkzeug javap verwendet werden, das die Variablen und Methodensignaturen einer Java-Klasse ausgibt. Der Aufruf von javap für die Klasse Beispiel.java erfolgt mittels javap -s -p Beispiel Hierbei werden die Flags "-s" und "-p" verwendet. Mittels "s" wird javap dazu veranlasst, Signaturen auszugeben, mittels "-p" werden auch als private gekennzeichnete Elemente ausgegeben. Wird in JNI eine Java-Methode aufgerufen, so wird der aufrufenden Funktion die ID der Methode übergeben. Anhand des Beispiels ist ersichtlich, dass diese Aufgabe nicht trivial ist. Da die Feststellung der ID aber vom tatsächlichen Aufruf getrennt ist, muss diese Operation nur einmal ausgeführt werden. Es ist daher möglich, zu einem frühen Zeitpunkt der Programmabarbeitung die ID festzustellen und diese später mehrfach zu verwenden. Hierbei darf aber nicht vergessen werden, dass die ID nur solange gültig ist, wie die Klasse geladen ist. Solange also die Referenz zur Java-Klasse (Wert jclass) existiert, solange existiert auch die Klasse, ohne von der Garbage Collection freigegeben zu werden und die ID in JNI kann verwendet werden. In JNI stehen verschiedene Möglichkeiten zur Verfügung, Argumente an Java-Methoden zu übergeben. In den meisten Fällen werden die Argumente nach der Methoden-ID übergeben. Java-Klassenmethoden können aus native Code in einer ähnlichen Art und Weise aufgerufen werden. Hierzu sind die folgenden Schritte zu durchlaufen: Feststellung der ID der Java-Methode, indem die JNI-Funktion GetStaticMethodID anstelle der Funktion GetMethodID verwendet wird. Übergabe der Parameter Klasse, Methoden-ID und Argumente an eine der Aufrufsmethoden, bspw. CallStaticVoidMethod oder CallStaticBooleanMethod. Das folgende Beispiel modifiziert das erste Beispiel, so dass nun die statische Methode javafkt aufgerufen wird. JNIEXPORT void JNICALL Java_Beispiel_bspStatFkt(JNIEnv *env, jobject obj) { int r = 0; //Feststellen des Klassen-Objekts jclass klasse = (*env)->GetObjectClass(env, obj); //Feststellen der ID der statischen Methode javafkt jmethodID mId = (*env)->GetStaticMethodID(env, klasse, "javafkt", "(I)V"); if (mId == 0) { //Pech gehabt return; } //Aufruf der Methode (*env)->CallStaticVoidMethod(env, obj, mId, r); } In JNI können auch Instanzmethoden aufgerufen werden, die in einer Superklasse definiert sind, welche durch die Klasse überschrieben wurden, zu der das Objekt gehört. Hierzu muss eine der Funktionen aus der Menge CallNonvirtual<typ>Method verwendet werden. Zum Aufruf einer Instanzmethode der Superklasse müssen dann die folgenden Schritte durchlaufen werden: Feststellung der ID der Java-Methode der Superklasse mittels der Methode GetMethodID anstelle der Methode GetStaticMethodID. Übergabe der Parameter Objekt, Superklasse, Methoden-ID und Argumente an eine der hierzu notwendigen Funktionen, bspw. CallNonvirtualVoidMethod oder CallNonvirtualBooleanMethod. Es sollte aber darauf hingewiesen werden, dass der Aufruf von Instanzmethoden der Superklasse eher selten vorkommt. Der Vorgang ist dem Aufruf einer Methode der Superklasse in der Sprache Java mittels der Anweisung super.f(); sehr ähnlich. Zugriff auf Java-Variablen Mittels JNI können Methoden aus native Code dazu verwendet werden, auf Java-Variablen (Instanz- und Klassenvariablen) zuzugreifen. Die hierzu notwendigen Anweisungen ähneln den Funktionen, die zum Zugriff auf Java-Methoden verwendet werden, da sowohl Methoden zur Verfügung stehen, mit denen auf Klassenvariablen zugegriffen werden kann, als auch solche, mit denen Instanzvariablen verarbeitet werden. Zur Verarbeitung von Java-Methoden in native Code sind die folgenden Schritte auszuführen: Feststellung der ID einer Variable, indem der Klassenname, der Name der Variablen und die Signatur verwendet werden. Zur Feststellung von Klassenvariablen wird hierzu die Methode GetStaticFieldID, zur Feststellung von Instanzvariablen die Methode GetFieldID verwendet. Anwendung einer der JNI-Funktionen zum Setzen oder Abfragen der Werte der Variablen, indem die ID der Variablen verwendet wird. Um den Wert einer Klassenvariablen zu manipulieren, wird die Klasse an eine der Funktionen übergeben, die zum Zugriff auf Klassenvariablen zur Verfügung stehen. Analog wird ein Objekt an eine Funktion übergeben, die zur Manipulation von Instanzvariablen zur Verfügung steht. Der Vorgang ähnelt in großem Maße dem Vorgehen bei der Verwendung von Java-Methoden. Zuerst wird eine ID festgestellt, die anschließend zum Zugriff auf die Variable selbst verwendet wird. Auch hierbei ist zu beachten, dass die ID nur solange gültig ist, wie die Referenz aufrechterhalten wird. Anderenfalls löscht die Garbage Collection die Variable, woraus Zugriffsfehler resultieren, wenn dementsprechende Aufrufe aus native Code erfolgen. Das folgende Beispiel demonstriert die Verwendung von Variablen. Hierbei wird auf eine statische Variable wert vom Typ int zugegriffen. JNIEXPORT void JNICALL Java_Beispiel_bspVar(JNIEnv *env, jobject obj) { jint wert; //Feststellen des Klassen-Objekts jclass klasse = (*env)->GetObjectClass(env, obj); //Feststellen der ID der statischen Variablen wert jfieldID fid = (*env)->GetStaticFieldID(env, klasse, "wert", "I"); if (fid == 0) { return; } wert = (*env)->GetStaticIntField(env, klasse, fid); printf(" Zugriff auf wert = %d\n", wert); (*env)->SetStaticIntField(env, klasse, fid, 1); } Die Signaturen von Variablen müssen in derselben Art und Weise angegeben werden, wie die von Methoden (siehe Tab. 3-15). Die allgemeine Form der Signatur einer Variablen ist "Variablentyp". Dies entspricht dem kodierten Symbol des Typs der Variablen, eingeschlossen in Anführungszeichen. Wie auch bei Methoden beginnen die Signaturen von Objekten mit dem Buchstaben L, gefolgt vom Klassennamen des Objekts und einem Semikolon. Der Zugriff auf eine String-Variable muss daher mit der Signatur "Ljava/lang/String;" erfolgen. Auch für Variablen kann das Werkzeug javap dazu verwendet werden, sich die notwendigen Signaturen ausgeben zu lassen. Abschließend wird als weiteres komplexes Beispiel der Zugriff auf eine Instanzvariable s vom Typ String dargestellt. JNIEXPORT void JNICALL Java_Beispiel_bspStrVar(JNIEnv *env, jobject obj) { jstring js; const char *s; //Feststellen des Klassen-Objekts jclass klasse = (*env)->GetObjectClass(env, obj); //Feststellen der ID der statischen Variablen wert fid = (*env)->GetFieldID(env, klasse, "s", "Ljava/lang/String;"); if (fid == 0) { return; } js = (*env)->GetObjectField(env, obj, fid); s = (*env)->GetStringUTFChars(env, js, 0); printf("String = \"%s\"\n", s); (*env)->ReleaseStringUTFChars(env, js, s); } Abfangen von Exceptions in native Code Wird in Java eine Exception ausgelöst, so sucht die Java Virtual Machine automatisch den in der Klassenhierarchie nächstgelegenen Exception Handler. Hierdurch muss sich der Programmierer nicht in jedem Programmteil um die Verarbeitung von unerwarteten Fehlermeldungen kümmern. Die Java Virtual Machine leitet die Fehlermeldung automatisch an eine Routine weiter, die die jeweilige Klasse von Fehlern in einer zentralisierten Art und Weise bearbeiten kann. In native Code steht keine derartige einheitliche Funktionalität zur Verfügung. JNI erzwingt daher, dass nach dem Aufruf von JNI-Funktionen mögliche Exceptions überprüft werden. Weiterhin stehen in JNI Möglichkeiten zur Verfügung, Java-Exceptions auszulösen. Diese können dann entweder von anderen Programmteilen des native Codes bearbeitet werden oder durch die Java Virtual Machine. Nach einer Verarbeitung in native Code kann die Exception entweder gelöscht werden, es kann aber auch eine weitere generiert werden, die dann für einen anderen Exception Handler bestimmt ist. Leider existieren eine Vielzahl von JNI-Funktionen, die Exceptions auslösen können, und die daher auch überwacht werden müssen. Ein Beispiel hierfür ist die Funktion GetFieldID, die eine NoSuchFieldError-Exception auslöst, wenn das angegebene Feld nicht existiert. Um die Fehlerverarbeitung zu erleichtern, verwenden die meisten JNI-Funktionen eine Kombination aus Fehler-Code und Java-Exception, um einen Fehlerfall anzuzeigen. Anstelle des Aufrufs der JNI-Funktion ExceptionOccurred kann daher bspw. auch geprüft werden, ob das Resultat, das die Methode GetFieldID zurückgibt, gleich null ist. Wird diese Überprüfung vorgenommen, so wird auch keine Exception ausgelöst. Es ist außerordentlich wichtig, ausstehende Exceptions zu verarbeiten, bevor weitere JNI-Funktionen aufgerufen werden. Werden weitere Funktionen aufgerufen, während eine Exception noch nicht bearbeitet ist, so sind die Resultate vollkommen unvorhersagbar. Die einzigen JNI-Funktionen, die bei einer ausgelösten Exception verarbeitet werden sollten, sind ExceptionOccurred, ExceptionDescribe und ExceptionClear, also Funktionen, die speziell zur Verarbeitung von Exceptions zur Verfügung stehen. Das nun folgende Beispiel demonstriert die Bearbeitung ausstehender Exceptions. JNIEXPORT void JNICALL Java_Beispiel_bspExc(JNIEnv *env, jobject obj) { jstring js; const char *s; jthrowable ex; //Feststellen des Klassen-Objekts jclass klasse = (*env)->GetObjectClass(env, obj); jmethodID mId = (*env)->GetMethodID(env, klasse, "fkt", "()V"); // Erste Variante der Abarbeitung von Exceptions if (mId == 0) { return; } (*env)->CallVoidMethod(env, obj, mId); //Feststellung einer Exception ex = (*env)->ExceptionOccurred(env); if (ex) { jclass neueEx; //Beschreibung anlegen (*env)->ExceptionDescribe(env); //... und loeschen der Exception (*env)->ExceptionClear(env); //Wer war der Boesewicht neueEx = (*env)->FindClass(env, "java/lang/IllegalArgumentException"); //Nicht gefunden? if (neueEx == 0) return; //Ausserhalb bestaetigen (*env)->ThrowNew(env, neueEx, "Exception in native Code"); } } Lokale und globale Referenzen in native Code Referenzen zu Java-Objekten wurden bis zu dieser Stelle des Buches immer mit Datentypen wie bspw. jobject, jclass oder jstring angelegt. JNI erzeugt allerdings Referenzen für alle Objektargumente, die an Methoden in native Code übergeben werden, bzw. auch für alle Objekte, die von JNI-Funktionen zurückgegeben werden. Referenzen verhindern, dass Java-Objekte von der Garbage Collection gelöscht werden. JNI erzeugt standardmäßig nur lokale Referenzen, da nur diese garantieren, dass die Java Virtual Machine derartig referenzierte Objekte nicht löscht. Ein Nachteil von lokalen Referenzen ist allerdings, dass diese verfallen, wenn die Bearbeitung der Methode in native Code abgeschlossen ist, in der die Referenz definiert wurde. Eine derartige Methode darf daher lokale Referenzen in der Erwartung einer späteren Wiederverwendung auf keinen Fall speichern. Dieses Problem kann umgangen werden, indem eine globale Referenz angelegt wird, die solange gültig ist, bis sie wieder aufgelöst wird. Im folgenden Beispiel wird eine derartige Referenz angelegt. static jclass klasse = 0; static jfieldID fid; JNIEXPORT void JNICALL Java_Beispiel_bspGlob(JNIEnv *env, jobject obj) { int wert = 0; //Feststellen des Klassen-Objekts if (klasse == 0) jclass klasse1 = (*env)->GetObjectClass(env, obj); jclass klasse1 = (*env)->GetObjectClass(env, obj); if (klasse1 == 0) //Fehler return; klasse = (*env)->NewGlobalRef(env, klasse1); //ID der Variable wert als globale Referenz fid = (*env)->GetStaticFieldID(env, klasse,"wert", "I"); // Abarbeitung von Exceptions if (fid == 0) return; wert = (*env)->GetStaticIntField(env, klasse, fid); printf(" Zugriff auf wert = %d\n", wert); (*env)->SetStaticIntField(env, klasse, fid, 1); } Eine globale Referenz verhindert, dass die Java Virtual Machine die Java-Klasse löscht und garantiert daher, dass die ID der Variablen bestehen bleibt. Es liegt auf der Hand, dass die globale Referenz zu einem späteren Zeitpunkt wieder zu löschen ist. Dies liegt in der Verantwortung des Programmierers, darf aber nicht vergessen werden. Zum Löschen einer globalen Referenz muss die JNI-Funktion DeleteGlobalRef verwendet werden. In den meisten Fällen kann sich der Programmierer von native Code darauf verlassen, dass die Java Virtual Machine alle lokalen Referenzen löscht, wenn die Abarbeitung der Methode in native Code beendet ist. In manchen Situationen allerdings muss die JNI-Funktion DeleteLocalRef aufgerufen werden, um eine lokale Referenz explizit zu löschen. Dies tritt immer dann auf, wenn in native Code ein großes Java-Objekt referenziert wird, von dem allerdings ab einem bestimmten Zeitpunkt bereits klar ist, dass es nicht länger benötigt wird. Wird die lokale Referenz daher bereits im native Code aufgelöst, so kann die Garbage Collection die Klasse löschen und damit Speicherplatz freigeben. Ein weiteres Anwendungsfeld dieser Operation besteht darin, dass eine Vielzahl von Java-Objekten aus native Code referenziert werden sollen. Da nur eine bestimmte Obergrenze dieser Objekte simultan verwaltet werden können, ist es außerordentlich sinnvoll, nicht weiter benötigte Objekte zu löschen, da anderenfalls das Programm abzustürzen droht. Das folgende Beispiel demonstriert die Anwendung der Funktion DeleteLocalRef. JNIEXPORT jint JNICALL Java_Beispiel_bspFkt(JNIEnv *env, jobject obj) { int r = 0; //Feststellen des Klassen-Objekts jclass klasse = (*env)->GetObjectClass(env, obj); //Feststellen der ID der Methode javafkt jmethodID mId = (*env)->GetMethodID(env, klasse, "javafkt", "(I)V"); if (mId == 0) { //Pech gehabt return; } //Aufruf der Methode (*env)->CallVoidMethod(env, obj, mId, r); (*env)->DeleteLocalRef(env, mId); //eine lange Berechnung des Wertes r return r; } Threads und native Code Eine Besonderheit tritt immer dann auf, wenn Segmente von native Code simultan von mehreren Prozessen (Threads) verwendet werden sollen. Da Threads aber erst in Kapitel 4.2 vorgestellt werden, erfolgt diese Beschreibung in Kapitel 4.2.