Kaffee mit Vitamin C
JNI als ultimativer Zweikomponentenkleber
(C) Michael Härtfelder, 2000 <michael@haertfelder.com>
Artikel veröffentlicht in der Zeitschrift c't 20/2000

 

Home

Einführung

Wer hätte gedacht was das Schatzkästlein des Java Development Kits (JDK) noch so alles bietet. Da gibt es also die Möglichkeit aus Java heraus Bibliotheken fremder Programmiersprachen (eben "native" Code) aufzurufen. Natürlich mag man einwenden, dass dies einem Rückschritt gleichkommt. Denn schliesslich wurde Java unter der Prämisse entwickelt Betriebssystemunabhängigkeit zu gewährleisten. Und nun soll also wieder auf nicht portierbaren Code zugegriffen werden ?

Im Grunde genommen stimmt das schon. Aber in der Praxis steht man nun einmal des öfteren vor Problemen die einen Rückgriff auf native Code erfordern. Da sind zunächst all jene Situationen zu nennen, in denen bereits eine fertige Bibliothek eines externen Anbieters besteht und deren Funktionalität verwendet werden soll. Noch allgemeiner gesprochen sind C(++)-Schnittstellen zu angrenzenden Systemen vorhanden in die oder aus denen Daten transportiert werden soll. Da bleibt einem nur die Wahl zwischen der Neuimplementierung der Bibliothek in Java mit dem entsprechenden immensen Aufwand, dem Verzicht auf Java oder aber man greift eben zu JNI.

JNI bietet sich auch in Situationen an in denen Teile der Funktionalität sehr rechenintensiv sind und daher in C(++) oder Assembler geschrieben werden sollen. Verwendet man JNI lässt sich eine komfortable Java-Oberfläche schreiben und bindet als Rechenknecht eine C-Library ein.

Alles was man dazu benötigt ist das JDK ab Version 1.1 und einen C(++)-Compiler. Ob der native Code dabei in C oder C++ geschrieben ist spielt keine entscheidende Rolle. Bei den weiteren Ausführungen wird vom kleinsten gemeinsamen Nenner, von C Code, ausgegangen.

In der Praxis ist der Aufruf einer C Bibliothek aus einem Java Programm heraus wesentlich häufiger anzutreffen als der Aufruf von Java aus C heraus. Deshalb wird bei den folgenden Ausführungen auch diese Aufrufstruktur im Vordergrund stehen.

Der Entwicklungsprozess (vergleiche Schaubild 1) ist dabei relativ einfach zu beschreiben:

  1. Schreiben der Java Klasse
  2. Compilieren der Java Klasse
  3. Generieren einer C-kompatiblen headerdatei aus der Java Klasse
  4. Schreiben der C Methode(n)
  5. Compilieren der C Bibliothek
  6. Starten der Java Klasse und automatischem Ansprechen der C Methode aus Java heraus.
Wie ist das Ganze nun auf das obligatorische "HelloWorld" anzuwenden ?

class helloworld {
  private native void callnative();

  public static void main(String[] args)
    {
    new helloworld().callnative();
    }

  static {
    System.loadLibrary("hello");
    }
  }
[Listing 1] helloworld.java

In Listing 1 fallen drei Dinge auf: Da ist zunächst die Deklaration einer native Funktion "callnative()". Dieses Statement ist vergleichbar mit einer "extern" Deklaration einer externen Funktion unter C++. Für jede Funktion, die später in einer externen Bibliothek angesprochen werden soll, muss eine native Deklaration im Java Programm vorhanden sein.

Weiterhin fällt die "System.loadLibrary()"-Anweisung zum Einbinden der externen Bibliothek mit dem Namen "hello" auf. Native Bibliotheken werden nicht statisch gelinkt sondern dynamisch geladen. Der physische Name der Bibliothek nach der während des Ladevorgangs gesucht wird, ist dabei unter Windows "hello.dll" und unter SunSolaris/Linux "libhello.so".

Zuletzt nun der eigentliche Aufruf der externen Funktion "callnative()" der so unauffällig und selbstverständlich erscheint, als ob callnative() eine Java Methode der Klasse HelloWorld wäre.

Das mit dem Compilieren der Java Klasse entstehende HelloWorld.class dient nun als Ausgangsbasis für den sich anschliessenden wichtigen Schritt, der Generierung der C-header Datei.

Dazu wird das Programm javah.exe aus dem Java Development Kit verwendet und der Befehl

javah -jni HelloWorld

ausgeführt. Als Ergebnis erhält man eine neue Datei HelloWorld.h mit den Prototypen für den C-Code in einer sehr speziellen Form:

JNIEXPORT void JNICALL Java_Helloworld_callnative(JNIEnv *, jobject);

JNIEXPORT und JNICALL sind dabei Macros die als gegeben hingenommen werden können. Interessanter ist da schon die Art der Namensgebung für die callnative Funktion. Im Allgemeinen orientiert sich dieser Funktionsname an dem Schema

"Java_"<Klassenname>"_"<Funktionsname>

Abweichungen von diesem Format existieren nur bei überlagerten Funktionen die aber hier nicht besprochen werden sollen. Merkwürdig erscheint über die dargestellten Syntax hinaus zunächst auch die Tatsache, dass zwei Übergabeparameter auftauchen, die im Java Quellcode gar nicht deklariert wurden. Diese Argumente sind jedoch Standard bei der Verwendung von JNI. Der erste Parameter verweist auf das JNI-Environment Interface während der zweite eine Art "this" pointer auf das HelloWorld Objekt selbst ist.

Nun ein Blick auf den C Code "hello.c" in Listing 2.

#include <jni.h>
#include <stdio.h>
#include "helloworld.h"

JNIEXPORT void JNICALL Java_helloworld_callnative(JNIEnv *env,
                                                  jobject obj)
   {
   printf("HelloWorld\n");
   return;
   }
[Listing 2] hello.c

Kennzeichnend für die JNI Bibliothek ist die schon bekannte Funktionsdefinition gemäss der Prototypendeklaration sowie die header Dateien "jni.h" und "HelloWorld.h". Erstere muss immer eingebunden werden und enthält JNI spezifische Informationen. HelloWorld.h ist die mit javah bereits erzeugte header-Datei.

Jetzt bleibt nur noch die Compilierung des C-Codes. Unter SunSolaris sieht das etwa wie folgt aus:

cc -G -I/java/include -I/java/include/solaris hello.c -o libhello.so

Die Option -G signalisiert dem Compiler, dass eine Shared Library zu erzeugen ist und kein einzeln ausführbares Programm.

Die Pfadangabe "/java/" ist im Einzelfall natürlich an den jeweiligen Installationspfad des jdk anzupassen. <jni.h> ist also eine Include-Datei die Bestandteil des Java Develoment Kits ist und nicht des C++ Compilers. Unter Linux wäre folgendes einzugeben:

gcc -shared -I/usr/local/java/include -I/usr/local/java/include/genunix hello.c -o libhello.so

Die Includepfade sind ggf. anzupassen.

Tja, eigentlich war das schon alles, was zu tun wäre. Das Programm kann jetzt wie üblich mit java Helloworld aufgerufen werden. Mitunter tritt an dieser Stelle noch ein Problem mit dem Library-Suchpfad auf. Programmabbrüche mit einem Fehlerhinweis der Art java.lang.UnsatisfiedlinkError: no hello in library path deuten darauf hin, dass (unter Solaris) der LD_LIBRARY_PATH nicht richtig gesetzt bzw. angepasst wurde. In dieser Umgebungsvariablen wird die Suchreihenfolge für Libraries festgelegt. Im Fehlerfalle ist libhello.so in eines der Verzeichnisse im Pfad zu kopieren oder alternativ (falls es sich im selben Verzeichnis wie HelloWorld.class befindet) die Variable mit

LD_LIBRARY_PATH=.
export LD_LIBRARY_PATH

neu zu setzen.

Ein paar Worte zum Vorgehen unter Windows. Da an den Compiler keine besonderen Ansprüche gestellt werden ist jeder Compiler verwendbar der die Erzeugung von DLLs unterstützt. Im allgemeinen kann das der MS Visual C++ ab V5.0 sowie die gcc Windows-Portierungen. Gelegentlich wird bei speziellen Compilern über Probleme mit dem Datentyp __int64 (aus jni_md.h) berichtet. Dies kann durch die Deklaration von

#ifdef FOOBAR_COMPILER
#define __int64 <special signed_64_bit_type>
#endif

umgangen werden, wobei der unterstützte 64 Bit Typ einzusetzen ist.

Das Erzeugen der DLL mit dem MS Visual C++ könnte dann wie folgt aussehen:

cl -Ix:\java\include -Ix:\java\include\win32 -MD -LD hello.c -Fehello.dll

Sollte bislang mit Visual C++ nur über die GUI entwickelt worden sein, dann müssen vor dem erstmaligen Aufruf über Kommandozeilen noch einmalig die entsprechenden Umgebungsvariablen gesetzt werden. Dazu ist die Batch-Datei vcvars32.bat aufzurufen, die diese Arbeit vollständig für den User übernimmt.

Parameterübergabe

Als nächster Schritt bietet sich an das vorherige HelloWorld-Beispiel um die Übergabe von Parametern bzw. den Zugriff auf Java-Klassenvariablen zu erweitern. In der neuen Bibliotheksfunktion "jnipower" soll eine ganze Zahl vom Typ Integer mit einem Wert vom Typ Float potenziert und darauf ein Wert von Typ Double addiert werden. Dabei werden die beiden ersten Werte als Parameter übergeben während der zu addierende Term direkt aus der aufrufenden Klasse abgeholt wird. Das Ergebnis wird als Wert der Funktion an Java zurückgegeben.

Eine mögliche Implementierung der Java-Klasse ist in Listing 3 zu sehen und deren dazugehörige C-Library "two.c" in Listing 4.

class classtwo {
  static int iii = 4;
  static float fff = (float)2.50;
  static double ddd = 0.10;
  static String sss = "Vor der Berechnung von";

  private static native double jnipower(int in_iii,
                                        String in_sss,
                                        float in_fff);

  public static void main(String[] args)
    {
    double res = jnipower(iii,sss,fff);
    System.out.println("Ergebnis des JNI Aufrufs=" + res);
    }

  static {
    System.loadLibrary("two");
    }
  }
[Listing 3] classtwo.java

#include <jni.h>
#include <stdio.h>
#include <math.h>
#include "classtwo.h"

JNIEXPORT jdouble JNICALL Java_classtwo_jnipower(JNIEnv *env,
                                                 jclass in_cls,
                                                 jint in_int,
                                                 jstring in_string,
                                                 jfloat in_float)
  {
  /* Java String in C-String konvertieren und an C-Variable
     zuweisen: */
  const char *c_string = (*env)->GetStringUTFChars(env, in_string, 0);

  /* Adresse des Feldes "ddd" in der der aufrufenden Klasse
     ermitteln: */
  jfieldID jfid = (*env)->GetStaticFieldID(env, in_cls, "ddd", "D");
  /* Wert des Feldes "ddd" aus der aufrufenden Klasse auslesen: */
  double in_double = (*env)->GetStaticDoubleField(env, in_cls, jfid);

  printf("%s %lf + pow(%d,%f)\n",
         c_string, in_double, in_int, in_float);

  return (in_double + pow(in_int,in_float));
  }
[Listing 4] two.c

Aus dem erweiterten Beispiel ist die Handhabung und der Umgang mit Basistypen und mit Strings zu erkennen. Das entscheidende Problem bei der Kommunikation von Programmmodulen aus verschiedenen Programmierwelten ist die physische Konvertierung der Datentypen dem sogenannten Mapping (bzw. Marshalling). Folgende Tabelle gibt Aufschluss wie die Java-Datentypen in C gesehen werden:

JavaTypeNativeTypeBeschreibung
boolean jboolean unsigned 8 bits
byte jbyte signed 8 bits
char jchar unsigned 16 bits
short jshort signed 16 bits
int jint signed 32 bits
long jlong signed 64 bits
float jfloat 32 bits
double jdouble 64 bits
void void N/A
[Tabelle 1]

Wie zu erkennen, lassen sich die übergebenen Parameter, sofern sie einen Basistyp darstellen, einfach im C-Code einbinden. Etwas anders sieht es mit den Variablen aus, die irgendwo in der Bytecodesuppe der aufrufenden Java-Klasse schwimmen. Deren genaue Position muss erst mit der JNI-Funktion GetStaticFieldID() lokalisiert und in einem zweiten Schritt explizit mit einer JNI-Funktion wie etwa "GetStaticDoubleField()" ausgelesen werden.

Aufmerksamkeit verdient der dritte Parameter der Funktion GetStaticFieldID(), in diesem Falle "D". Hierdurch wird der Datentyp des zu suchenden Feldes festgelegt. Folgende kleine Tabelle zeigt eine Übersicht des Mappings der zur Auswahl stehenden sogenannten "Signaturen".

TypeSignatureJavaType
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
[Tabelle 2]

O.k. das ist jetzt a bisserl kryptisch. Am besten fährt man damit wenn man die Kürzel nicht zu hinterfragen versucht, sondern als gegeben hinnimmt. Was suche ich ? Eine double Variable ? Also nehme ich "D" als dritten Parameter. Oder wie wäre es mit einer Long Variablen ? Dann ist ein "J" einzusetzen.

Strings

Ein paar Bemerkungen zu Strings: Diese sind, da sie keinen atomaren Basistyp darstellen, nicht in der Tabelle 1 aufgeführt und werden aus C-Sicht wie ein Array of chars behandelt bzw. aus Java-Sicht durch die Klasse java.lang.String repräsentiert.

JNI verwendet zur physischen Repräsentation der Strings das Format UTF-8. UTF-8 Strings sind dieselben die auch von Java verwendet und unterscheiden sich nur wenig von der Unicode Darstellung. Ohne zu detailliert darauf eingehen zu wollen, benötigt man im allgemeinen nur drei UTF spezifische Funktionen im native Code im Zusammenhang mit JNI. Eine Funktion zum Konvertieren von UTF-8 (Java) String in C-Strings:

const jchar *c_str = (*env)->GetStringUTFChars(env, jstring_var, 0);

Eine Funktion zum Konvertieren von C-Strings zurück in UTF-8 (Java) Strings:

char buffer[128];
jstring jstring_var = (*env)->NewStringUTF(env, buffer);

Eine Funktion zum Freigeben von durch UTF-8 gehaltenem Speicherplatz:

(*env)->ReleaseStringUTFChars(env, jstring_var, buffer);

Bequemerweise erhält man im zweiten Beispiel schon die Referenz auf den String als Parameter übergeben. Wie aber müsste man vorgehen, wenn man den String analog der Variablen "ddd" erst in der aufrufenden Java-Klasse suchen müsste ?
Für Strings ist die zuständige Javaklasse bekannt: java.lang.String. Gemäss Tablle 2 wäre daher die Signatur "Ljava/lang/String;" (Man beachte "L", "/" und ";") als dritter Parameter einzutragen.

Objekte

Aber JNI bietet selbstverständlich noch sehr viel weiterreichende Möglichkeiten als nur die Übergabe von Basistypen-Parametern und Strings. Komplexere Strukturen wie etwa ganze Klassen können auch über- bzw. zurückgegeben werden. Dazu wiederum ein Beispiel. Zusätzlich zu der aufrufenden Javaklasse "classthree" und der aufgerufenen Bibliotheksfunktion "dem2euro" sind zwei Container-Klassen "Request" und "Response" zu definieren in der analog einem struct in C eine Anzahl von Variablen enthalten sind.

Listing 5 gibt einen Überblick über eine Möglichkeit einer Java-Implementierung.

class classthree {
  private static native Response dem2euro(Request in_req);

  public static void main(String[] args)
    {
    Request req = new Request();
    req.whrg_betrag = (long)(Math.random() * 1000);
    req.whrg_kuerzel = "DEM";

    //classthree c = new classthree();
    //c.dummy = 9898;
    //Response resp = c.calc(req);
    Response resp = dem2euro(req);
    if (resp.calc_erfolgreich)
       {
       System.out.println("Ergebnis=" + resp.euro_betrag + " Euro, " +
                          "konvertiert aus " + req.whrg_betrag + " " +
                          resp.whrg_langbezeichnung);
       }
    else
       {
       System.out.println("Kein DEM Betrag zum Konvertieren " +
                          "vorhanden");
       }
    }

  static {
    System.loadLibrary("three");
    }
  }

class Request {
   long    whrg_betrag;
   String  whrg_kuerzel;
   }

class Response {
   double  euro_betrag;
   boolean calc_erfolgreich;
   String  whrg_langbezeichnung;
   }
[Listing 5] classthree.java

Die hierin referenzierte C Library "three.c" (Listing 6) ist für die Konvertierung von DEM-Beträgen in Euro zuständig.

#include <jni.h>
#include <stdio.h>
#include "classthree.h"

JNIEXPORT jobject JNICALL Java_classthree_dem2euro(JNIEnv *env,
                                               jclass in_cls,
                                               jobject in_request)
   {
   double dem_rate_c = 1.95583;

   double exchange_rate_c = 0.0;
   long whrg_betrag_c = 0;
   const char *whrg_kuerzel_c;
   double euro_betrag_c = 0.0;
   int calc_erfolgreich_c = 0;
   char whrg_langbezeichnung_c[30];
   jclass jcls;
   jfieldID jfid;
   jmethodID jmid;
   jobject jobj;

   /*  Die Klasse des übergebenen Objektes bestimmen: */
   jcls = (*env)->GetObjectClass(env,in_request);

   /* Die Adresse des Feldes "whrg_betrag" in dem übergebenen
      Objektes ermitteln: */
   jfid = (*env)->GetFieldID(env, jcls, "whrg_betrag", "J");
   /* Den Wert des Feldes "whrg_betrag" auslesen: */
   whrg_betrag_c = (*env)->GetLongField(env, in_request, jfid);

   jfid = (*env)->GetFieldID(env, jcls, "whrg_kuerzel",
                             "Ljava/lang/String;");
   whrg_kuerzel_c = (*env)->GetStringUTFChars(env,
     (jstring)((*env)->GetObjectField(env,in_request, jfid)),NULL);

   /* Nur Konvertieren wenn DEM Betrag */
   if (!strcmp(whrg_kuerzel_c,"DEM"))
      {
      euro_betrag_c = whrg_betrag_c / dem_rate_c;
      calc_erfolgreich_c = 1;
      strcpy(whrg_langbezeichnung_c,"Deutsche Mark");
      }

   /* Klassendefinition für Klasse "Response" suchen: */
   jcls = (*env)->FindClass(env, "Response");
   if (jcls == NULL)
      {
      printf("Error FindClass\n");
      return NULL;
      }

   /* Adresse des Konstruktors der Klasse "Response" ermitteln: */
   jmid = (*env)->GetMethodID(env, jcls, "<init>","()V");
   if (jmid == NULL)
      {
      printf("Error GetMethodID\n");
      return NULL;
      }

   /* Neues Objekt der Klasse "Response" erzeuegen und
      initialisieren: */
   jobj = (*env)->NewObject(env, jcls, jmid);
   if (jobj == NULL)
      {
      printf("Error NewObject\n");
      return NULL;
      }

   /* Adresse des Feldes "euro_betrag" in dem neuen Objekt
      ermitteln: */
   jfid = (*env)->GetFieldID(env, jcls, "euro_betrag", "D");
   /* Wert des Feldes "euro_betrag" in dem neuen Objekt füllen: */
   (*env)->SetDoubleField(env, jobj, jfid, euro_betrag_c);

   jfid = (*env)->GetFieldID(env, jcls, "calc_erfolgreich", "Z");
   (*env)->SetBooleanField(env, jobj, jfid, calc_erfolgreich_c);

   jfid = (*env)->GetFieldID(env, jcls, "whrg_langbezeichnung",
                             "Ljava/lang/String;");
   (*env)->SetObjectField(env, jobj, jfid,
                (*env)->NewStringUTF(env, whrg_langbezeichnung_c));

   /* Neues Objekt zurückgeben: */
   return jobj;
   }
[Listing 6] three.c

Um auf die unterschiedlichen Felder eines durch jobject typisierten Java- Objektes zugreifen zu können, muss zunächst die Klassendefinition bekannt sein. Diese wird mit der JNI-Funktion "GetObjectClass()" ermittelt. Mit der jetzt bekannten Referenz auf die Klasse lassen sich auch die einzelnen Felder identifizieren. Da die Variablen nicht wie in Beispiel zwei statisch definiert sind, ist diesmal die JNI-Funktion "GetFieldID()" zu verwenden, um die Referenz auf den Speicherplatz eines Feldes zu erhalten. Dann lässt sich (wie hier für die Variable "whrg_betrag") der Wert mit der für Long-Variablen zuständigen JNI-Funktion "GetLongField()" abgreifen (vergleiche auch Schaubild).

Lassen sich eigentlich im native Code nur existierende Objekte empfangen ? Nein, man könnte auch Neue erzeugen und zurückgeben. Dazu ist die Klassendefinition zu suchen (FindClass()), der dazugehörige Konstruktor darin zu lokalisieren (GetMethodID()), das Objekt zu allokieren (NewObjekt()), die Felder zu füllen (SetXXXXField()) und abschliessend die Referenz zurückzugeben (return jobj).

Callback

Bislang wurde nur über Aufrufe von Java in Richtung C(++) gesprochen. Dass auch der umgekehrte Weg gangbar ist soll das letzte Beispiel zeigen.

Nachdem die Java-Klasse classfour wie in den bisherigen Beispielen auch eine Methode der Library aufgerufen hat, soll diese Library Funktion nun ihrerseits eine Methode der aufrufenden Java-Klasse ansprechen. Dabei ist der Library- Funktion die genaue Aufrufadresse der Callbackmethode zunächst unbekannt. Einzig und allein der Funktionsname muss bekannt sein einschliesslich der genauen Schreibweise (Gross-/Kleinschreibung)

Da man darauf angewiesen ist die Funktion Callback() nicht nur irgendeiner Instanz der Klasse classfour anzusprechen, sondern genau diejenige der aufrufenden Instanz, muss entsprechend nicht nur auf Klassenebene, sondern auf Instanzebene danach gesucht werden. Wie aber bekommt man die Objektreferenz auf das aufrufende Objekt ? Eigentlich ist sie schon bekannt: Es ist die zweite Variable der übergebenen Parameterliste.

Listing 7 und 8 zeigt eine solche Konstruktion.

class classfour {
  static int number = (int)(Math.random()*1000);

  private native void checknumber(int in_i);

  private void callback(int in)
    {
    System.out.print("In Java callback(): ");
    if (in == number)
       {
       System.out.println("Ja, die Zahl hat zwei JNI Calls " +
                          "überlebt " + number + "==" + in);
       }
    else
       {
       System.out.println("Nein, die Nummer hat sich verändert " +
                       "während der JNI Calls " + number + "->" + in);
       }
    }

  public static void main(String[] args)
    {
    classfour c = new classfour();
    c.checknumber(number);
    System.out.println("Und wieder in Java main()");
    }

  static {
    System.loadLibrary("four");
    }
  }
[Listing 7] classfour.java

#include <jni.h>
#include <stdio.h>
#include "classfour.h"

JNIEXPORT void JNICALL Java_classfour_checknumber(JNIEnv *env,
                                                  jobject in_obj,
                                                  jint  in_number)
   {

   /* Klasse des aufrufenden Java Objektes ermitteln: */
   jclass icls = (*env)->GetObjectClass(env, in_obj);

   /* Adresse der Methode "callback" des aufrufenden Java Objektes
      ermitteln: */
   jmethodID jmid = (*env)->GetMethodID(env, icls, "callback",
                                        "(I)V");
   if (jmid == 0)
      {
      printf("jmid == NULL\n");
      }
   /* Methode "callback" des aufrufenden Java Objektes aufrufen: */
   (*env)->CallVoidMethod(env, in_obj, jmid, in_number);
   printf("Wieder zurück im native Code\n");
   }
[Listing 8] four.c

Auch in diesem Beispiel taucht der erfrischend offensichtliche Signaturparameter "(I)V" auf, diesmal bei der Verwendung der JNI-Funktion "GetMethodID()".

Ähnlich wie beim Zugriff auf einzelne Variablen einer Klasse (siehe zweites und drittes Beispiel) muss auch beim Zugriff auf Methoden einer Java-Klasse die Signatur mit angegeben werden. Diese Signatur folgt dabei dem Schema "Klammer auf, konkatenierte Signaturparameterliste der Übergabeargumente ohne Blank und Komma, Klammer zu, Signatur des Rückgabewertes". Beispiel gefällig ? Es ist aus C heraus eine Java-Methode "blubb" mit folgender Definition zu suchen:

long blubb(int n, String s, int[] arr);

Der korrekt JNI-Suchbefehl hierfür lautet (vergleiche auch Tabelle 2 mit den Typ-Signaturen):

(*env)->GetMethodID(...., "blubb", "(ILjava/lang/String;[I)J");

Mag das auch beim erstmaligen Gebrauch verwirrend erscheinen, einmal korrekt ins Werk gesetzt ist das Mapping stabil und wenig fehlerträchtig.

Packages and more

Hinzuweisen bleibt auf die Handhabung von package Strukturen. Ist eine Java- Klasse Teil eines packages, dann hat dies auch Auswirkung auf die Zusammenstellung des Funktionsnamens. Aus der Spezifikation

... Java_classname_functionname(...)

wird dann

... Java_packagename_classname_functioname(...).

Dies triff auch für jede weitere Packageverschachtelung zu. Somit wird etwa aus

package xxx.yyy.zzz;

ein Funktionspräfix Java_xxx_yyy_zzz_......

Im Zweifelsfall hilft immer ein Blick auf die Funktionsprototypen in der durch javah generierten headerdatei.

Wie eingangs erwähnt können prinzipiell sowohl C als auch C++ Bibliotheken über JNI angesprochen werden. In der Handhabung unterscheiden sie sich nur wenig. Ein Unterschied besteht in der Codierung der JNI spezifischen Funktionen. Aus der C-Schreibweise

(*env)->xxxxxx(env,.....)

wird unter C++ generell

env->xxxxxx(.....).

Wrapperklassen

Nun noch einmal zurück zum Problem einer bereits existierenden Library und wie diese verwendet werden kann. Wie weiter oben schon angesprochen bedarf es nicht nur javaseitig einer geringfügigen Vorbereitung auf JNI sondern auch auf Seiten der Bibliothek. Wie kann das aber geschehen, wenn der Quellcode der Bibliothek nicht vorliegt ?

Dazu liegt es nahe einfach eine Wrapperbibliothek zu schreiben, also eine Funktion, die über JNI Anfragen entgegennimmt und dann den eigentlichen Aufruf der vorgegebenen Bibliothek ausführt. Wenn ausser den Typkonvertierungen keine weitere Funktionalität in der Wrapperklasse eingebaut wird, erhält man einen transparenten Layer der einen Zugriff auf precompilierte Binaries ermöglicht.

JNI oder Corba

Nachdem nun JNI und dessen Möglichkeiten vorgestellt wurden stellt sich die Frage nach der Abgrenzung zu (scheinbar) verwandten Technologien. Wird nicht in ähnlichen Szenarien, in denen es um die Kopplung unterschiedlicher Programmierwelten geht, auf Corba verwiesen ?

Zur Beantwortung dieser Frage müssen die Rahmenbedingungen des Projektes betrachtet werden. Corba, das in letzter Zeit in der Praxis immer deutlicher an Bedeutung zu gewinnen scheint, ist dort erste Wahl wo es um die Nutzung von verteilten funktionalen Ressourcen geht. Der Akzent liegt hier auf dem Wort "verteilt". Bieten mehrere, möglicherweise im lokalen Netz physisch verteilte Objekt-Server Funktionalität an, die von unterschiedlichen Software-Konsumenten genutzt wird, dann ist Corba als Kommunikationsprotokoll eindeutig zu empfehlen.

RMI fällt ohnehin als Alternative aus, da hierdurch nur Kommunikation zwischen einzelnen Java Bausteinen, nicht aber zu Komponenten anderer Programmiersprachen aufgebaut werden kann.

JNI hingegen sollte immer dann zum Zuge kommen, wenn eine Funktionsbibliothek eindeutig EINER aufrufenden Klasse zugeordnet ist. Dies bedeutet sicherlich nicht, dass eine Funktionsbibliothek nicht auch so angepasst werden könnte, dass sie zugleich Diener zweier (oder mehrerer) Klassen sein könnte. Aber typischerweise besteht eine 1:1 Beziehung.

Darüber hinaus ist auch der Kommunikations- und Verwaltungsaufwand in Betracht zu ziehen. Corba erfordert die Installation und Konfiguration von ObjectRequestBrokern (ORBs), d.h. von zusätzlicher (zumeist kostenpflichtiger) Software. Diese Software steuert den Verbindungsaufwand und Datentransport zwischen den Beteiligten. Klar, dass dies auch Performanceeinbussen nach sich zieht.

Demgegenüber benötigt man für JNI keine weitere Software. Nichts ist zwischen Java und C(++). Just JNI.

Dies eröffnet dem Einsatz von JNI einzigartige Chancen in Projekten in denen es auf die Wiederverwendung existierender externer Komponenten, auf Performancesteigerung sowie auf kostengünstige Realisation ankommt.

Rückfragen und Kommentare an email michael@haertfelder.com
Michael Härtfelder, 07.05.2000

Literatur:
- Sheng Liang, The Java Native Interface, Addison-Wesley, 1999
- http://java.sun.com/products/jdk/1.2/docs/guide/jni/jni-12.html