Das Java Native Interface Wozu JNI? 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 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. Aufruf von C-Routinen aus JAVA 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. HelloWorld: aufzurufendes C-Programm #include <stdio.h> void helloWorld() { printf("Hello, world!\n"); } 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. Struktur HelloWorld.java HelloWorld.class Hello.java Hello.class C_hello.h C_hello.c C_hello.dll hello.h hello.c hello.lib Aufrufendes JAVA-Programm public class Hello { public static native void helloWorld(); static { System.loadLibrary("C_hello"); } } 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 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 Erzeugtes header-File C_Hello.h /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class Hello */ #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 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. #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 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 { public static void main(String[] args) { Hello.helloWorld(); } } Übergabe von Parametern 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() { printf("Hello, world!\n"); } void echoText(const char *text) { printf("%s\n",text); } 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. /* * 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); 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; 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); 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); strcat(s,s2); return s; } Hello.java public class Hello { public static native void helloWorld(); public static native void echoText(String text); public static native String concatStrings(String s1, String s2); static { System.loadLibrary("C_hello"); } } C_hello.h /* * Class: Hello * Method: concatStrings * Signature: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; */ 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) { 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; } Zugriffsmethoden für Zeichenketten 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. 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 <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 ! JNI_ABORT : Ressourcen freigeben und Daten nicht kopieren Teile von Feldern umkopieren 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 JAVA-Typen / C-Typen 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; for (i=0;i<m;i++) { result[i]=0.0; for (j=0;j<n;j++) { result[i]+=matrix[i*n+j]*vector[j]; } } } C_hello.h /* * Class: Hello * Method: matrixXvector * Signature: ([[D[D)[D */ 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; 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; } Zugriff auf JAVA-Objekte 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 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 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; Signatur von Methoden: (Signaturen der Parameter)Signatur des Wertes. Z.B.: double[] methode(int[][], boolean)([[DZ)[D Zugriff auf Attribute von Objekten 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 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; 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); } statische Attribute 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; 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); } nichtstatische Methoden von Objekten 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; objClass=(*env)->GetObjectClass(env, obj); methID=(*env)->GetMethodID(env, objClass, "setDimension", "(I)V"); (*env)->CallVoidMethod(env, obj, methID, 777); } Aufruf statischer Methoden 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; 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 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; 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)