Java Native Interface Eine Einführung Cornelius Zühl [email protected] 01.03.2010 Inhaltsverzeichnis 1. Einführung......................................................................................................................... 3 2. Was ist JNI? ..................................................................................................................... 3 3. JNI, was habe ich damit zu tun? ................................................................................... 4 4. Schlüsselwort „native“, warum kann ich es verwenden? .......................................... 5 5. Nativ (wie C++) spricht Java über die Invocation-API ............................................... 6 6. Ein Blick hinter die Kulissen........................................................................................... 8 7. Wie verwende ich JNI in meinem Java Programm? ................................................ 10 8. Zusammenfassung........................................................................................................ 13 9. Quellen ............................................................................................................................ 14 2 1. Einführung Ziel dieser Ausarbeitung ist es einen allgemeinen Überblick über die Funktionsweise und Verwendung des JNI zu geben. Dabei wird das Thema zuerst motiviert. Dann werden auf die Gründe für die Verwendung und die Arten des Einsatzes eingegangen. Zum Schluss folgt eine kurze Zusammenfassung. 2. Was ist JNI? Das JNI (Java Native Interface) ist eine nicht Java-spezifische API (Application Programming Interface Programmierschnittstelle). Dem JNI liegt aber auch eine Standardisierung zugrunde. Dieser Standard garantiert zwei wichtige Eigenschaften: • Kompatibilität Jede beliebige JVM lässt sich von außerhalb auf exakt der gleichen Form betreiben. • Gleichheit Es wird nur eine einzige API in allen JVMs ausgeführt. Das heißt, dass einmal erzeugter binärer Code für eine JVM auf einem bestimmten Hardwaretyp auch auf allen JVMs dieses Hardwaretyps verwendet werden kann. JNI bildet die Schnittstelle zwischen der JVM (Java Virtual Mashine) und der nativen Umgebung, wie in C++ geschriebene Programmbibliotheken, außerhalb von Java. Die nativen Bibliotheken können die Schnittstelle nutzen, um eine JVM zu erzeugen und so mit der Java – Welt zu interagieren. 3 3. JNI, was habe ich damit zu tun? Nun, jeder der Java Programme einsetzt hat indirekt mit JNI zu tun. Schon die Java – VM verwendet intern JNI. So wird z.B. bei der Ausführung einer .jar – Datei (die zip – komprimiert ist) der Inhalt zuerst temporär dekomprimiert um die .class – Dateien zu laden. Egal ob unter Windows oder Unix, um diese Aufgabe zu erledigen wird eine betriebssystemabhängige Implementierung verwendet. Unter Windows wird die zip.dll geladen. Unter Unix wird die libzip.so geladen. 4 Aber auch jeder der schon einmal selbst Java Programme geschrieben hat ist indirekt mit JNI in Berührung gekommen. Viele Methoden aus dem Java – Framework wie z.B. bei der Klasse „System“ verwenden intern das JNI. 4. Schlüsselwort „native“, warum kann ich es verwenden? Es gibt grundsätzlich zwei Gründe für den Einsatz des JNI aus Java heraus. Wobei ein Grund ehr ein „scheinbarer“ Grund für die Verwendung darstellt: • Performancesteigerung erreichen • Spezielle Eigenheiten des Betriebsystems verwenden Grund 1 – Performancesteigerung erreichen Zum einen könnte eine Geschwindigkeitssteigerung durch JNI erzielt werden. Denn es werden einige Aufgaben der Java – Release nicht in derselben Weise wie optimierte C/C++ – Programme ausgeführt. Das könnte konkret bedeuten, dass kritische Stellen wie innere Schleifen über einen native – Aufruf ausgelagert werden. Grund 2 – Spezielle Eigenheiten des Betriebsystems verwenden Zum anderen könnte so der Zugang zu speziellen Fähigkeiten des Rechners / Betriebssystems bereitet werden. Dazu zählen unter 5 vielen anderen Möglichkeiten der Anschluss an neue Peripheriegeräte und Steckkarten. Der Zugriff auf verschiedene Netztypen und die Verwendung von eindeutigen Merkmalen des Betriebssystems. Ein Konkretes Beispiel wäre das Erfassen von Echtzeitton über Mikrofon. Dafür gibt es im Java – Framework keine generische Bibliothek. Es war davon die Rede, dass ein Grund ein „scheinbarer“ Grund sei. Aber welcher ist das? Die Performancesteigerung ist ein verbreiteter Grund wenn man an den Gebrauch von JNI denkt. Dieser ist aber der scheinbare Grund. Denn wer wirklich echtzeitkritische Anwendungen will, wird nicht auf Java setzen. Bei der Deutschen Post AG wird beispielsweise ein System eingesetzt um die Zieladressen auf Briefen zu parsen und entsprechend Weichen auf den Wegen zu stellen den die Briefe weiter nehmen. Dafür stehen bei den hohen Geschwindigkeiten zum Erkennen nur wenige Millisekunden zur Verfügung. Für diesen Einsatzbereich wird man bestimmt auf andere Programmiersprachen als Java setzen. Außerdem werden viele zeitkritische Probleme ohnehin schon in der JVM nativ gelöst (siehe „System.arraycopy(…)“ weiter oben). Besser ist es die Energie auf ein gutes Design von Klassen und Methoden zu verwenden, sie abstrakt und wieder verwendbar zu machen. Wenn das Programm seine Vorgaben erfüllt, kann als letzter Schritt die Geschwindigkeitssteigerung angegangen werden. 5. Nativ (wie C++) spricht Java über die Invocation-API Unter C++ kann mit Einbinden der Java – Headerdatei (#include <jni.h>) der Zugang zu einer eigenständigen JVM geschaffen werden. 6 Natürlich muss das JDK (Java Development Kit) installiert sein. Ausserdem müssen Includeverzeichnisse in des dem JDK Projekt die zusätzlichen eingebunden werden (C:\Programme\Java\jdkVERSION\include;C:\Programme\Java\jdkVERS ION\include\win32). #include <jni.h> /* alle Java bezogenen Typen und Methoden sind hier definiert */ . . . JavaVM *jvm /* Pointer auf die JVM */ JNIEnv *e; /* Pointer auf das native method interface */ JDK1_1InitArgs vm_args; /* JDK 1.1 VM Init-Argumente */ . . . JNI_GetDefaultJavaVMInitArgs(&vm_args); /* setzt Standard-Argumente */ vm_args.classpath = ...; /* können geändert werden */ JNI_CreateJavaVM(&jvm, &e, &vm_args); /* lädt und initialisiert eine VM */ jclass jc = (*e)->FindClass("Main"); /* findet Klasse innerhalb der VM */ jmethodID jmID = (*e)->GetStaticMethodID(jc, "test", "(I)V"); (*e)->CallStaticVoidMethod(jc, jmID, 100); /* ruft in der Klasse eine Methode auf */ (*jvm)->DestroyJavaVM(); /* beendet die VM */ Das obige Beispiel zeigt wie aus C++ heraus eine JVM erzeugt wird. In der VM wird unter Verwendung des „classpath“ nach einer Klasse namens „Main“ (im defaultpackage) gesucht. Dann wird in dieser Klasse die Methode „test“ aufgerufen. Die Signatur "(I)V" verrät, dass die Methode einen Integer als Prameter verlang und „void” zurückgibt. Es können mit Hilfe der VM • Java Objekte erzeugt, untersucht und geändert • Java Methoden aufgerufen • Exceptions geworfen und gefangen 7 • Klassen und Klasseninformationen abgerufen • Typen zur Laufzeit geprüft werden. 6. Ein Blick hinter die Kulissen Der Aufruf von nativ in Richtung JVM erfolgt über den Interface Pointer. Der Interface Pointer ist dabei ein Zeiger auf ein Array von Funktionszeigern. Der Typ von JNIEnv ist ein struct welches die Zeiger auf die verschiedenen Funktionen der JVM enthält: struct void void void void ... JNINativeInterface_ { *reserved0; *reserved1; *reserved2; *reserved3; jfieldID (JNICALL *GetStaticFieldID) (JNIEnv *env, jclass clazz, const char *name, const char *sig); … } /* nur ein Auszug, weil in JNIEnv_ auf dieses struct bezug genommen wird */ struct JNIEnv_ { const struct JNINativeInterface_ *functions; ... jfieldID GetStaticFieldID(jclass clazz, const char *name, const char *sig) { return functions->GetStaticFieldID(this,clazz,name,sig); } … } /* das ist nur ein Auszug um die Struktur zu verdeutlichen */ 8 Die Methoden in JNIEnv sind für das jeweilige Betriebssystem unter dem das JDK installiert ist entsprechend angepasst und kompiliert. Auf der nativen Seite erhalten Methoden immer zwei zusätzliche Argumente unabhängig von ihrer Parameterliste: • Argument eins ist der JNI Interface Pointer (JNIEnv) • Argument zwei ist bei static native Methoden o Zeiger auf die Java – Klasse in der JVM • Argument zwei ist bei nonstatic native Methoden o Zeiger auf das Java – Objekt („this“) in der JVM Weiterhin ist das Speichermanagement ein interessanter Punkt der hier kurz beleuchtet werden soll. Wenn Java eine native Funktion aufruft, dann werden die primitiven Datentypen (wie int, long, double usw.) kopiert. Java – Objekte hingegen werden in lokale bzw. globale Referenzen beim Aufruf der nativen Methode unterteilt. Die lokalen Referenzen leben im Scope ihres erzeugenden Stack-Frame und Thread. Sie werden bei verlassen der nativen Methode freigegeben. Zu beachten ist hierbei, dass die JVM nur begrenzten Speicher zur Verfügung hat um sich diese lokalen Referenzen zu merken! Die globalen Referenzen leben bis sie explizit freigegeben werden. In JNIEnv existieren Methoden um lokale Referenzen in globale umzuwandeln. Jede JNI Funktion die beim Aufruf Referenzen zurückgibt wird diese als lokal zurückgeben. Ein weiterer Punkt sind Fehlerbehandlung und Exceptions. Bei jedem Aufruf auf nativer Seite von JNI Funktionen können Exceptions erzeugt werden. Die JNI Funktionen geben in diesen Fällen Fehlercodes aus die über die Methode „ExceptionOccured()“ abgerufen werden können. Bei der Fehlerbehandlung gibt es grundsätzlich zwei Möglichkeiten um 9 darauf zu reagieren (neben der dritten Möglichkeit nichts machen). Falls nicht auf der nativen Seite auf den Fehler reagiert wird, kehrt der Methodenaufruf irgendwann auf die Java Seite zurück. Dann wird dort der Fehler erneut als Exception geworfen. Die andere Möglichkeit ist auf der nativen Seite den Fehler zu behandeln. Dann muss mit der Methode „ExceptionClear()“ aus JNIEnv der Fehler zurückgesetzt werden damit er auf der Java Seite nicht geworfen wird. 7. Wie verwende ich JNI in meinem Java Programm? Für die Aufrufe von Java in Richtung nativ gelten spezielle regeln für externe d.h. nativ (z.B. unter C++) deklarierten Methoden die über Java aufgerufen werden können. Es muss der voll qualifizierte Methodenname kodiert werden Paketname + Klassenname + Funktionsname mit Parametern. Dabei wird der Paketname der sonst mit Punkten getrennt ist durch Unterstriche getrennt. Zusätzlich zu den Parametern der Funktion werden zwei weitere Parameter übergeben (weiter oben schon erleutert). Eine externe (native) Funktion wird in Java dann so deklariert: package de.paket.jni; public class Klasse { public native boolean callNative(int arg0); } Auf der nativen Seite muss dann entsprechend der Namenskonvention für die Methode das passende Gegenstück in einer Headerdatei deklariert werden: JNIEXPORT jboolean JNICALL Java_de_paket_jni_Klasse_callNative(JNIEnv*, jobject, jint); 10 Dieses Verfahren muss für jede auf Java Seite als „native“ deklarierte Methode angewendet werden. Die JVM sucht in der externen Bibliothek nach genau dieser Funktion mit genau diesem Namen und exakt dieser Signatur wenn aus „Klasse“ die Methode „callNative()“ aufgerufen wird. Jetzt stellt sich die Frage, ob das nicht auch einfacher geht. Bei einem großen Projekt mit vielen nativen Methoden ist es erstens schwer alle Konventionsgemäß auf der nativen Seite zu deklarieren. Zweitens müssen bei Änderungen in Funktionsnamen oder der Parameterliste die nativen Sourcen nachgepflegt werden. Die gute Nachricht ist: es gibt ein Kommandozeilentool das dabei hilft die Headerdatei für die native Seite automatisch zu erzeugen. Ganz wichtig ist dabei folgende Schritte zu beachten, da sonst die Headerdatei nicht erzeugt wird! In einer Konsole (cmd unter Windows) folgende Kommandos der Reihe nach ausführen: • set JAVA_HOME=C:\Programme\Java\jdkVERSION • set PATH=%PATH%;%JAVA_HOME%\bin und schließlich • C:\JAVA_PROJECT_PATH\bin>javah de.paket.jni.Klasse (Unter Unix funktioniert das analog) 11 Jetzt wurde in „C:\JAVA_PROJECT_PATH\bin“ eine verwendbare Headerdatei namens „de_paket_jni_Klasse.h“ erzeugt. So sieht die automatisch erzeugte Datei dann aus: /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class de_paket_jni_Klasse */ #ifndef _Included_de_paket_jni_Klasse #define _Included_de_paket_jni_Klasse #ifdef __cplusplus extern "C" { #endif /* * Class: de_paket_jni_Klasse * Method: callNative * Signature: (I)Z */ JNIEXPORT jboolean JNICALL Java_de_paket_jni_Klasse_callNative (JNIEnv *, jobject, jint); #ifdef __cplusplus } #endif #endif Wenn diese Funktion (Java_de_paket_jni_Klasse_callNative(…)) dann Implementiert und eine .dll daraus erzeugt wurde, kann sie fast schon von Java Seite aus verwendet werden. Was jetzt noch Fehlt ist das Laden der externen Ressource über den „loadLibrary“ Befehl. Dafür wird der Java Code um diese Zeile ergänzt: package de.paket.jni; public class Klasse { static { System.loadLibrary("LIB_PATH/MeineDynamischeBibliothek"); } public native boolean callNative(int arg0); } 12 Unter Windows heißt diese Bibliothek • MeineDynamischeBibliothek.dll Unter Unix heißt diese Bibliothek • libMeineDynamischeBibliothek.so In beiden Fällen wird sie aber nur über ihren eigentlichen Namen „MeineDynamischeBibliothek“ geladen. Das Auflösen der Abhängigkeit des tatsächlichen Namens vom Betriebssystem übernimmt Java intern. Somit ist der obige Java Code sowohl unter Unix als auch Windows gleichermaßen gültig. 8. Zusammenfassung Das JNI bietet die Möglichkeiten sowohl von nativer (C++) Seite aus auf Java zuzugreifen, als auch umgekehrt von Java aus die native Seite (C++) einzubinden. Der Einsatz von JNI begegnet jedem indirekt der Java Programme verwendet oder selbst Java Programme schreibt. Der Einsatzzweck um von Java aus native Bibliotheken zu verwenden liegt primär darin die Eigenschaften des Betriebssystems dort direkt nutzbar zu machen wo das Java Framework keine Bibliotheken anbietet. Oder auch wo sich native Bibliotheken bereits bewährt haben und jetzt in einer Java GUI eingebettet werden sollen. Nur die Steigerung der Performance als alleiniger Grund für die Verwendung von JNI ist zwar ein verbreitetes Einsatzziel, in den meisten Fällen aber nicht mehr weiter erreichbar. Java selbst ist schon an vielen kritischen Stellen optimiert und setzt dort intern schon auf native Bibliotheken. Wer von Java Seite aus JNI in seinen Programmen verwenden möchte, dem Stehen Werkzeuge zur Verfügung die das Generieren von Headerdateien und somit den Einsatz erleichtern. 13 9. Quellen • IBM Understanding the Java Native Interface o http://publib.boulder.ibm.com/infocenter/javasdk/v6r0/topic/co m.ibm.java.doc.diagnostics.60/html/jni.html#jni • Native Methoden und Bibliotheken (1997) o http://olymp.idle.at/~apollo/books/Java%20in%2021%20Tage n/kap20.htm • JNI 1.1 Specification ff. o http://java.sun.com/j2se/1.4.2/docs/guide/jni/ • Gute Übersicht für den Praktischen Einsatz o http://www.haertfelder.com/jni.html 14