20
Von
Charles L. Perkins
Heute lernen Sie die Gründe für die Verwendung von nativen Methoden in Java kennen und die in Java integrierten Optimierungen und Tricks, mit denen Sie ihre Programme beschleunigen können. Ferner lernen Sie die Prozedur zum Erstellen von Headern für native-Methoden und zum Verknüpfen derselben in einer dynamischen ladbaren Bibliothek kennen. Und schließlich befassen wir uns mit der Java Native Interface (native Schnittstelle von Java).
Wir beginnen jedoch mit den Gründen für eine Implementierung von native-Methoden für die Verwendung anderer Sprachen als Java.
Für die Deklaration von native-Methoden gibt es nur zwei Gründe. Der erste und bei weitem wichtigste Grund ist, daß Sie spezielle Fähigkeiten Ihres Rechners oder Betriebssystems nutzen können, die von der Klassen-Bibliothek nicht bereitgestellt werden. Dazu zählen der Anschluß an neue Peripheriegeräte oder Steckkarten, der Zugriff auf verschiedene Netztypen oder die Verwendung eines eindeutigen Merkmals Ihres Betriebssystems. Zwei konkrete Beispiele dafür sind die Erfassung von Echtzeitton über ein Mikrophon und die Verwendung von 3D-Beschleunigerhardware in einer 3D-Bibliothek. Diese und ähnliche Fähigkeiten werden derzeit von der Java-Umgebung nicht bereitgestellt (obwohl 3D-Fähigkeiten schon bald hinzukommen sollen). Deshalb müssen sie außerhalb von Java in einer anderen Sprache (meist C oder eine mit C verträgliche Sprache) implementiert werden.
Der zweite, häufig illusorische Grund für die Implementierung von native-Methoden ist die Geschwindigkeit. Ich sage »illusorisch«, weil man mit diesem Ansatz nur selten eine wirkliche Geschwindigkeitssteigerung erzielt. Noch seltener kann die Geschwindigkeit auf andere Art gesteigert werden (wie Sie heute noch sehen werden). Die Verwendung von native-Methoden in diesem Fall bringt immerhin einen Vorteil: Das derzeitige Java-Release führt einige Aufgaben nicht in derselben Weise aus wie ein optimiertes C-Programm. Für diese Aufgaben können Sie den »geschwindigkeitsbedürftigen« Teil in C schreiben (z.B. kritische innere Schleifen) und eine größere Java-Shell von Klassen benutzen, um diesen Trick vor den Benutzern zu verbergen. Die Java-Klassenbibliothek nutzt diesen Ansatz selbst bei bestimmten kritischen Systemklassen, um die Effizienz des Systems insgesamt zu steigern. Wie der Benutzer der Java-Umgebung können auch Sie die diesbezüglichen Nebeneffekte nicht erkennen.
Wenn Sie sich einmal dafür entschieden haben, native-Methoden in Ihrem Programm zu verwenden, kostet Sie diese Entscheidung auch einen Preis. Sie genießen zwar die oben genannten Vorteile, verlieren aber die Portabilität Ihres Java-Codes.
Ohne native-Methoden kann Ihr Programm oder Applet auf jeder Java-Umgebung der Welt jetzt und in alle Ewigkeit laufen. Neue Architekturen oder neue Betriebssysteme können Ihrem Code nichts anhaben. Erforderlich ist dazu nur eine (winzige) virtuelle Java-Maschine (oder ein Browser mit diesen Fähigkeiten), um das Programm irgendwo irgendwann jetzt und immerdar auszuführen.
Sobald Sie eine Bibliothek mit nativem Code erstellt haben, der mit Ihrem Programm verknüpft werden muß, um zu funktionieren, verliert Ihr Programm oder Applet als erstes die Fähigkeit, fröhlich auf allen Plattformen zu wandern. Aus Sicherheitsgründen gibt es (mit gutem Recht) derzeit keinen javafähigen Browser, der nativen Code in einem Applet zuläßt. Das Java-Team hat sich bemüht, soviel wie möglich in die java-Pakete zu packen, weil sie die einzige Umgebung sind, in der ein Applet leben kann. (Die anfänglich ausgelieferten sun-Pakete für einzelne Java-Programme sind nicht alle für Applets verfügbar.)
Der Verlust der Fähigkeit, überall im Internet präsent zu sein und sich auf einer beliebigen Plattform niederzulassen, ist schwerwiegend genug. Es kommt aber noch hinzu, daß Ihr Code, der kein Applet mehr sein darf, von den Maschinen abgängig ist, welche die virtuelle Java-Maschine auf Ihr Betriebssystem portiert haben. (Applets ziehen aus der großen Anzahl von Maschinen und Betriebssystemen automatisch ihre Vorteile, die nun nicht mehr gegeben sind).
Das ist aber immer noch nicht alles: Sie gehen davon aus, daß diese Maschinen und Betriebssysteme Ihre native-Methoden implementieren. Das bedeutet aber meist, daß Sie für einige (oder alle) dieser Maschinen und Betriebssysteme einen anderen Quellcode schreiben müssen. Durch die Verwendung von native-Methoden sind Sie gezwungen, eine getrennte binäre Bibliothek für jede Maschine und jedes Betriebssystem (auf denen Ihr Programm laufen soll) zu produzieren. Und das ist schier endlos.
Die neue JNI (Native Java-Schnittstelle diese wird später in dieser Lektion erläutert) verringert die Beschränkung durch native-Methoden, indem sie es gestattet, nur eine Quellversion des nativen Codes zu erstellen und diese binäre Version des nativen Codes für alle Betriebssysteme wieder zu verwenden. Aber es trifft immer noch zu, daß Sie neue binäre Versionen für jede andere Maschine erstellen müssen und möglicherweise auch eine neue Quelldatei.
Falls Sie nach den obigen Ausführungen immer noch einen nativen Code verwenden möchten oder müssen, gibt es eine Hilfe, die ich Ihnen später in der heutigen Lektion vorstelle. Was aber, wenn Sie glauben, native-Methoden aus Effizienzgründen verwenden zu müssen?
Mit diesem Vorhaben pflegen Sie zumindest die Tradition der Programmierer der gesamten (jungen) Computerära. Sicherlich ist es eine geistige Herausforderung, mit gewissen Einschränkungen zu programmieren. Man ist der Meinung, daß Effizienz immer erforderlich ist, und überlegt sich alle möglichen Arten der Ausführung von Aufgaben auf effizienteste Weise. Ich hatte diesen Anfall von Kreativität, als ich mit dem Programmieren begann, weiß aber heute, daß Kreativität in diesem Bereich falsch am Platz ist.
Bei der Programmierung sollte man alle Energie und Kreativität auf das Design einer straffen, schlanken Serie von Klassen und Methoden richten, die sehr allgemein, abstrakt und wieder verwendbar sind. (Wenn Sie glauben, das sei einfach, sehen Sie sich einmal die vielen Berge schlecht gemachter Software an). Wenn Sie den Großteil Ihres Programmieraufwands auf das Erreichen dieser grundlegenden Ziele richten, sind Sie für die Zukunft gut gerüstet. Eine Zukunft, in der Software nach Bedarf von kleinen Komponenten, die in einem Meer von Netzeinrichtungen schwimmen, zusammengestellt wird. Eine Zukunft, in der jeder in Minuten eine Komponente schreiben kann, die Millionen benutzen (und in ihren Programmen wiederverwenden). Wer statt dessen seine Energie auf die Geschwindigkeit der Software auf einem bestimmten Rechner nach heutigen Kriterien verwendet, kann sicher sein, daß diese Software sehr kurzlebig ist.
Ich will damit nicht behaupten, Effizienz sei nicht wichtig. Vielmehr sollten die obigen Ziele zuerst erreicht werden, dann ergibt sich eine gelungene Software, die ganz natürlich die Struktur des Problems, das sie lösen soll, widerspiegelt. Die Geschwindigkeit kann dann getrost im nächsten Schritt realisiert werden.
Wenn Sie eine solide Grundlage aufgebaut haben, debuggen Sie Ihre Klassen und lassen Ihr Programm oder Applet wunschgemäß ausführen. Erst dann ist es an der Zeit, mit der Optimierung zu beginnen. Handelt es sich um ein Benutzeroberflächen-Applet, müssen Sie eventuell überhaupt nichts tun. Der Benutzer ist im Vergleich zu modernen Rechnern sehr langsam (und wird schätzungsweise alle 18 Monate noch langsamer), deshalb ist Ihr Applet ohnehin schnell genug. Nehmen wir aber für unsere Lernzwecke an, daß dies nicht so ist.
Der nächste Schritt besteht darin, zu prüfen, ob Ihr Release den »Just-in-Time«-Compiler oder einen anderen nativen Code-Übersetzer oder Compiler benutzt. (Sowohl Netscape als auch der Internet Explorer verfügen über integrierte »Just-in-Time«-Compiler.)
Beim »Just-in-Time«-Compiler handelt es sich um eine experimentelle Technologie, die beim Ablaufen der Methoden in der virtuellen Java-Maschine den Bytecode in den nativen Binärcode für den lokalen Rechner übersetzt. Dann bleibt dieser native Code eine Weile als Cache verfügbar. Dieser Trick ist für den Java-Code völlig transparent. Sie müssen beim Schreiben des Codes nichts darüber wissen und nichts berücksichtigen. Auf jedem System, das diese Technologie importiert, läuft Ihr Code aber auch schneller. Erfahrungen mit experimentellen Versionen dieser Technologie haben gezeigt, daß diese Technik nach einem gewissen Anfangsaufwand, wenn eine Methode erstmals ausgeführt wird, die Geschwindigkeit von kompiliertem C-Code erreichen kann.
Ein nativer Code-Übersetzer (oder ein Werkzeug wie java2c, das morgen erläutert wird), übersetzt die Bytecodes einer ganzen .class-Datei (gleichzeitig) in einen portablen C-Quellcode. Diese Version kann dann in einem herkömmlichen C-Compiler kompiliert werden, um eine Art native-Methodenbibliothek zu produzieren.
Dieser große Cache mit nativem Code wird benutzt, wenn die Methoden der betreffenden Klasse aufgerufen werden, aber nur auf dem lokalen Rechner. Ihr ursprünglicher Java-Code kann nach wie vor als Bytecode umherreisen und auf jedem Rechnersystem ausgeführt werden. Unternimmt die virtuelle Java-Maschine diese Schritte automatisch, kann das so transparent sein wie die »Just-in-Time«-Technologie. Erfahrungen mit einer experimentellen Version dieses Werkzeugs haben gezeigt, daß eine voll optimierte C-Leistung erreichbar ist. (Mehr kann keiner erwarten!)
Sie sehen also, daß Ihr Code immer schnell genug ist, ohne weitere Optimierungsschritte zu unternehmen. Da die Welt nach Geschwindigkeit hungert, kann Java nur schneller und die Werkzeuge besser werden. Ihr Code ist das einzig Bleibende in dieser neuen Welt. Deshalb machen Sie das Beste daraus ohne Kompromisse.
Angenommen, diese Technologien stünden nicht zur Verfügung oder Ihr Programm ist nicht nach Ihrem Geschmack optimiert. In diesem Fall sollten Sie zuerst feststellen, welche Methoden die meiste Zeit beanspruchen. Wenn Sie über diese wichtige Information verfügen, können Sie damit beginnen, gezielte Änderungen in Ihren Klassen durchzuführen.
In einem frühen (und wahrscheinlich letzten Release) kann das javaprof-Werkzeug diese Informationen in einem besser lesbaren Format ausgeben. Im Release 1.1 von Java ist javaprof nicht enthalten, während java -prof funktioniert, aber undokumentiert ist. (Dennoch ist java -prof sehr nützlich, Sie sollten nicht zögern, es zu verwenden!) Prüfen Sie die neuen Java-Releases, um zu sehen, ob dort javaprof enthalten ist, und/oder informieren Sie sich im Internet über ähnliche Tools.
Im ersten Schritt ermitteln Sie also, welche Methoden am zeitaufwendigsten sind (meistens sind dies nur einige wenige oder sogar nur eine). Wenn diese Methoden Schleifen enthalten, prüfen Sie die internen Schleifen nach folgenden Kriterien:
Wenn Sie feststellen, daß eine lange Kette von vier oder mehr Methodenaufrufen notwendig ist, um den Code einer Zielmethode zu erreichen, und befindet sich dieser Ausführungspfad in einem kritischen Programmbereich, können Sie direkt in der obersten Methode einen Umweg zu dieser Zielmethode festlegen. Dafür müssen Sie eventuell eine neue Instanzvariable hinzufügen. Meist werden dadurch Schichten- oder Kapselungsbeschränkungen überschritten. Das und mehr ist der Preis, den Sie für die Effizienz zahlen müssen.
Finden Sie Ihren Java-Code nach diesen Tricks immer noch zu langsam, geht an der Verwendung von native-Methoden kein Weg mehr vorbei.
Wir lassen einmal die Gründe beiseite und gehen davon aus, daß Sie wild entschlossen sind, native-Methoden in Ihrem Programm zu verwenden. Sie haben auch schon entschieden, welche Methoden in welchen Klassen native sein müssen. Sie sind also bereit zu beginnen.
Zuerst löschen Sie die Methodenkörper (den gesamten Code zwischen den geschweiften Klammern und die Klammern {} selbst ) jeder Methode, die Sie ausgesucht haben. Dann ersetzen Sie sie durch ein einzelnes Semikolon (;). Anschließend fügen Sie den Modifier native in die vorhandenen Modifier der Methode ein. Als nächstes fügen Sie einen static(class)-Initialisierer für jede Klasse ein, die jetzt native-Methoden enthält, damit die native Codebibliothek, an deren Aufbau Sie gerade arbeiten, geladen wird (Sie können diese Bibliothek mit einem beliebigen Namen versehen). Das war alles!
Mehr ist nicht zu tun, um in Java eine native-Methode anzugeben. Subklassen einer Klasse, die native-Methoden enthält, können nach wie vor überschrieben werden. Die neuen Methoden werden (wie erwartet) für Instanzen der neuen Subklassen aufgerufen.
Die Arbeit in der nativen Sprachumgebung ist allerdings nicht ganz einfach.
Stellen Sie sich eine Version der Java-Umgebung vor, die keine Datei-I/Os vorsieht. In diesem Fall müßte ein Java-Programm, das ein Dateisystem verwendet, zuerst native-Methoden schreiben, um Zugang auf die dafür notwendigen Primitiven des Betriebssystems zu erhalten.
Im folgenden Beispiel werden vereinfachte Versionen der zwei Java-Bibliotheksklassen java.io.File und java.io.RandomAccessFile zu einer neuen Klasse mit dem Namen SimpleFile zusammengefaßt:
public class SimpleFile {
public static final char separatorChar = '>';
protected String path;
protected int fd;
public SimpleFile(String s) {
path = s;
}
public String getFileName() {
int index = path.lastIndexOf(separatorChar);
return (index < 0) ? path : path.substring(index + 1);
}
public String getPath() {
return path;
}
public native boolean open();
public native void close();
public native int read(byte[] buffer, int length);
public native int write(byte[] buffer, int length);
static {
System.loadLibrary("simple"); // wird ausgeführt, wenn die Klasse
// erstmalig geladen wird
}
}
SimpleFile kann von anderen Methoden auf die übliche Weise erstellt und benutzt werden:
SimpleFile f = new SimpleFile(">some>path>and>fileName");
f.open();
f.read(...);
f.write(...);
f.close();
An der SimpleFile-Implementierung fällt auf, daß die ersten zwei Drittel des Codes kaum bemerkenswert sind. Er sieht wie jede andere Klasse aus, mit einer Instanzvariablen, einem Konstruktor und zwei herkömmlichen Methoden. Ferner gibt es vier native-Methoden-Deklarationen. Diese erkennen Sie daran, daß der Codeblock durch ein Semikolon ersetzt und der Modifier native eingefügt wurde. Das sind jene Methoden, die anschließend in einen C-Code implementiert werden müssen.
Am Ende der Klasse befindet sich noch ein etwas mysteriöses Codefragment. Dabei handelt es sich um einen static-Initialisierer. Jeder Code zwischen den Klammern { und } wird nur einmal beim erstmaligen Laden der Klasse ausgeführt. Sie können dies zu Ihrem Vorteil nutzen und dort alle Elemente plazieren, die nur einmal ausgeführt werden sollen etwa das Laden der nativen Codebibliothek, die Sie in der heutigen Lektion noch erstellen. Damit wird das Laden der Klasse selbst mit dem Laden des zugehörigen nativen Codes verbunden. Wenn einer der beiden Vorgänge nicht funktioniert, schlägt auch der andere fehl. Damit ist sichergestellt, daß kein »halbes Setup« einer Klasse entstehen kann.
Um Java-Objekte und Datentypen im C-Code bearbeiten zu können, müssen Sie spezielle .h-Dateien einbinden. Die meisten dieser Dateien befinden sich in Ihrem Release-Verzeichnis im Unterverzeichnis namens include. (Suchen Sie in diesem Verzeichnis nach native.h und allen Header-Dateien, auf die diese verweist).
Einige spezielle Definitionen müssen zugeschnitten werden, um genau zu den Klassenmethoden zu passen. An diesem Punkt kommt das javah-Werkzeug ins Spiel.
Um die Header zu generieren, die Sie für Ihre native-Methoden benötigen, kompilieren Sie zunächst wie gewöhnlich SimpleFile mit javac. Dadurch wird eine Datei namens SimpleFile.class erstellt. Diese Datei muß im javah-Werkzeug bearbeitet werden, wodurch die erforderliche Header-Datei entsteht (SimpleFile.h).
Im Release 1.1 gibt es neue Header-Dateien von Java Native Interface (wird heute zu einem späteren Zeitpunkt beschrieben). Sie sollten grundsätzlich javah -jni anstelle des einfachen javah eingeben. Damit stellen Sie sicher, daß Sie wirklich die neuen Header-Dateien erhalten. Wenn Sie javah ausführen, sollten Sie nur den Klassennamen selbst weitergeben und nicht den kompletten Dateinamen mit der Endung .class.
Die Ausgabe von javah -jni SimpleFile:
/* DO NOT EDIT THIS FILE it is machine generated */
#include <jni.h>
/* Header for class SimpleFile */
#ifndef _Included_SimpleFile
#define _Included_SimpleFile
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: SimpleFile
* Method: open
* Signature: ()Z
*/
JNIEXPORT jboolean JNICALL Java_SimpleFile_open
(JNIEnv *, jobject);
/*
* Class: SimpleFile
* Method: close
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_SimpleFile_close
(JNIEnv *, jobject);
/*
* Class: SimpleFile
* Method: read
* Signature: ([BI)I
*/
JNIEXPORT jint JNICALL Java_SimpleFile_read
(JNIEnv *, jobject, jbyteArray, jint);
/*
* Class: SimpleFile
* Method: write
* Signature: ([BI)I
*/
JNIEXPORT jint JNICALL Java_SimpleFile_write
(JNIEnv *, jobject, jbyteArray, jint);
#ifdef __cplusplus
}
#endif
#endif
Die in diesem Beispiel erzeugten Funktionsdefinitionen stehen in einer Eins-zu-Eins-Korrespondenz mit den native-Methoden Ihrer Klasse, aber jede Funktion verfügt über »Extra«-Argumente, die Sie nicht angegeben haben. Diese speziellen, ursprünglichen Argumente versehen ihre Funktion mit der aktuellen Thread-Umgebung und mit einem Verweis auf die aktuelle Instanz (this in Java). Darüber werden Sie morgen mehr erfahren.
Ein interessanter Nebeneffekt der Generierung von Header-Dateien ist die Ausgabe von Methoden-Descriptoren, informell auch Methodenbeschreibungen genannt. Sie werden in den voranstehenden Kommentaren mit "Signature:" gekennzeichnet. Diese Descriptoren sind ganz nützlich. Sie lassen sich z.B. an spezielle C-Funktionen (näheres hierzu später) weiterleiten, mit deren Hilfe Methoden aus C zurück in die Java-Welt gerufen werden können eventuell um die Implementierung für eine Subklassen-Überschreibung einer der native-Methoden zu bewirken. Sie können javah dazu verwenden, diese Descriptoren genauer kennenzulernen und sich anzusehen, wie sie für verschiedene Methodenargumente und Return-Werte aussehen. Mit diesen Kenntnissen lassen sich die geeigneten Java-Methoden aus dem C-Code heraus aufrufen.
Die Methoden-Descriptoren können Sie auch über javap finden. Angenommen Sie haben folgende Test-Klasse:
class Test {
Thread thread;
int i;
public static void main(String args[]) {}
private void aMethod() {}
}
Nachdem Sie diese in Test.class kompiliert haben, können Sie dessen Feld- und Methoden-Descriptoren durch die Eingabe von javap -p -s Test erhalten:
Compiled from Test.java
class Test extends java.lang.Object
/* ACC_SUPER bit set */
{
thread Ljava/lang/Thread;
i I
public static main ([Ljava/lang/String;)V
private aMethod ()V
<init> ()V
}
Ein Methoden-Descriptor enthält die Form (...)X, wobei das X für einen Buchstaben (oder eine Zeichenfolge) steht, die den Return-Typ darstellt und ... eine Zeichenfolge enthält, die wiederum die Argumenttypen darstellt. Der verwendete Buchstabe (oder die Zeichenfolge) und die Typen, die sie darstellen, sind in den folgenden Beispielen enthalten: [T ist ein Array des Typs T, Lmy/package/name/ClassName; ist ein Objektbezug des Typs my.package.name.ClassName, B ist Byte, I ist int, V ist void und Z ist boolesch.
Die Methode close(), die keine Argumente verwendet und void zurückgibt, wird deshalb von der Zeichenkette ()V repräsentiert, während deren Gegenstück open(), das einen booleschen Wert zurückgibt, durch ()Z dargestellt ist. read(), das ein Array von Bytes enthält sowie ein int als die beiden Argumente und ein int zurückgibt, ist ([BI)I. Und schließlich main(), das ein Array von java.lang.Strings enthält und void zurückgibt, ist ([Ljava/lang/String;)V. (Weitere Informationen finden Sie im Abschnitt »Methoden-Descriptor« in der morgigen Lektion).
Jetzt können wir endlich den C-Code für die native-Methoden von Java schreiben.
Die von javah erzeugte Header-Datei SimpleFile.h gibt Ihnen die Prototypen der vier C-Funktionen, die Sie benötigen, um Ihren nativen Code zu vervollständigen. Dann schreiben Sie einen C-Code, der die nativen Fähigkeiten für Ihre Java-Klasse bereitstellt (in diesem Fall werden einige Datei-I/O-Routinen benötigt). Schließlich stellen Sie den gesamten C-Code in eine neue Datei und nennen diese SimpleFileNative.c. Im folgenden finden Sie das Ergebnis:
#include "SimpleFile.h" /* for jni definitions, and our prototypes */
#include <sys/param.h> /* for MAXPATHLEN */
#include <fcntl.h> /* for O_RDWR and O_CREAT */
#define LOCAL_PATH_SEPARATOR '/' /* for UNIX */
static void fixSeparators(char *p, JNIEnv *e, jclass jc) {
jfieldID jsepChF = (*e)->GetStaticFieldID(e, jc, "separatorChar", "C");
jchar jsepCh = (*e)->GetStaticCharField(e, jc, jsepChF); /* errs? */
for (; *p != '\0'; ++p)
if (*p == (char) jsepCh) /* casts from Java (UTF) to C char */
*p = LOCAL_PATH_SEPARATOR;
}
JNIEXPORT jboolean JNICALL Java_SimpleFile_open(JNIEnv *e, jobject this) {
jclass jc = (*e)->GetObjectClass(e, this);
jfieldID jpathF = (*e)->GetFieldID(e, jc, "path", "S");
jobject jpath = (*e)->GetObjectField(e, this, jpathF);
const jbyte *jbuffer = (*e)->GetStringUTFChars(e, (jstring) jpath, NULL);
char buffer[MAXPATHLEN]; /* should test 1st if it ^ IS String */
int fd;
strncpy(buffer, (const char *) jbuffer, MAXPATHLEN); /* now modifiable */
(*e)->ReleaseStringUTFChars(e, (jstring) jpath, jbuffer); /* important */
fixSeparators(buffer, e, jc);
if ((fd = open(buffer, O_RDWR | O_CREAT, 0664)) < 0) /* UNIX open */
return(JNI_FALSE); /* or, JNI's Throw() could throw an exception */
(*e)->SetIntField(e, this, (*e)->GetFieldID(e, jc, "fd", "I"), (jint) fd);
return(JNI_TRUE); /* above should be checking for NULL fieldIDs */
}
JNIEXPORT void JNICALL Java_SimpleFile_close(JNIEnv *e, jobject this) {
jclass jc = (*e)->GetObjectClass(e, this);
jfieldID jfdF = (*e)->GetFieldID(e, jc, "fd", "I");
close((int) (*e)->GetIntField(e, this, jfdF));
(*e)->SetIntField(e, this, jfdF, (jint) -1);
}
static jint doIO(JNIEnv *e, jobject this, jbyteArray jarray, jint jcount,
jboolean doRead) {
jclass jc = (*e)->GetObjectClass(e, this);
jfieldID jfdF = (*e)->GetFieldID(e, jc, "fd", "I");
jint jfd = (*e)->GetIntField(e, this, jfdF);
jbyte *jdata = (*e)->GetByteArrayElements(e, jarray, NULL);
jsize jlen = (*e)->GetArrayLength(e, jarray);
int numBytes = ((int) jlen < (int) jcount ? (int) jlen : (int) jcount);
if (doRead == JNI_TRUE)
if ((numBytes = read((int) jfd, (char *) jdata, numBytes)) == 0)
numBytes = -1; /* return a special error code or... */
else
numBytes = write((int) jfd, (char *) jdata, numBytes);
(*e)->ReleaseByteArrayElements(e, jarray, jdata, 0); /* writes it back */
return((jint) numBytes); /* the number of bytes actually read/written */
}
JNIEXPORT jint JNICALL Java_SimpleFile_read(JNIEnv *e, jobject this,
jbyteArray jarray, jint jcount) {
return(doIO(e, this, jarray, jcount, JNI_TRUE));
}
JNIEXPORT jint JNICALL Java_SimpleFile_write(JNIEnv *e, jobject this,
jbyteArray jarray, jint jcount) {
return(doIO(e, this, jarray, jcount, JNI_FALSE));
}
Wenn Sie mit dem Schreiben der .c-Datei fertig sind, kompilieren Sie sie in Ihrem lokalen C-Compiler (meist cc oder gcc), um eine Objekt-Datei (SimpleFile.o auf UNIX) zu erstellen. Auf manchen Systemen sind spezielle Kompilierflaggen erforderlich.
Sie haben nun den gesamten Code fertiggestellt, der geschrieben (und kompiliert) werden muß, um eine ladbare native Bibliothek zu erstellen.
Sie sind nun in der Lage, alles zusammenzustellen und die native Bibliothek namens simple zu erstellen, die sich in SimpleFile.java befinden soll, wie zu Beginn dieser Lektion erwähnt.
Beginnen Sie jetzt mit der Zusammenstellung der Arbeit in einer einzigen Bibliotheksdatei. Diese unterscheidet sich je nach dem System, auf dem Java ausgeführt wird. Das folgende Beispiel ist in der UNIX-Syntax abgefaßt und soll das Grundkonzept darstellen.
cc -G SimpleFile.o -o libsimple.so
Die -G Flagge teilt dem Linker mit, daß Sie eine dynamisch verknüpfbare Bibliothek erstellen. Die Einzelheiten unterscheiden sich je nach System.
Wenn die Java-Klasse SimpleFile erstmals in Ihrem Programm geladen wird, versucht die System-Klasse, die Bibliothek namens simple zu laden, die Sie soeben erstellt haben. Sehen Sie sich den Java-Code von SimpleFile noch einmal an.
Wie positioniert er die Datei? Er ruft den dynamischen Linker auf, der sich durch eine Umgebungsvariable namens LD_LIBRARY_PATH informiert. Hier erfährt er, welche Folge von Verzeichnissen zu durchsuchen ist, wenn neue Bibliotheken mit native-Code geladen werden. Da das aktuelle Verzeichnis in Java standardmäßig der Ladepfad ist, kann libsimple.so im aktuellen Verzeichnis bleiben. Sie wird dort gut funktionieren.
Die JNI (Java Native Interface) ist eine spezielle, nicht Java-spezifische API auf unterer Ebene, welche die Schnittstelle zwischen der virtuellen Maschine und der Welt außerhalb Java definiert. native-Methoden können diese Schnittstelle verwenden, um einen Satz hilfreicher Funktionen aufzurufen oder mit der Java-Welt zu interagieren, während Nicht-Java-Programme einen untergeordneten Satz davon benutzen können, um eine virtuelle Java-Maschine zu erzeugen, eine Verbindung zu dieser herzustellen und mit der Java-Welt in Interaktion zu treten.
Bei der Erstellung von JNI wurden alle Lizenznehmer von SUN (Java-Partner) miteinbezogen, von denen viele über eigene Implementierungen für die virtuelle Maschine verfügen. Zwei wichtige Ergebnisse des JNI-Standards sind: Programmierer können darauf zählen, daß sich jede beliebige virtuelle Maschine von außerhalb in standardisierter Form betreiben läßt, und sie können sich darauf verlassen, daß nur eine einzige API in allen virtuellen Maschinen ausgeführt wird. Die letzte Eigenschaft garantiert, daß binärer nativer Code für eine virtuelle Maschine auf einem bestimmten Hardewaretyp auch für alle anderen virtuellen Maschinen dieses Hardwaretyps verwendet werden kann. Dadurch wird eine weitere Quelle für Inkompatibilität ausgeschaltet (allerdings nur für Nicht-Java-Codes).
Die JNI erlegt den Entwicklern von Implementierungen für virtuelle Maschinen keine neue Last auf, obwohl die Benutzer eine geringfügige Einbuße hinnehmen müssen. Diese kleine Ineffizienz ist im Vergleich zu den Vorteilen aber unerheblich, denn wenn Sie einmal eine virtuelle Maschine kennengelernt haben, können Sie diese Kenntnisse überall einsetzen (z.B. bei Werkzeug-Entwicklungen auf unterer Ebene).
Von allen Beiträgen zur JNI hatte die ehemalige JRI von Netscape (Java Runtime Interface) den größten Einfluß. Wenn Sie JRI bereits kennen, sollten Sie mit JNI bald vertraut sein. Die JNI ist nicht binär kompatibel mit der JRI, aber eine virtuelle Maschine kann sowohl die JRI und die JNI unterstützen.
Wenn Sie mit JNI programmieren, können die native-Methoden
Sie können auch einen untergeordneten Satz von JNI (wird später in dieser Lektion erläutert) verwenden, um eine willkürliche native Anwendung laden zu lassen und ihr den Zugriff auf die virtuelle Java-Maschine zu ermöglichen. Damit ist es Programmierern möglich, ihre bereits vorhandenen, nicht in Java erstellten Anwendungen für Java zu präparieren, ohne dazu eine statische Verbindung zum Quellcode der virtuellen Maschine herstellen zu müssen.
In der Vergangenheit erforderten die verschiedenen Implementierungen für virtuelle Maschinen auch verschiedene native Methoden-Schnittstellen. Diese verschiedenen Schnittstellen zwangen die Programmierer dazu, mehrere Versionen der nativen Methoden-Bibliotheken zu erstellen, zu verwalten und zu vertreiben. Es mußte ein Standard geschaffen werden.
Hinzu kam, daß viele dieser älteren Schnittstellen auf dem Stil der nativen Methoden-Schnittstellen von JDK 1.0 basierten, der zwei wesentliche Mängel aufwies. Erstens, nativer Code griff auf die Felder in Java-Objekten direkt über die Elemente von C struct zu. Aber die Spezifikationen der virtuellen Java-Maschine definierten nicht, wie Objekte im Arbeitsspeicher ausgelegt sind, weshalb eine virtuelle Maschine jedes beliebige Format auswählen konnte, um die Objektmitglieder nicht aneinander grenzen zu lassen oder mit der Zeit zwischen ihnen verschiedene Abstände einzurichten. Letzteres würde vom Programmierer erfordern, zumindest die struct-Referenzen jedesmal neu zu kompilieren, sobald die Objektmitglieder von der virtuellen Maschine neu angeordnet worden sind (lächerlich).
Zweitens, die native Methoden-Schnittstelle 1.0 war von ihrem konservativen Garbage-Collector abhängig die unbeschränkte Verwendung des alten Makros unhand() zwang den Garbage-Collector den nativen Stack zu scannen. Diese unkontrollierte Mischung aus nativen und nichtnativen Referenzen erschwerten andere Aufgabendurchführungen des Garbage-Collector bzw. machten diese unmöglich aber jede Form von Garbage Collection mußte erlaubt sein (Spezifikationen für die virtuelle Java-Maschine).
Trotz der Notwendigkeit für einen Standard wird JNI nicht unbedingt die einzige native Methoden-Schnittstelle sein, die von einer bestimmten virtuellen Maschine unterstützt wird. Aus Effizienzgründen (oder um Zugriff auf einige besondere Fähigkeiten zu erhalten) muß der Programmierer eventuell eine maschinenspezifische Schnittstelle der unteren Ebene verwenden. Für das Erstellen von Software-Komponenten kann auch eine Schnittstelle auf höherer Ebene erforderlich sein. Je mehr sich die Java-Umgebung und die Komponenten-Software-Technologie entwickelt, desto eher werden die nativen Methoden ihre Bedeutung verlieren, und die JNI wird zu Gunsten dieser Ansichten auf höherer Ebene verschwinden.
Die Funktionen in JNI sind durch einen Schnittstellen-Pointer zugänglich. Dieser Pointer verweist auf eine JNI-Datenstruktur pro Thread, dessen erstes Element ein weiterer Pointer sein muß. Dieser neue Pointer verweist auf ein Array von Funktions-Pointern, von denen wiederum jeder auf eine bestimmte JNI-Funktion verweist. Jede JNI-Funktion befindet sich an einer vordefinierten Position in diesem Array von Funktions-Pointern.
Der JNI-Schnittstellen-Pointer ist nur im aktuellen Thread gültig. Eine native Methode sollte deshalb den Schnittstellen-Pointer nicht zu einem anderen Thread überschreiten. (Virtuelle Maschinen können andere, thread-spezifische Daten in jener Thread-Struktur ermitteln und speichern, auf die vom JNI-Schnittstellen-Pointer verwiesen wird.)
Alle native-Methoden erhalten den JNI-Schnittstellen-Pointer als deren erstes Argument. Die virtuelle Maschine reicht denselben Schnittstellen-Pointer in Mehrfachaufrufen an eine native-Methode aus demselben Java-Thread weiter, aber native-Methoden können von verschiedenen Threads aufgerufen werden, und dadurch können verschiedene JNI-Schnittstellen-Pointer eintreffen.
Die JNI führt eine Reihe von Schritten durch, um den Nicht-Java-Namen für eine Java-native-Methode vorzubereiten. Einige dieser Schritte beinhalten die Entstellung von Namen. Dabei wird ein Unterstrich-Präfix durch spezielle Zeichen im Namen ersetzt (nähere Informationen hierzu erhalten Sie später). Der Nicht-Java-Name ist in folgende Komponenten unterteilt:
Ein einfaches Zerlegungsschema für Namen stellt sicher, daß Unicode-Zeichen (und der Seperator "/") in gültige Nicht-Java-Funktionsnamen übersetzt werden. Zunächst ersetzt der Unterstrich (»_«) den Schrägstrich (»/«) in komplett qualifizierten Klassennamen. Da ein Name oder ein Typ-Descriptor niemals mit einer Zahl beginnt, kann für die Escape-Sequenzen _0,..._9 verwendet werden:
Escape: Denotes:
_0XXXX a Unicode character XXXX.
_1 the character "_"
_2 the character ";" in descriptors
_3 the character "[" in descriptors
Wir haben schon festgestellt, daß der JNI-Schnittstellen-Pointer (Typ JNIEnv *) immer das erste Argument einer native-Methode ist. Das zweite Argument ist grundsätzlich ein Objektbezug, aber worum es sich dabei handelt, unterscheidet sich je nach Instanz- und native-Klassenmethode. Bei Instanzmethoden ist es ein Bezug zur Instanz selbst (this). Bei Klassenmethoden ist es ein Bezug zur Klasse der Instanz.
Die übrigen Argumente entsprechen ihren regulären Java-Gegenstücken, verwenden aber spezielle HNI-Typen anstelle der einzelnen Java-Typen. Im folgenden finden Sie ein einfaches Beispiel für eine native-Methode namens test():
package packageName;
class ClassName {
native double test(int i, String s);
. . .
}
Und hier ist die C-Funktion (Nicht-Java) mit der längeren Version des entstellten Namens, der test() implementiert:
jdouble Java_packageName_ClassName_test__ILjava_lang_String_2 (
JNIEnv *e, /* interface pointer */
jobject this, /* instance pointer */
jint ji, /* Java argument #1 */
jstring js) /* Java argument #2 */ {
const jbyte *s = (*e)->GetStringUTFChars(e, js, NULL);
. . . /* do something useful with s (and ji) */
(*e)->ReleaseStringUTFChars(e, js, s);
return ...;
}
In diesem Beispiel gibt es zwei Aufrufe für JNI-Funktionen. Das Präfix ("(*e)->"), das jedem Aufruf voransteht, ist ein notwendiger Umweg, um die bereits erwähnten Funktions-Pointer-Arrays zu unterstützen.
Primitive Typen (wie int) werden einfach zwischen Java- und Nicht-Java-Code kopiert. Aber Java-Objekte werden andererseits durch Bezüge weitergeleitet. Die virtuelle Maschine muß alle Objekte verfolgen, die durch Ihren Nicht-Java-Code weitergeleitet wurden, damit sie nicht vom Garbage-Collector freigesetzt werden, ehe Sie mit ihnen fertig sind. Der Nicht-Java-Code wiederum muß eine Möglichkeit besitzen, diese Objekte freizugeben, wenn Sie damit fertig sind.
Objektbezüge in Nicht-Java-Codes sind entweder lokal oder global. Lokale Bezüge existieren ausschließlich während eines Funktionsaufrufs von Nicht-Java-Code und werden automatisch freigesetzt, sobald sie zurückgegeben werden. Globale Bezüge bleiben so lange bestehen, bis sie explizit freigesetzt werden. Alle Objekte, die als Argumente an Nicht-Java-Funktionen weitergeleitet werden, und alle Objekte, die von JNI-Funktionen zurückgegeben werden, sind lokale Bezüge. Sie können diese bei Bedarf in globale Bezüge konvertieren (durch eine JNI-Funktion). JNI-Funktionen akzeptieren sowohl globale als auch lokale Referenzen als Objekt-Argumente und ihre Nicht-Java-Funktionen können beide als deren Ergebnis zurückgeben.
Um sicherzustellen, daß Sie lokale Bezüge jederzeit manuell freisetzen können, ist es JNI-Funktionen nicht möglich, eigene lokale Bezüge zu erstellen, mit Ausnahme von Bezügen, die diese als deren Ergebnis zurückgeben. Lokale Bezüge sind ferner nur im aktuellen Thread gültig, weshalb sie nicht an andere Threads weitergeleitet werden sollten.
Die JNI definiert spezielle Zugangsfunktionen zu diesen lokalen und globalen Objektbezügen, mit denen Sie deren Inhalte prüfen und ändern können. Das heißt, unabhängig davon, welche lokale virtuelle Maschine die Java-Objekte intern darstellt, funktioniert dieselbe Implementierung für Nicht-Java-Funktionen (wesentlich für eine breite JNI-Unterstützung).
Bei der Verwendung von Zugangsfunktionen kann es zu einem geringfügigen Mehraufwand kommen. Aber in den meisten Fällen werden Sie native-Methoden einbinden, um eher ungewöhnliche Aufgaben durchzuführen, bei denen dieser zusätzliche Aufwand also nicht ins Gewicht fällt. Bei Arrayzugriffen ist dies jedoch nicht der Fall. In diesem speziellen Fall sieht JNI zwei Lösungen vor: Kleine Subarrays können von einem einzigen JNI-Funktionsaufruf kopiert werden, und große Arrays lassen sich vorübergehend in den Arbeitsspeicher kopieren, während Sie diese aktualisieren.
Um auf Felder und Methoden in Java-Objekten zuzugreifen, sieht JNI einen Vorgang aus zwei Schritten vor, der dazu dient, die Ineffizienz von Feld- und Methoden-Ermittlungen herauszufiltern. Zunächst suchen Sie ein Feld oder eine Methode anhand deren Namen und Descriptor:
jmethodID jtestF = (*e)->GetMethodID(jc, "test", "(ILjava/lang/String;)D");
Angenommen, jc bezieht sich auf die Klasse ClassName aus dem vorherigen Beispiel. Damit erhielten wir die Methoden-ID der hier definierten Java-Methode test(). Ihr Nicht-Java-Code kann diese Methoden-ID dann immer wieder verwenden, ohne jedesmal den Aufwand einer Methodenermittlung tragen zu müssen:
jdouble jresult = (*e)->CallDoubleMethod(jobj, jtestF, 10, jstr);
Nicht-Java-Code kann mit JNI zu Java-Ausnahmen führen und herausragende Java-Ausnahmen damit behandeln alle nicht behandelten Ausnahmen werden an die Java-Welt zurückgeschickt. Einige JNI-Funktionen »verwerfen« Java-Ausnahmen aber dies ist kein idealer Weg, um mit illegalen Argumenten und ähnlichen in Nicht-Java-Code umzugehen. Durch das Ignorieren von Java-Ausnahmen wird ein ungültiger Systemstatus für den nächsten JNI-Aufruf erzeugt, aber gelegentlich ist es in effizientem Nicht-Java-Code notwendig, einige Fehlerprüfungen zu ignorieren (insbesondere da diese Prüfungen häufig bereits vorgenommen wurden oder von den Java-Code umgebenden native-Methoden behandelt werden).
Es gibt deshalb einen kleinen Satz von Ausnahmen, die eine konforme JNI-Implementierung für bestimmte Fehler hervorbringen müssen. Der Rest von JNI verwendet ausgegebene Fehlercodes (und einige wenige andere Java-Ausnahmen), um über Fehler zu berichten. Strengere JNI-Implementierungen sind erlaubt, aber diese zusätzlichen Fehler sollten die korrekten Java-Programme nicht betreffen (die bereits in Java geprüft wurden).
In Nicht-Java-Code ersetzen spezielle JNI-Typen die herkömmlichen Java-Typen. Für primitive Typen ist die Kennzeichnung einfach: Der Typ <type> wird zum Typ j<type> (für <type>: boolean, byte, char, short, int, long und double); void ist in beiden Welten identisch. Diese <type>-Abkürzung wird von jetzt an häufig verwendet.
Aus Bequemlichkeitsgründen wurden folgende spezielle boolesche Werte definiert: JNI_FALSE (0) und JNI_TRUE (1). Der JNI-Typ jsize, ein nicht gekennzeichneter Ganzzahltyp, kann jeden Index im Adressenraum enthalten (d.h. in der Mindestgröße eines Nicht-Java-Pointer).
Der JNI-Bezugstyp (für Objekte) heißt jobject, aber er hat spezielle Untertypen für Klassen, Zeichenketten und Arrays: jclass, jstring, jarray. Arrays verfügen über spezielle Sub-Sub-Typen für jeden primitiven Typ eines Arrays: j<type>Array plus einen jobjectArray-Typ für Objekt-Arrays.
IDs von Feldern (jfieldID) und Methoden (jmethodID) sind in der Regel Nicht-Java-Pointertypen. Ein spezieller Typ, jvalue, wird dazu verwendet, für ein beliebiges Objekt oder einen primitiven Typ zu stehen, er ist die Vereinigung aller Nicht-Java-Typen (j<type>) und Objekte (jobject).
JNI-UT-8-Unicode-Zeichenketten sind mit dem »Standard«-Format identisch, ausgenommen daß der Null-Byte in eine Zwei-Byte-Form codiert ist (damit wird eine C-ähnliche Null-Termination von Zeichenfolgen in der Nicht-Java-Welt möglich) und nur die 1-, 2- und 3-Byte-Formate unterstützt werden.
Jeder Funktion ist durch das JNIEnv-Argument an einer bestimmten Position zugänglich. Der JNIEnv-Typ ist ein Pointer zu einer Struktur, die alle JNI-Funktions-Pointer speichert. In C ist dies wie folgt definiert:
typedef const struct JNINativeInterface *JNIEnv;
und die Tabelle der Funktions-Pointer ist wie folgt definiert:
const struct JNINativeInterface ... = {
NULL, NULL, NULL, NULL,
GetVersion, DefineClass, FindClass,
NULL, NULL, NULL,
GetSuperclass, IsAssignableFrom,
NULL,
Throw, ThrowNew,
ExceptionOccurred, ExceptionDescribe, ExceptionClear,
FatalError,
NULL, NULL,
NewGlobalRef, DeleteGlobalRef, DeleteLocalRef,
IsSameObject,
NULL, NULL,
AllocObject,
NewObject, NewObjectV, NewObjectA,
GetObjectClass, IsInstanceOf,
GetMethodID,
CallObjectMethod, CallObjectMethodV, CallObjectMethodA,
CallBooleanMethod, CallBooleanMethodV, CallBooleanMethodA,
. . .
CallDoubleMethod, CallDoubleMethodV, CallDoubleMethodA,
CallVoidMethod, CallVoidMethodV, CallVoidMethodA,
CallNonvirtualObjectMethod, CallNonvirtualObjectMethodV, CallN...MethodA,
CallNonvirtualBooleanMethod, CallNonvirtualBooleanMethodV, CallN...nMethodA,
. . .
CallNonvirtualDoubleMethod, CallNonvirtualDoubleMethodV, CallN...MethodA,
CallNonvirtualVoidMethod, CallNonvirtualVoidMethodV, CallN...thodA,
GetFieldID,
GetObjectField, GetBooleanField, . . ., GetDoubleField,
SetObjectField, SetBooleanField, . . ., SetDoubleField,
GetStaticMethodID,
CallStaticObjectMethod, CallStaticObjectMethodV, CallStaticObjectMethodA,
CallStaticBooleanMethod, CallStaticBooleanMethodV, CallStaticBooleanMethodA,
. . .
CallStaticDoubleMethod, CallStaticDoubleMethodV, CallStaticDoubleMethodA,
CallStaticVoidMethod, CallStaticVoidMethodV, CallStaticVoidMethodA,
GetStaticFieldID,
GetStaticObjectField, GetStaticBooleanField, . . ., GetStaticDoubleField,
SetStaticObjectField, SetStaticBooleanField, . . ., SetStaticDoubleField,
NewString, GetStringLength, GetStringChars, ReleaseStringChars,
NewStringUTF, GetStringUTFLength, GetStringUTFChars, ReleaseStringUTFChars,
GetArrayLength,
NewObjectArray,
GetObjectArrayElement,
SetObjectArrayElement,
NewBooleanArray, . . ., NewDoubleArray,
GetBooleanArrayElements, . . ., GetDoubleArrayElements,
ReleaseBooleanArrayElements, . . ., ReleaseDoubleArrayElements,
GetBooleanArrayRegion, . . ., GetDoubleArrayRegion,
SetBooleanArrayRegion, . . ., SetDoubleArrayRegion,
RegisterNatives, UnregisterNatives,
MonitorEnter, MonitorExit,
GetJavaVM,
};
Ferner sind die zusätzlichen NULL-Einträge am Anfang der Funktionstabelle reserviert, damit z.B. eine künftige klassenbezogene JNI-Operation nach FindClass hinzugefügt werden kann und nicht an das Ende der Tabelle gesetzt werden muß.
In diesem Abschnitt finden Sie eine Reihe von Konventionen für Namen, die Argumente, Return-Werte usw. beschreiben. Neben den bereits erläuterten JNI-Typen finden Sie hier die neuen Konventionen:
jint GetVersion(JNIEnv *e);
Gibt die Version von JNI aus, die größere Version in den höheren 16 Bit, die kleinere in den unteren 16 Bit. IN JDK 1.1 gibt GetVersion() folgendes zurück: 0x00010001.
jclass DefineClass(JNIEnv *e, jobject loader, const char *buf, jsize bufLen);
Definiert eine Klasse aus einem Puffer roher Klassendaten. loader ist ein Klassen-Loader, der einer definierten Klasse zugewiesen ist. Gibt ein Java-Klassenobjekt aus oder NULL, wenn ein Fehler auftritt. Verwirft ClassFormatError, wenn die Klassendaten keine gültige Klasse angeben.
jclass FindClass(JNIEnv *e, const char *name);
Die Funktion lädt eine lokal definierte Klasse. Sie sucht die Verzeichnisse und Zip-Dateien, die von der Umgebungsvariablen CLASSPATH für die Klasse mit dem angegebenen name (ein Paketname, beschränkt von "/", gefolgt vom Klassennamen) angegeben wird. Wenn name mit »[« beginnt (dem Array-Descriptor-Zeichen), wird eine Array-Klasse ausgegeben. Wenn die Klasse nicht gefunden wird, wird ein Klassenobjekt oder NULL ausgegeben.
jclass GetSuperclass(JNIEnv *e, jclass clazz);
Wenn clazz eine beliebige andere Klasse als die Klasse java.lang.Object darstellt, dann gibt diese Funktion das Objekt zurück, welches die Superklasse dieser Klasse darstellt; andernfalls wird NULL ausgegeben.
jboolean IsAssignableFrom(JNIEnv *e, jclass clazz1, jclass clazz2);
Bestimmt, ob ein Objekt von clazz1 sicher in clazz2 übertragen werden kann. Gibt JNI_TRUE zurück, wenn folgendes wahr ist: Beide Argumente beziehen sich auf dieselbe Klasse, die erste ist ein Subklasse der zweiten, oder die erste verwendet die zweite als eine ihrer Schnittstellen.
jint Throw(JNIEnv *e, jobject obj);
Bewirkt, daß ein java.lang.Throwable-Objekt (obj) verworfen wird. Gibt bei Erfolg Null aus und bei Fehlschlage eine negative Zahl.
jint ThrowNew(JNIEnv *e, jclass clazz, const char *message);
Konstruiert ein Ausnahme-Objekt aus der angegebenen clazz mit der Meldung, die von message definiert wird, und bewirkt das Verwerfen der Ausnahme. Gibt bei Erfolg Null aus und bei Fehlschlag eine negative Zahl.
jobject ExceptionOccurred(JNIEnv *e);
Bestimmt, ob eine Ausnahme verworfen wird. Die Ausnahme bleibt verworfen, bis entweder ExceptionClear() aufgerufen wird oder der Java-Code die Ausnahme behandelt. Gibt das Ausnahme-Objekt aus, das aktuell verworfen wird oder NULL, wenn keine Ausnahme aktuell verworfen wird.
void ExceptionDescribe(JNIEnv *e);
Gibt eine Ausnahme aus und eine Rückverfolgung bis zu einem Fehler berichtenden Systemkanal wie stderr. Dies ist eine hilfreiche Routine, die für das Debuggen vorgesehen ist.
void ExceptionClear(JNIEnv *e);
Löscht jene Ausnahme, die aktuell verworfen wird. Wenn aktuell keine Ausnahme verworfen wird, hat diese Routine keine Auswirkungen.
void FatalError(JNIEnv *e, char * msg);
Übermittelt einen schweren Fehler, bei dem die virtuelle Maschine nicht mehr eingreifen kann. Diese Funktion wird nicht zurückgegeben.
jobject NewGlobalRef(JNIEnv *e, jobject obj);
Erstellt einen neuen globalen Bezug zu obj (bei dem es sich um eine globale oder lokale Referenz handeln kann). Globale Bezüge müssen explizit eingerichtet werden, indem DeleteGlobalRef() aufgerufen wird. Gibt einen globalen Bezug aus oder NULL, wenn das System den Arbeitsplatzspeicher übersteigt.
void DeleteGlobalRef(JNIEnv *e, jobject *globalRef);
Löscht den globalen Bezug (globalRef).
void DeleteLocalRef(JNIEnv *e, jobject *localRef);
Löscht den lokalen Bezug (localRef).
jobject AllocObject(JNIEnv *e, jclass clazz);
Ordnet eine neue Java-Funktion zu, ohne einen Konstruktor für das Objekt aufzurufen. Gibt einen Bezug zum Objekt aus. Das clazz-Argument muß sich nicht auf eine Array-Klasse beziehen. Gibt ein Java-Objekt aus oder NULL, wenn das Objekt nicht konstruiert werden kann. Verwirft InstantiationException, wenn die Klasse eine Schnittstelle oder eine abstrakte Klasse ist, oder OutOfMemoryError, wenn das System den Speicherplatz übersteigt.
jobject NewObject(JNIEnv *e, jclass clazz, jmethodID methodID, ...);
jobject NewObjectA(JNIEnv *e, jclass clazz, jmethodID methodID, jvalue *args);
jobject NewObjectV(JNIEnv *e, jclass clazz, jmethodID methodID, va_list args);
Konstruiert ein neues Java-Objekt. Der Methoden-ID zeigt an, welche Konstruktor-Methode aufzurufen ist. Diese ID muß beibehalten werden, wenn GetMethod-ID() mit <init> als Methodenname und void (V) als Return-Typ aufgerufen wird.
Diese Funktionen und viele der folgenden Funktionen weisen drei Funktionen auf, wenn sie eine Argumentenliste verschiedener Größe erhalten:
Von nun an wird dieser Sachverhalt als »herkömmliche Argument-Konvention« bezeichnet.
NewObject<X>() akzeptiert diese Argumente und gibt sie wiederum an jene Java-Methode weiter, die der Programmierer aufrufen möchte. Gibt ein Java-Objekt zurück oder NULL, wenn das Objekt nicht konstruiert werden kann. Verwirft InstantiationException, wenn die Klasse eine Schnittstelle oder eine abstrakte Klasse ist, oder OutOfMemoryError, wenn das System den Speicherplatz übersteigt.
jclass GetObjectClass(JNIEnv *e, jobject obj);
Gibt die Java-Klasse von obj zurück.
jboolean IsInstanceOf(JNIEnv *e, jobject obj, jclass clazz);
Testet, ob obj eine Instanz von clazz ist. Gibt JNI_TRUE zurück, wenn obj in clazz unterzubringen ist, andernfalls wird JNI_FALSE zurückgegeben. Ein NULL-Objekt läßt sich in jeder Klasse unterbringen.
jboolean IsSameObject(JNIEnv *e, jobject ref1, jobject ref2);
Testet, ob sich zwei Bezüge auf dasselbe Java-Objekt beziehen. Gibt JNI_TRUE zurück, wenn ref1 und ref2 einen Bezug zum selben Java-Objekt beinhalten oder beide NULL sind; andernfalls wird JNI_FALSE zurückgegeben.
jfieldID GetFieldID(JNIEnv *e, jclass clazz, const char *name, const char *desc);
Gibt den Feld-ID für ein Instanzfeld (nicht-static) einer Klasse zurück. Das Feld wird von dessen Namen und Deskriptor angegeben. name und desc sind 0-terminierte UTF-8-Zeichenketten. Gibt eine Feld-ID zurück oder Null, wenn das angegebene Nicht-static-Feld nicht gefunden werden kann. Verwirft NoSuchFieldError, wenn das angegebene Nicht-static-Feld nicht gefunden werden kann, ExceptionInInitializerError, wenn der Klassen-Initialisierer aufgrund einer Ausnahme fehlschlägt, oder OutOfMemoryError, wenn das System den Speicherplatz übersteigt.
j<type> Get<Type>Field(JNIEnv *e, jobject obj, jfieldID fieldID);
void Set<Type>Field(JNIEnv *e, jobject obj, jfieldID fieldID, j<type> value);
Diese Familie von Zugangsroutinen gibt den Wert eines Instanzfeldes (nicht-static) eines Objekts an. <type> ist, wie bereits definiert, für primitive Typen oder object. Der erste Buchstabe von <Type> wird groß geschrieben.
jmethodID GetMethodID(JNIEnv *e, jclass clazz, const char *name, const char *desc);
Gibt die Methoden-ID für eine Instanzmethode (nicht-static) einer Klasse oder Schnittstelle zurück. Die Methode kann in einer der Superklassen von clazz definiert sein und von clazz erben. Die Methode wird von ihrem Namen und Descriptor bestimmt. name und desc sind 0-terminierte UTF-8-Zeichenfolgen. Gibt einen Methoden-ID zurück oder NULL, wenn die Operation fehlschlägt. Verwirft NoSuchMethodError, wenn die angegebene Nicht-static-Methode nicht gefunden werden kann, ExceptionInInitializerError, wenn der Klassen-Initialisierer aufgrund einer Ausnahme fehlschlägt oder OutOfMemoryError, wenn das System den Arbeitsspeicher übersteigt.
j<type> Call<Type>Method(JNIEnv *e, jobject obj, jmethodID methodID, ...);
j<type> Call<Type>MethodA(JNIEnv *e, jobject obj, jmethodID methodID, jvalue *args);
j<type> Call<Type>MethodV(JNIEnv *e, jobject obj, jmethodID methodID, va_list args);
Diese Familie von Operationen ruft eine Instanzmethode (nicht-static) für ein Java-Objekt entsprechend der angegebenen Methoden-ID auf. Wenn diese Funktion dazu verwendet wird, private-Methoden und Konstruktoren aufzurufen, muß die Methoden-ID von der realen Klasse von obj abstammen, nicht von einer ihrer Superklassen.
Die herkömmlichen Argument-Konventionen werden verwendet, und Call<Type>-Method<X>() akzeptiert diese Argumente und leitet sie an jene Java-Methode weiter, die der Programmierer aufrufen möchte. Gibt das Ergebnis des Aufrufs der Java-Methode zurück. Verwirft alle Ausnahmen, welche die Java-Methode verwirft. j<type> ist, wie zuvor definiert, für primitive Typen, void oder jobject. Der erste Buchstabe von <Type> wird groß geschrieben.
j<type> CallNonvirtual<Type>Method(JNIEnv *e, jobject obj, jclass clazz,
jmethodID methodID, ...);
j<type> CallNonvirtual<Type>MethodA(JNIEnv *e, jobject obj, jclass clazz,
jmethodID methodID, jvalue *args);
j<type> CallNonvirtual<Type>MethodV(JNIEnv *e, jobject obj, jclass clazz,
jmethodID methodID, va_list args);
Diese Familie von Operationen ist identisch mit der letzten Familie, ausgenommen daß anstelle der Klasse obj für die Auswahl clazz verwendet wird.
jfieldID GetStaticFieldID(JNIEnv *e, jclass clazz, const char *name,
const char *desc);
Gibt die Feld-ID für ein static-Feld einer Klasse aus. Das Feld wird durch seinen Namen und Descriptor definiert. name und desc sind 0-terminierte UTF-8-Zeichenfolgen. Gibt eine Feld-ID zurück oder NULL, wenn das angegebene static-Feld nicht gefunden werden kann. Verwirft NoSuchFieldError, wenn das angegebene static-Feld nicht gefunden werden kann, ExceptionInInitializerError, wenn der Klassen-Initialisierer aufgrund einer Ausnahme fehlschlägt, oder OutOfMemoryError, wenn das System den Arbeitsspeicher übersteigt.
j<type> GetStatic<Type>Field(JNIEnv *e, jobject obj, jfieldID fieldID);
void SetStatic<Type>Field(JNIEnv *e, jobject obj, jfieldID fieldID, j<type> value);
Diese Familie der Zugangsroutinen gibt den Wert eines static-Felds eines Objekts aus. <type> ist, wie bereits definiert, für primitive Typen oder object. Der erste Buchstabe von <Type> wird groß geschrieben.
jmethodID GetStaticMethodID(JNIEnv *e, jclass clazz, const char *name,
const char *desc);
Gibt die Methoden-ID für eine Klassenmethode (static) für eine Klasse oder Schnittstelle zurück. Die Methode wird von ihrem Namen und Descriptor bestimmt. name und desc sind 0-terminierte UTF-8-Zeichenfolgen. Gibt einen Methoden-ID zurück oder NULL, wenn die Operation fehlschlägt. Verwirft NoSuchMethod-Error, wenn die angegebene static-Methode nicht gefunden werden kann, ExceptionInInitializerError, wenn der Klassen-Initialisierer aufgrund einer Ausnahme fehlschlägt, oder OutOfMemoryError, wenn das System den Arbeitsspeicher übersteigt.
j<type> CallStatic<Type>Method(JNIEnv *e, jclass clazz, jmethodID methodID, ...);
j<type> CallStatic<Type>MethodA(JNIEnv *e, jclass clazz, jmethodID methodID,
jvalue *args);
j<type> CallStatic<Type>MethodV(JNIEnv *e, jclass clazz, jmethodID methodID,
va_list args);
Diese Familie von Operationen ruft eine Klassenmethode (static) für eine Java-clazz auf, entsprechend der angegebenen Methoden-ID. Die Methoden-ID muß von clazz, nicht von einer ihrer Superklassen abstammen.
Die herkömmlichen Argument-Konventionen werden verwendet, und CallStatic<Type>Method<X>() akzeptiert diese Argumente und leitet sie an die Java-Methode weiter, die der Programmierer aufrufen möchte. Gibt das Ergebnis eines Aufrufs einer Java-Methode zurück. Verwirft jene Ausnahmen, die die Java-Methode verwirft. j<type> ist, wie bereits definiert, für primitive Typen, void oder jobject. Der erste Buchstabe von <Type> wird groß geschrieben.
jstring NewString(JNIEnv *e, const jchar *unicodeChars, jsize len);
Konstruiert ein neues java.lang.String-Objekt aus einem Array von Unicode-Zeichen. Gibt ein Java-Zeichenkettenobjekt zurück oder NULL, wenn die Zeichenkette nicht konstruiert werden kann. Verwirft OutOfMemoryError, wenn das System den Arbeitsspeicher übersteigt.
jsize GetStringLength(JNIEnv *e, jstring string);
Gibt die Länge (Anzahl der Unicode-Zeichen) einer Java-Zeichenkette aus.
const jchar *GetStringChars(JNIEnv *e, jstring string, jboolean *isCopy);
Gibt einen Pointer zu einem Array von Unicode-Zeichen der Zeichenkette zurück. Dieser Pointer ist gültig, bis ReleaseStringchars() aufgerufen wird. Wenn isCopy nicht NULL ist, dann wird *isCopy auf »Eins« gesetzt, sobald eine Kopie erstellt wird, oder auf »Null«, wenn keine Kopie erstellt wird. Gibt einen Pointer an eine Unicode-Zeichenkette aus oder NULL, wenn die Operation fehlschlägt.
void ReleaseStringChars(JNIEnv *e, jstring string, const jchar *chars);
Informiert die virtuelle Maschine darüber, daß ein Zugang zu chars nicht länger notwendig ist (der Pointer wird von GetStringChars() zurückgegeben).
jstring NewStringUTF(JNIEnv *e, const char *bytes, jsize length);
jsize GetStringUTFLength(JNIEnv *e, jstring string);
const jbyte *GetStringUTFChars(JNIEnv *e, jstring string, jboolean *isCopy);
void ReleaseStringUTFChars(JNIEnv *e, jstring string, const jbyte *utf);
Identisch mit den vorherigen vier Funktionen, aber für Operationen mit UTF-8-Zeichenketten d.h. C (nicht-Java) darstellenden Zeichenketten.
jsize GetArrayLength(JNIEnv *e, jarray array);
Gibt die Anzahl der Elemente im Array zurück.
jarray NewObjectArray(JNIEnv *e, jsize length, jclass elementClass, jobject obj);
Konstruiert ein neues Array, das Objekte der Klasse elementClass enthält. Alle Elemente sind ursprünglich mit obj definiert. Gibt ein Java-Array-Objekt aus oder NULL, wenn das Array nicht konstruiert werden kann. Verwirft OutOfMemoryError, wenn das System den Arbeitsspeicher übersteigt.
jobject GetObjectArrayElement(JNIEnv *e, jarray array, jsize index);
Gibt ein Element eines Objekts zurück. Erzeugt ArrayIndexOutOfBoundsException, wenn der angegebene Index keinen gültigen Index im Array definiert.
void SetObjectArrayElement(JNIEnv *e, jarray array, jsize index, jobject value);
Richtet ein Element eines Objekt-Arrays ein. Erzeugt ArrayIndexOutOfBoundsException, wenn der angegebene Index keinen gültigen Index im Array angibt, oder ArrayStoreException, wenn die Klasse des Wertes keine Subklasse der Elementklasse des Arrays ist.
j<type>Array New<Type>Array(JNIEnv *e, jsize length);
Eine Familie von Operationen, um ein neues Array von <type> zu definieren (wie bereits definiert). Gibt ein Java-Array-Objekt zurück oder NULL, wenn das Array nicht konstruiert werden kann. Verwirft OutOfMemoryError, wenn das System den Arbeitsspeicher übersteigt.
j<type> *Get<Type>ArrayElements(JNIEnv *e, j<type>Array array, jboolean *isCopy);
Eine Familie von Funktionen, die den Körper eines Arrays von <type> zurückgibt (wie bereits definiert). Das Ergebnis ist gültig, bis die entsprechende Release-ScalarArrayElements()-Funktion aufgerufen wird. Da das zurückgegebene Array eine Kopie des Original-Java-Arrays sein kann, werden die Änderungen am zurückgegebenen Array nicht unbedingt im Originalarray wiedergegeben, bis nicht ReleaseScalarArrayElements() aufgerufen ist. Wenn isCopy nicht NULL ist, dann wird *isCopy auf »Eins« gesetzt, wenn eine Kopier erstellt wird oder auf »Null«, wenn keine Kopie erstellt wird. Gibt einen Pointer zu den Array-Elementen zurück oder Null, wenn die Operation fehlschlägt.
Unabhängig davon, wie boolean-Arrays in der Java VM dargestellt werden, GetBooleanArrayElements() gibt immer einen Pointer an jboolean aus, wobei jedes Byte ein Element markiert (d.h. die unverpackte Repräsentation). Alle Arrays der anderen Typen grenzen garantiert im Speichern aneinander.
void Release<Type>ArrayElements(JNIEnv *e, j<type>Array array, j<type> *elems,
jint mode);
Eine Familie von Funktionen, welche die virtuelle Maschine darüber informiert, daß ein Zugriff auf elems nicht länger notwendig ist (der Pointer, der vom entsprechenden GetGet<Type>ArrayElements()zurückgegeben wird). Wenn notwendig, werden alle Änderungen von elems zum Original-Array zurückkopiert. Das mode-Argument enthält Informationen darüber, wie der Array-Puffer freigegeben werden sollte. mode hat keine Auswirkungen, wenn elems keine Kopie der Elemente im Array enthält. Andernfalls hat mode die folgende Auswirkung:
mode: Actions:
0 Copy back the content and free the elems buffer.
JNI_COMMIT Copy back the content but do not free the elems buffer.
JNI_ABORT Free the buffer without copying back the possible changes.
In den meisten Fällen leiten die Programmierer Null an das mode-Argument weiter, um ein konsistentes Verhalten für die aufgezeichneten und kopierten Arrays sicherzustellen. Die anderen Optionen geben dem Programmierer größere Kontrolle über die Speicherverwaltung. Sie müssen mit extremer Sorgfalt verwendet werden.
void Get<Type>ArrayRegion(JNIEnv *e, j<type>Array array, jsize start, jsize len,
j<type> *buf);
Eine Familie von Funktionen, die einen Bereich eines Arrays von <type> (wie bereits definiert) in einen Puffer kopiert. Verwirft ArrayIndexOutOfBoundsException, wenn (start + len 1) keinen gültigen Index im Array angibt.
void Set<Type>ArrayRegion(JNIEnv *e, j<type>Array array, jsize start, jsize len,
j<type> *buf);
Eine Familie von Funktionen, die einen Puffer zurück in einen Bereich eines Arrays von <type> definiert (wie bereits definiert). Verwirft ArrayIndexOutOfBoundsException, wenn (start + len 1) keinen gültigen Index im Array angibt.
jint RegisterNatives(JNIEnv *e, jclass clazz, const JNINativeMethod *methods,
jint nMethods);
Registriert native-Methoden mit der Klasse, die vom Argument clazz angegeben wird. Jede JNINativeMethod-Struktur (von nMethods) enthält den Namen, Descriptor und den Nicht-Java-Funktionszeiger für eine native-Methode. Gibt bei Erfolg Null zurück, bei Fehlschlag eine negative Zahl. Verwirft NoSuchMethodError, wenn eine angegebene Methode nicht gefunden werden kann oder wenn die Methode nicht native ist.
jint UnregisterNatives(JNIEnv *e, jclass clazz, const jstring *methods,
jint nMethods);
Unregistrierte native-Methoden mit der Klasse, die vom Argument clazz angegeben wird. Jedes Element des NULL-terminierten methods-Array (von nMethods) ist der Name einer nicht registrierten native-Methode. Bei Erfolg wird Null zurückgegeben, bei Mißerfolg eine negative Zahl.
jint MonitorEnter(JNIEnv *e, jobject obj);
Gibt den Monitor an, der mit dem darunterliegenden Java-Objekt verbunden ist; der Bezug wird durch obj hergestellt. Jedes Java-Objekt verfügt über einen zugehörigen Monitor. Wenn der aktuelle Thread bereits mit einem eigenen Monitor durch obj verbunden ist, wird der Counter im Monitor heraufgesetzt. Dieser gibt an, wie oft dieser Thread den Monitor besucht hat. Wenn der durch ref verbundene Monitor nicht durch einen Thread belegt ist, wird der aktuelle Thread zum Eigentümer des Monitors; hierbei wird die Eingangszählung für diesen Monitor auf 1 gesetzt. Wenn ein anderer Thread bereits den mit ref verbundenen Monitor besitzt, wartet der aktuelle Thread, bis der Monitor freigegeben wird, und versucht dann erneut, Eigentümer zu werden. Bei Erfolg wird Null zurückgegeben, bei Mißerfolg eine negative Zahl.
jint MonitorExit(JNIEnv *e, jobject obj);
Der aktuelle Thread muß der Eigentümer jenes Monitors sein, der mit dem darunterliegenden Java-Objekt durch obj verbunden ist. Der Thread setzt den Counter herab, der die Anzahl der Besuche im Monitor angibt. Wenn der Wert des Counter Null wird, gibt der aktuelle Thread den Monitor frei. Bei Erfolg wird Null ausgegeben, bei Mißerfolg eine negative Zahl.
jint GetJavaVM(JNIEnv *e, JavaVM **vm);
Gibt die Schnittstelle der virtuellen Java-Maschine (die in der Invocation-API verwendet wird, siehe nächsten Abschnitt) mit dem aktuellen Thread verbunden aus. Das Ergebnis wird an der Position plaziert, auf die vom zweiten Argument vm verwiesen wird. Bei Erfolg wird Null zurückgegeben, bei Mißerfolg eine negative Zahl.
Sie haben die wesentlichen JNI-Funktionen nun kennengelernt. Ehe wir mit der Invocation-API fortfahren, sollten Sie noch einmal zurückgehen und sich SimpleFile.C zuwenden, um zu sehen, ob ihnen dieser Code nun klarer ist (das sollte er!). Und jetzt holen Sie noch einmal tief Luft, der nächste (große) Abschnitt ist der letzte.
Die Invocation-API ermöglicht es, die virtuelle Java-Maschine in eine Nicht-Java-Anwendung zu laden. D.h. Sie können Anwendungen Java-fähig machen, ohne eine statische Verbindung zum Quellcode der virtuellen Java-Maschine einzurichten. Im folgenden finden Sie ein Beispiel für deren Verwendung:
#include <jni.h> /* where everything is defined */
. . .
JavaVM *jvm /* denotes a Java Virtual Machine */
JNIEnv *e; /* pointer to native method interface */
JDK1_1InitArgs vm_args; /* JDK 1.1 VM initialization args. */
. . .
JNI_GetDefaultJavaVMInitArgs(&vm_args); /* get the default arguments */
vm_args.classpath = ...; /* optionally change them */
JNI_CreateJavaVM(&jvm, &e, &vm_args); /* load and init. a Java VM */
jclass jc = (*e)->FindClass("Main"); /* find a class inside the VM */
jmethodID jmID = (*e)->GetStaticMethodID(jc, "test", "(I)V");
(*e)->CallStaticVoidMethod(jc, jmID, 100); /* invoke a method there */
(*jvm)->DestroyJavaVM(); /* were done */
In diesem Beispiel werden drei Funktionen der Invocation-API verwendet. Diese sollen im folgenden beschrieben werden.
Die Funktion JNI_CreateJavaVM() lädt und initialisiert die virtuelle Java-Maschine und gibt einen Pointer an den JNI-Schnittstellen-Pointer aus. Der Thread, der JNI_CreateJavaVM() aufruft, wird als Haupt-Thread betrachtet. Der JNI-Schnittstellen-Pointer (JNIEnv) ist nur im aktuellen Thread gültig. Sollte ein anderer Thread Zugang zur virtuellen Maschine wünschen, muß dieser zuerst AttachCurrentThread() aufrufen, um sich selbst zu verbinden und einen JNI-Schnittstellen-Pointer zu erhalten. Sobald die Verbindung hergestellt ist, funktioniert ein nativer Thread ebenso wie ein herkömmlicher Java-Thread, der innerhalb einer native-Methode ausgeführt wird. Der native Thread bleibt verbunden, bis er DetachCurrentThread() aufruft.
Der Haupt-Thread kann sich selbst nicht abkoppeln. Statt dessen muß er DestroyJavaVM() aufrufen, um die gesamte virtuelle Maschine abzubrechen (dies ist der einzige Thread, der das kann). Der Haupt-Thread muß der einzige Benutzer-Thread sein, der noch ausgeführt wird, wenn er DestroyJavaVM() aufruft. Benutzer-Threads enthalten sowohl Java-Threads als auch zugewiesene native Threads. Diese Beschränkung ist sinnvoll, weil ein Java-Thread oder ein nativer Thread Systemressourcen enthalten kann, wie z.B. Datensperren, Fenster usw., und die Funktion DestroyJavaVM() diese Ressourcen nicht automatisch freisetzen kann die Verantwortung für die Freigabe von Systemressourcen, die von beliebigen Threads beansprucht werden, liegt beim Programmierer.
Da die verschiedenen virtuellen Java-Maschinen verschiedene Implementierungsschemen verwenden können, sind wahrscheinlich auch verschiedene Initialisierungsstrukturen erforderlich. Deshalb variiert der genaue Inhalt der folgenden Initialisierungsstrukturen zwischen den verschiedenen Implementierungen. Eine native Anwendung muß die Initialisierungsstruktur exakt definieren, abhängig von der jeweiligen virtuellen Maschine, welche die Anwendung aufrufen will. Im folgenden finden Sie die Struktur, die von der virtuellen Maschine 1.1 von SUN verwendet wird:
typedef struct JDK1_1InitArgs {
jint reserved0;
void *reserved1;
jint checkSource;
jint nativeStackSize;
jint javaStackSize;
jint minHeapSize;
jint maxHeapSize;
jint verifyMode;
const char *classpath;
jint (*vprintf)(FILE *fp, const char *format, va_list args);
void (*exit)(jint code);
void (*abort)();
jint enableVerboseGC;
jint disableAsyncGC;
} JavaVMInitArgs;
Der JavaVM-Typ ist ein Pointer zum Funktions-Pointer-Array der Invocation-API:
typedef const struct JNIInvokeInterface *JavaVM;
const struct JNIInvokeInterface ... = {
NULL, NULL, NULL,
DestroyJavaVM,
AttachCurrentThread, DetachCurrentThread,
};
void JNI_GetDefaultJavaVMInitArgs(void *vm_args);
Gibt eine Standard-Konfiguration der virtuellen Java-Maschine aus. vm_args ist ein Pointer zu einer für virtuelle Maschinen spezifischen Struktur, in welcher die Standard-Argumente plaziert werden.
jint JNI_GetCreatedJavaVMs(JavaVM **vmBuf, jsize bufLen, jsize *nVMs);
Gibt alle virtuellen Java-Maschinen aus, die erstellt worden sind. Pointers zu diesen werden im Puffer vmBuf in jener Reihenfolge geschrieben, in der sie erstellt wurden. Meistens wird die Anzahl der Einträge von bufLen geschrieben. Die gesamte Anzahl erstellter virtueller Maschinen wird in *nVM ausgegeben. JDK 1.1 unterstützt nicht mehr als eine virtuelle Maschine in einem einzelnen Prozeß. Bei Erfolg wird Null ausgegeben, bei Mißerfolg eine negative Zahl.
jint JNI_CreateJavaVM(JavaVM **p_vm, JNIEnv **p_env, void *vm_args);
Lädt und initialisiert die virtuelle Java-Maschine. Der aktuelle Thread wird zum Haupt-Thread. Definiert *p_vm, um auf das resultierende JavaVM zu verweisen, und *p_env für den JNI-Schnittstellen-Pointer des Haupt-Thread. JDK 1.1 unterstützt nicht mehr als eine VM in einem einzelnen Prozeß. Bei Erfolg wird Null ausgegeben, bei Mißerfolg eine negative Zahl.
jint DestroyJavaVM(JavaVM *vm);
Bricht eine virtuelle Java-Maschine ab und fordert deren Ressourcen. Dies kann nur der Haupt-Thread. Der Haupt-Thread muß der einzige noch verbliebene Benutzer-Thread sein, wenn er DestroyJavaVM() aufruft. Gibt bei Erfolg Null aus, bei Mißerfolg eine negative Zahl. JDK 1.1 unterstützt das Abbrechen virtueller Maschinen nicht.
jint AttachCurrentThread(JavaVM *vm, JNIEnv **p_env, void *thr_args);
Verbindet den aktuellen Thread mit der virtuellen Java-Maschine. Definiert *p_env für den neuen JNI-Schnittstellen-Pointer. Der Versuch, einen Thread anzufügen, der bereits verbunden ist, ist eine Nicht-Operation. Ein nativer Thread kann nicht gleichzeitig mit zwei virtuellen Java-Maschinen verbunden werden. thr_args verweist auf die Argumente der Thread-Verbindung eine Struktur, die ausschließlich dazu dient, die Implementierung in SUN auszuführen (es sind keine Verbindungsargumente erforderlich). Bei Erfolg wird Null ausgegeben, bei Mißerfolg eine negative Zahl.
jint DetachCurrentThread(JavaVM *vm);
Verbindet den aktuellen Thread von einer virtuellen Java-Maschine. Alle Java-Monitoren, die von diesem Thread belegt werden, werden freigegegeben. Alle Java-Threads, die darauf warten, daß dieser Thread vernichtet wird, werden vermerkt. Der Haupt-Thread als jener Thread, der die virtuelle Maschine erstellt hat, kann davon nicht losgelöst werden. Dafür muß der Haupt-Thread JNI_DestroyJava-VM()aufrufen. Bei Erfolg wird Null ausgegeben, bei Mißerfolg eine negative Zahl.
Heute haben Sie die zahlreichen Nachteile der Verwendung von native-Methoden kennengelernt. Sie haben viele Möglichkeiten erfahren, mit denen sich das Schreiben und Ausführen der Programme beschleunigen läßt, aber auch verschiedene Aspekte eines übertriebenen Verständnisses von Ausrichtung und Effizienz kennengelernt.
Sie wissen nun, wie die native-Methoden auf der Java- und C-Seite erstellt werden und wie sich eine Header-Datei erzeugen, kompilieren und verknüpfen läßt.
Schließlich haben Sie alle Einzelheiten über die neue native Java-Schnittstelle kennengelernt und darüber hinaus viele andere sinnvolle Funktionen.
Nachdem Sie das schwierige heutige Material durchgearbeitet haben, haben Sie einen der komplexesten Teile der Sprache Java gemeistert. Sie wissen nun, wie die Laufzeit-Umgebung von Java selbst erstellt wurde und wie Sie diese leistungsstarke Umgebung auf unterster Ebene erweitern und optimal einsetzen können.
Als Belohnung erfahren Sie morgen, was sich »hinter den Kulissen« von Java abspielt. Sie lernen einige verborgene Leistungsmerkmale von Java kennen.
F Muß die Java-Klassenbibliothek System.loadLibrary() aufrufen, um die integrierte Klasse zu laden?
A Nein, Sie werden keine loadLibrary()-Aufrufe in der Implementierung von Klassen in der Java-Klassenbibliothek finden. Das Java-Team konnte sich den Luxus leisten, den Großteil seines Codes statisch mit der Java-Umgebung zu verknüpfen. Das ist natürlich nur sinnvoll, wenn man in der glücklichen Lage ist, ein Gesamtsystem zu entwickeln. Ihre Klassen müssen ihre Bibliotheken dynamisch mit einer bereits laufenden Kopie des Java-Systems verknüpfen. Das ist aber auch flexibler als die statische Verknüpfung. Sie haben dadurch die Möglichkeit, jederzeit alte und neue Versionen Ihrer Klassen einzubinden oder zu entfernen, d.h. Ihre Klassen laufend zu aktualisieren.
F Kann ich meine eigenen Klassen in Java statisch verknüpfen, wie es das Java-Team gemacht hat?
A Sie können das, wenn Sie es wünschen. Fragen Sie SUN Microsystems nach den Quellen der Java-Laufzeitumgebung. Solange Sie die gesetzlichen Regelungen in Bezug auf die Verwendung dieses Codes einhalten, können Sie das gesamte Java-System plus Ihre eigenen Klassen verknüpfen. Dadurch werden Ihre Klassen statisch in das System eingebunden. Sie müssen aber jemanden, der ihr Programm nutzen will, diese spezielle Version der Java-Umgebung zur Verfügung stellen. In der Regel ist die dynamische Verknüpfung zwar stärker eingeschränkt, sie ist aber die praktischere Alternative.
F Ich brauche in meinen Applets eine Funktionalität, die ich in der Java-Bibliothek vermisse. Angesichts der vielen Nachteile möchte ich möglichst keine native-Methoden verwenden. Gibt es Alternativen?
A Da wir uns noch am Anfang der Java-Geschichte befinden, könnten Sie als Alternative zu native-Methoden das Java-Team davon überzeugen, daß die von Ihnen benötigte Funktionalität ein allgemein interessantes Thema für Java-Programmierer ist. Eventuell wird sie in künftige Java-Versionen einbezogen. Außerdem ist bereits geplant, bestimmte »fehlende« Funktionalitäten künftig zu berücksichtigen (siehe http://java.sun.com für regelmäßige Aktualisierungen). Schicken Sie Ihre Vorschläge an die Newsgroup comp.lang.java, und sehen Sie auch nach, ob die betreffende Funktionalität eventuell bereits von SUN oder anderweitig bearbeitet wird. Vergessen Sie nicht, daß die Java-Gemeinschaft jung und dynamisch ist und daß Sie nicht allein sind.
(c) 1997 SAMS