Das Java Native Interface EDV2 - 02 - JavaNativeInterface 1 Wozu JNI? EDV2 - 02 - JavaNativeInterface Man benötigt Plattform-spezifische Features, die nicht durch die vorhandenen JAVA-Klassen bereitgestellt werden. Z.B.: Zugriff auf einen Bandroboter über die SCSI-Schnittstelle um eine grafische Oberfläche zur Bedienung des Roboters zu entwickeln. Es ist eine Bibliothek mit Routinen vorhanden, die nicht in JAVA programmiert wurden. Z.B.: Ausnutzung einer speziellen Hardware für numerische Berechnungen. Ein kleiner zeitkritischer Programmabschnitt soll in einer hardwarenahen Sprache programmiert werden, um das Programm zu beschleunigen. Z.B.: Textsuche in einem Editor Skalarprodukt in einem numerischen Programm Dreiecksberechnung in einem FEM-Code 2 Was bietet JNI? Erzeugen, Analysieren und Verändern von JAVA-Objekten (einschließlich Felder und Zeichenketten) in C-Programmen. Aufruf von JAVA-Methoden in C-Programmen. Erzeugen und Abfangen von JAVA-Ausnahmen in C-Programmen. Laden von JAVA-Klassen in C-Programmen. Konsequente Typprüfung. EDV2 - 02 - JavaNativeInterface 3 Aufruf von C-Routinen aus JAVA EDV2 - 02 - JavaNativeInterface Das Java Native Interface ermöglicht die Verbindung zwischen Programmen, die in JAVA geschrieben wurden mit Programmen, die in anderen Sprachen geschrieben sind, z.B. C, C++, Assembler, FORTRAN u.s.w. Ziele: Die strenge Schnittstellenprüfung von JAVA soll erhalten bleiben. Es soll ermöglicht werden aus den anderen Sprachen heraus • JAVA-Objekte zu lesen und zu erzeugen, • JAVA-Methoden aufzurufen und • JAVA-Ausnahmen zu erzeugen. Probleme: Unterschiede bei den primitiven Datentypen Unterschiede bei Objekttypen Unterschiede bei der Parameterübergabe Es werden eine Reihe von Tools zu Verfügung gestellt, die die Kopplung zwischen JAVA und C bzw. C++ vereinfachen. 4 HelloWorld: aufzurufendes C-Programm #include <stdio.h> void helloWorld() { printf("Hello, world!\n"); } EDV2 - 02 - JavaNativeInterface Das Programm helloWorld sei vorgegeben und als Header-File hello.h sowie als Objektlibrary (LIB) hello.lib gespeichert. Problem: Wie kann helloWorld in einem JAVA-Programm aufgerufen werden. Lösung: Entwickeln einer Schnittstelle, die in einer oder mehreren JAVA-Klassen realisiert ist und den komfortablen Zugriff auf die Library ermöglicht. 5 Struktur HelloWorld.java HelloWorld.class Hello.java Hello.class EDV2 - 02 - JavaNativeInterface C_hello.h C_hello.c C_hello.dll hello.h hello.c 6 hello.lib Aufrufendes JAVA-Programm public class Hello { public static native void helloWorld(); EDV2 - 02 - JavaNativeInterface static { System.loadLibrary("C_hello"); } } 7 Das Tool javah Das Tool javah dient dazu aus einem JAVA-Programm, das nativeMethoden enthält, das header-File zu erzeugen, das die Schnittstelle zum Interface-Programm beschreibt. Aufruf: javah –o h-file.h filename EDV2 - 02 - JavaNativeInterface Es wird ein header-File filename.h erzeugt, das die zu realisierenden Schnittstellen beschreibt. Beispiel: javah -o C_hello.h Hello erzeugt C_hello.h 8 Erzeugtes header-File C_Hello.h /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class Hello */ EDV2 - 02 - JavaNativeInterface #ifndef _Included_Hello #define _Included_Hello #ifdef __cplusplus extern "C" { #endif /* * Class: Hello * Method: helloWorld * Signature: ()V */ JNIEXPORT void JNICALL Java_Hello_helloWorld (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif 9 Ausfüllen der Schnittstelle Es ist nun das Interface zu programmieren. Dazu muss das in dem Header-File definierte Programm Java_Hello_helloWorld programmiert werden. Die Parameter können zunächst vernachlässigt werden. EDV2 - 02 - JavaNativeInterface 10 #include "C_hello.h" /* * Class: Hello * Method: helloWorld * Signature: ()V */ JNIEXPORT void JNICALL Java_Hello_helloWorld (JNIEnv * env, jclass class) { helloWorld(); } Übersetzen des C-Interfaces Bei der Übersetzung muss die Library hello.dll eingebunden werden: hello.lib Es müssen die JNI-spezifischen Include-Verzeichnisse angegeben werden: -Ic:\jdk1.3\include -Ic:\jdk1.3\include\win32 EDV2 - 02 - JavaNativeInterface 11 Es muss eine DLL erzeugt werden: -LD -FeC_hello.dll Insgesamt für MS-C-Compiler unter Windows: cl -Ic:\jdk1.3\include -Ic:\jdk1.3\include\win32 C_hello.c hello.lib -LD -FeC_hello.dll Nutzung des Interfaces Das so entwickelte Interface kann nun wie eine ganz normale JAVAKlasse verwendet werden. public class HelloWorld { EDV2 - 02 - JavaNativeInterface 12 public static void main(String[] args) { Hello.helloWorld(); } } Übergabe von Parametern EDV2 - 02 - JavaNativeInterface 13 Problem: Datentypen von JAVA und C sind nicht kompatibel. C-Datentypen sind nicht vollständig standardisiert und damit abhängig von Compiler, Betriebssystem und Hardware. JAVA-Datentypen sind vollständig standardisiert und überall identisch. Der Zugriff auf Attribute von Parameter-Objekten ist naturgemäß recht kompliziert. Beispiel: EchoText #include <stdio.h> void helloWorld() { EDV2 - 02 - JavaNativeInterface printf("Hello, world!\n"); } void echoText(const char *text) { printf("%s\n",text); } 14 Header-File:C_hello.h Zusätzlich wird folgender Text generiert. Er enthält einen zusätzlichen Parameter vom Typ jstring. Problem: dieser Parameter muss in eine Zeichenkette vom C-Typ char* umgewandelt werden. EDV2 - 02 - JavaNativeInterface 15 /* * Class: Hello * Method: echoText * Signature: (Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_Hello_echoText (JNIEnv *, jclass, jstring); Lesen des Parameters Problem: Umwandlung des JAVA-String-Parameters in ein entsprechendes C-Format (const char *). JNI stellt dafür diverse Methoden zur Verfügung. Die Methoden sind erreichbar in C: (*env)->Funktionsname(env,parameter); In C++: env->Funktionsname(parameter); EDV2 - 02 - JavaNativeInterface 16 Speziell lesen einer Unicode-Zeichenkette: GetStringUTFChars(env, text, isCopy) Alle mit (*env)->GetXXXX angelegten C-Objekte müssen mit (*env)->ReleaseXXXX wieder freigegeben werden, wenn sie nicht mehr benötigt werde. Beispiel JNIEXPORT void JNICALL Java_Hello_echoText (JNIEnv *env, jclass class, jstring text) { const char *ctext; EDV2 - 02 - JavaNativeInterface 17 ctext= (*env)->GetStringUTFChars(env, text, NULL); echoText(ctext); (*env)->ReleaseStringUTFChars(env, text, ctext); } Erzeugen von JAVA-Strings Zum Erzeugen eines JAVA-Strings aus einer C-Zeichenkette gibt es die Methode jstring NewStringUTF(JNIEnv *env, const char *bytes); Achtung: Nicht vergessen, die im C-Teil reservierten Speicher freizugeben. EDV2 - 02 - JavaNativeInterface 18 Beispiel: concatSrings char *concatStrings(const char *s1, const char *s2) { char *s; s=(char *)malloc(strlen(s1)+strlen(s2)+1,sizeof(s[0])); strcpy(s,s1); EDV2 - 02 - JavaNativeInterface 19 strcat(s,s2); return s; } Hello.java public class Hello { public static native void helloWorld(); EDV2 - 02 - JavaNativeInterface public static native void echoText(String text); public static native String concatStrings(String s1, String s2); static { System.loadLibrary("C_hello"); } } 20 C_hello.h /* * Class: Hello * Method: concatStrings * Signature: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; EDV2 - 02 - JavaNativeInterface 21 */ JNIEXPORT jstring JNICALL Java_Hello_concatStrings (JNIEnv *, jclass, jstring, jstring); C_hello.c /* * Class: Hello * Method: concatStrings * Signature: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_Hello_concatStrings (JNIEnv *env, jclass class, jstring s1, jstring s2) { EDV2 - 02 - JavaNativeInterface const char *cs1; const char *cs2; char *cs; jstring s; cs1= (*env)->GetStringUTFChars(env, s1, NULL); cs2= (*env)->GetStringUTFChars(env, s2, NULL); cs = concatStrings(cs1, cs2); (*env)->ReleaseStringUTFChars(env, s1, cs1); (*env)->ReleaseStringUTFChars(env, s2, cs2); s=(*env)->NewStringUTF(env, cs); free(cs); return s; } 22 Zugriffsmethoden für Zeichenketten EDV2 - 02 - JavaNativeInterface 23 C-ASCII-Zeichenkette JAVA-Zeichenkette jstring NewStringUTF(JNIEnv *env, const char *bytes) const char* GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy) void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf) jsize GetStringUTFLength(JNIEnv *env, jstring string) C-Unicode-Zeichenkette JAVA-Zeichenkette jstring NewString (JNIEnv *env, const jchar *bytes) const jchar* GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy) void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *utf) jsize GetStringLength(JNIEnv *env, jstring string) Zugriff auf Felder Wie für Zeichenketten gibt es für alle Arten von eindimensionalen Feldern entsprechende New-, Get-, Set- und Release-Methoden. Dabei muss unterschieden werden zwischen den Zugriffsmethoden für Felder von primitiven Typen und Felder von Objekten. EDV2 - 02 - JavaNativeInterface 24 jsize GetArrayLength(JNIEnv *env, jarray array) bestimmt die Länge des Feldes (array.length). <ArrayType> New<PrimitiveType>Array(JNIEnv *env, jsize length) bzw. jarray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initialElement) erzeugt ein neues Feld Felder umkopieren EDV2 - 02 - JavaNativeInterface 25 <NativeType> *Get<PrimitiveType>ArrayElements (JNIEnv *env, <ArrayType> array, jboolean *isCopy) erzeugt ein C-Feld und kopiert ggf. den Inhalt des JAVA-Feldes in das C-Feld. Änderungen des C-Feldes werden ggf. erst bei dem entsprechenden Release-Aufruf zurückkopiert. isCopy liefert JNI_TRUE, wenn das Feld kopiert wurde, JNI_FALSE sonst. void Release<PrimitiveType>ArrayElements(JNIEnv *env, <ArrayType> array, <NativeType> *elems, jint mode) das C-Feld wird ggf. auf das JAVA-Feld zurückkopiert und die Ressourcen werden wieder freigegeben. mode steuert die Funktionen: 0 : Kopieren der Daten und Ressourcen freigeben JNI_COMMIT : Kopieren der Daten und Ressourcen nicht freigeben JNI_ABORT : Ressourcen freigeben und Daten nicht kopieren Teile von Feldern umkopieren EDV2 - 02 - JavaNativeInterface 26 void Get<PrimitiveType>ArrayRegion(JNIEnv *env, <ArrayType> array, jsize start, jsize len, <NativeType> *buf) kopiert die Elemente start,...,start+len-1 aus dem JAVAFeld array in das C-Feld buf void Set<PrimitiveType>ArrayRegion(JNIEnv *env, <ArrayType> array, jsize start, jsize len, <NativeType> *buf) kopiert die Elemente start,...,start+len-1 aus dem C-Feld buf in das JAVA-Feld array Elemente von Object-Arrays jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index) liest ein Element aus eine Object-Array void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value) schreibt das Element value in das Feld EDV2 - 02 - JavaNativeInterface 27 JAVA-Typen / C-Typen EDV2 - 02 - JavaNativeInterface 28 JAVA Feldtyp PrimitiveType NativeType ArrayType boolean[] Boolean jboolean jbooleanArray byte[] Byte jbyte jbyteArray char[] Char jchar jcharArray short[] Short jshort jshortArray int[] Int jint jintArray long[] Long jlong jlongArray float[] Float jfloat jfloatArray double[] Double jdouble jdoubleArray Beispiel : matrixXvector void matrixXvector(int n, int m, double *matrix, double *vector, double *result) { int i,j; EDV2 - 02 - JavaNativeInterface for (i=0;i<m;i++) { result[i]=0.0; for (j=0;j<n;j++) { result[i]+=matrix[i*n+j]*vector[j]; } } } 29 C_hello.h /* * Class: Hello * Method: matrixXvector * Signature: ([[D[D)[D */ EDV2 - 02 - JavaNativeInterface 30 JNIEXPORT jdoubleArray JNICALL Java_Hello_matrixXvector (JNIEnv *, jclass, jobjectArray, jdoubleArray); C-hello.c JNIEXPORT jdoubleArray JNICALL Java_Hello_matrixXvector (JNIEnv *env, jclass clazz, jobjectArray matrix, jdoubleArray vector) { jsize n, m, i; jdouble *cMatrix; jdouble *cVector; jdouble *cResult; jboolean isCopy; jdoubleArray zeile; jdoubleArray result; EDV2 - 02 - JavaNativeInterface n=(*env)->GetArrayLength(env, vector); m=(*env)->GetArrayLength(env, matrix); cVector=(*env)->GetDoubleArrayElements(env, vector, &isCopy); cMatrix=(jdouble *)calloc(m*n,sizeof(jdouble)); for (i=0;i<m;i++){ zeile=(jdoubleArray)((*env)->GetObjectArrayElement(env, matrix, i)); (*env)->GetDoubleArrayRegion(env, zeile, 0, n, &(cMatrix[i*n])); } cResult=(jdouble *)calloc(m,sizeof(jdouble)); matrixXvector(n, m, cMatrix, cVector, cResult); result=(*env)->NewDoubleArray(env, m); (*env)->SetDoubleArrayRegion(env, result, 0, m, cResult); free(cMatrix); free(cResult); (*env)->ReleaseDoubleArrayElements(env, vector, cVector, JNI_ABORT); return result; 31 } Zugriff auf JAVA-Objekte EDV2 - 02 - JavaNativeInterface 32 Objekte können als Parameter an das Interface JAVA-C übergeben werden und es soll auf die Attribute und Methoden des Objektes zugegriffen werden. Es soll auf JAVA-Klassen und deren statische Methoden zugegriffen werden, z.B. sollen die Methoden von StrictMath im C-Programm verwendet werden. Es sollen im C-Programm Objekte erzeugt werden, z.B. um ein Objekt als return-Wert zurückgeben zu können. Für die Unterscheidung der überladenenMethode spielen die Signaturen eine entscheidende Rolle. Mit Hilfe der Signaturen lassen sich Datentypen eindeutig und relativ kompakt beschreiben. Signaturen Type Signature Java Type Z boolean EDV2 - 02 - JavaNativeInterface B byte C char S short I int J long F float D double V void Lfully-qualified-class; fully-qualified-class [type type[] (arg-types)ret-type 33 method type Beispiele für Signaturen Signatur von Feldern: Für jeden Index eine "[" anschließend die Signatur des Elementtyps. Z.B.: double[][] [[D Signatur von Objekten: Lvoller-Klassen-Name; Z.B.: String Ljava/lang/String; EDV2 - 02 - JavaNativeInterface 34 Signatur von Methoden: (Signaturen der Parameter)Signatur des Wertes. Z.B.: double[] methode(int[][], boolean)([[DZ)[D Zugriff auf Attribute von Objekten EDV2 - 02 - JavaNativeInterface 35 Allgemeiner Typ von Objekten: jobject Der Zugriff auf Attribute erfolgt über den FieldID. Um den FieldID zu bestimmen benötigt man: Die Klasse zu der das Objekt gehört. Den Namen des Attributes. Die Signatur des Attributes. Es muss unterschieden werden zwischen statischen und nichtstatischen Attributen und Methoden. jclass GetObjectClass(JNIEnv *env, jobject obj) bestimmt die Klasse zu der das Objekt obj gehört. jclass FindClass(JNIEnv *env, const char *name) bestimmt die Klasse anhand ihres vollständigen Namens Für interne Klassen gilt der Klassenname: Klasse$interneKlasse nichtstatische Attribute EDV2 - 02 - JavaNativeInterface 36 jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig) bestimmt aus der Klasse, dem Attributnamen und seiner Signatur den FieldID. <NativeType> Get<PrimitiveType>Field(JNIEnv *env, jobject obj, jfieldID fieldID) liest den Wert des Attributes, funktioniert auch für Objekt-Typen void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID, <NativeType> value) schreibt einen Wert in das Attribut Beispiel ... method(JNIEnv *env, jclass class, jobject obj) { jclass objClass; jfieldID attrID; jint intAttr; EDV2 - 02 - JavaNativeInterface objClass=(*env)->GetObjectClass(env, obj); attrID=(*env)->GetFieldID(env, class, "dimension", "I"); intAttr=(*env)->GetIntField(env, obj, attrID); intAttr=2*intAttr+77; (*env)->SetIntField(env, obj, attrID, intAttr); } 37 statische Attribute EDV2 - 02 - JavaNativeInterface 38 jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig) bestimmt aus der Klasse, dem Attributnamen und seiner Signatur den FieldID. <NativeType> GetStatic<PrimitiveType>Field(JNIEnv *env, jclass clazz, jfieldID fieldID) liest den Wert des Attributes, funktioniert auch für Objekt-Typen void SetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID, <NativeType> value) schreibt einen Wert in das Attribut Beispiel ... method(JNIEnv *env, jclass class, jobject obj) { jclass objClass; jfieldID attrID; jint intAttr; EDV2 - 02 - JavaNativeInterface objClass=(*env)->GetObjectClass(env, obj); attrID=(*env)->GetStaticFieldID(env, class, "dimension", "I"); intAttr=(*env)->GetStaticIntField(env, class, attrID); intAttr=2*intAttr+77; (*env)->SetStaticIntField(env, class, attrID, intAttr); } 39 nichtstatische Methoden von Objekten EDV2 - 02 - JavaNativeInterface 40 jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) bestimmt den MethodID einer Methode. Bei überladenen Methoden wird die sie durch die Signatur identifiziert. Diese Varianten des Aufrufs von JAVA-Methoden unterscheiden sich nur in der Art der Übergabe der Parameter. <NativeType> Call<PrimitiveType>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...) alle Parameter werden nacheinander angegeben <NativeType> Call<PrimitiveType>MethodA(JNIEnv *env, jobject obj, jmethodID methodID, jvalue *args) die Parameter werden als Feld übergeben <NativeType> Call<PrimitiveType>MethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args) die Parameter werden als Liste übergeben Beispiel method(JNIEnv *env, jclass class, jobject obj) { jclass objClass; jmethodID methID; EDV2 - 02 - JavaNativeInterface 41 objClass=(*env)->GetObjectClass(env, obj); methID=(*env)->GetMethodID(env, objClass, "setDimension", "(I)V"); (*env)->CallVoidMethod(env, obj, methID, 777); } Aufruf statischer Methoden EDV2 - 02 - JavaNativeInterface 42 jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) <NativeType> CallStatic<PrimitiveType>Method (JNIEnv *env, jclass clazz, jmethodID methodID, ...) alle Parameter werden nacheinander angegeben <NativeType> CallStatic<PrimitiveType>MethodA (JNIEnv *env, jclass clazz, jmethodID methodID, jvalue *args) die Parameter werden als Feld übergeben <NativeType> CallStatic<PrimitiveType>MethodV (JNIEnv *env, jclass clazz, jmethodID methodID, va_list args) die Parameter werden als Liste übergeben Beispiel ... jdouble sin(JNIEnv *env, jclass clazz, jdouble x) { jclass cl; jmethodID sinID; jdouble sx; EDV2 - 02 - JavaNativeInterface 43 cl=(*env)->FindClass(env, "java/lang/StrictMath"); sinID=(*env)->GetStaticMethodID(env, cl, "sin", "(D)D"); sx=(*env)->CallStaticDoubleMethod(env, cl, sinID, x); return sx; } Neue JAVA-Objekte erzeugen EDV2 - 02 - JavaNativeInterface 44 jobject AllocObject(JNIEnv *env, jclass clazz) erzeugt eine neues Objekt der Klasse clazz ohne einen Konstruktor aufzurufen jobject NewObject(JNIEnv *env, jclass clazz, jmethodID methodID, ...) jobject NewObjectA(JNIEnv *env, jclass clazz, jmethodID methodID, jvalue *args) jobject NewObjectV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args) erzeugt ein neues Objekt, wobei der Konstruktor mit der ID methodID und den angegebenen Parametern benutzt wird. Als Name des Konstruktors wird "<init>" verwendet. Als Signatur für den return-Wert "V". Bei internen Klassen ist die Parameterliste am Anfang um einen Parameter jclass zu erweitern. Diesem ist der Parameter class der C-Methode zu übergeben. Beispiel ...jobject method(JNIEnv *env, jclass clazz) { jclass tsClass; jmethodID tsConst; jobject tsObj; jmethodID tsAdd; EDV2 - 02 - JavaNativeInterface 45 tsClass=(*env)->FindClass(env, "java/util/TreeSet"); tsConst=(*env)->GetMethodID(env, tsClass, "<init>", "()V"); tsObj=(*env)->NewObject(env, tsClass, tsConst); tsAdd=(*env)->GetMethodID(env, tsClass, "add", "(Ljava/lang/Object;)Z"); (*env)->CallBooleanMethod(env, tsObj, tsAdd, (*env)->NewStringUTF(env, "Mueller")); (*env)->CallBooleanMethod(env, tsObj, tsAdd, (*env)->NewStringUTF(env, "Meier")); return tsObj; } Das Tool javap Mit javap kann man class-Files bearbeiten. Javap liefert: Ausgabe des Byte-Codes (-c) Tabelle der lokalen Variablen (-l) Signaturen alle Methoden (-s) EDV2 - 02 - JavaNativeInterface 46