Native Methoden und Bibliotheken
von Charles L. Perkins
Heute lernen Sie das Warum und Wie von native-Methoden in Java und Javas-integrierte Optimierungen und Tricks, die Sie anwenden können, damit Ihre Programme schneller werden. Ferner lernen Sie die Prozedur zum Erstellen von Headern und Stubs für native-Methoden und zum Verknüpfen derselben in einer dynamisch ladbaren Bibliothek.
Wir beginnen mit den Gründen, warum native-Methoden überhaupt implementiert werden.
Für die Deklaration von native-Methoden gibt es nur zwei Gründe. Der erste und bei weitem wichtigere Grund ist, daß Sie spezielle Fähigkeiten Ihres Rechners oder Betriebssystems nutzen können, die die Java-Klassenbibliothek nicht bereitstellt. 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, 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, oft illusionäre Grund für die Implementierung von native-Methoden ist die Geschwindigkeit. Ich sage »illusionär«, weil man mit diesem Ansatz nur selten die Geschwindigkeit steigert. Noch seltener kann die Geschwindigkeit auf andere Art beschleunigt werden (wie Sie heute noch sehen werden). Die Verwendung von native-Methoden in diesem Fall birgt einen Vorteil in der Tatsache, daß das derzeitige Java-Release beispielsweise optimierte C-Programme nicht gut ausführt. Für solche Aufgaben können Sie den geschwindigkeitsorientierten Teil (z. B. kritische innere Schleifen) in C schreiben 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 Ergebnisse davon nicht erkennen.
Haben Sie sich entschieden, daß Sie native-Methoden in Ihrem Programm verwenden wollen oder müssen, kostet Sie diese Entscheidung einiges. 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 künftig auszuführen. Sobald Sie eine Bibliothek mit nativem Code erstellt haben, 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 javakundigen Browser, der nativen Code in einem Applet zuläßt. Das Java-Team hat sich bemüht, so viel 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 zu wandern und sich auf einer beliebigen Plattform niederzulassen, ist schlimm genug. Erschwerend kommt noch hinzu, daß Ihr Code, der kein Applet mehr sein darf, von den Maschinen abhängig ist, die die virtuelle Java-Machine auf ihr Betriebssystem portiert haben.
Das ist 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.
Falls Sie nach den obigen Ausführungen immer noch nativen Code verwenden wollen 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ß heute aber, 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 wiederverwendbar sind. (Wenn Sie glauben, daß das einfach ist, sehen Sie sich um und Sie werden Berge von schlechter Software finden.) Wenn Sie den Großteil Ihres Programmieraufwands auf der Überlegung dieser grundlegenden Ziele und deren Erreichung 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 sagen, daß Effizienz nicht wichtig ist. Vielmehr sollten die obigen Ziele zuerst erreicht werden, dann ergibt sich eine saubere Software, die ganz natürlich die Struktur des Problems, das sie lösen soll, widerspiegelt, so daß Geschwindigkeit im nächsten Schritt realisiert werden kann.
Im Zusammenhang mit einer neuen Art der Programmierung, die bald entstehen soll, erwähnt Sun-Mitbegründer Bill Joy die vier Hauptfaktoren von Java: klein, einfach, sicher und stabil. Die Java-Sprache an sich ermutigt zu Klarheit und Abbau von Komplexität. Die intensive Verfolgung von Effizienz, die in der Regel die Komplexität steigert und die Klarheit verringert, ist mit diesen Zielen unvereinbar.
Haben Sie eine solide Grundlage aufgebaut, debuggen Sie Ihre Klassen, dann funktioniert Ihr Programm oder Applet wunschgemäß. Dann ist es an der Zeit, es zu optimieren. Handelt es sich um ein Benutzeroberflächen-Applet, brauchen Sie vielleicht nichts zu 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ß das nicht so ist.
Der nächste Schritt ist das Prüfen, ob Ihr Release den »Just-in-Time«-Compiler oder das java2c-Werkzeug benutzt.
Beim ersten handelt es sich um eine experimentelle Technologie, die beim Ablaufen der Methoden in der virtuellen Java-Machine 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 berücksichtigen. Auf jedem System, das diese Technologie implementiert, 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.
Der java2c-Übersetzer übersetzt die Bytecodes einer ganzen .class-Datei (gleichzeitig) in einen portablen C-Quellcode. Diese Version kann dann in einem herkömmlichen C-Compiler auf Ihrem Rechner kompiliert werden, um quasi eine 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.
Nehmen wir an, daß diese Technologien nicht verfügbar sind oder Ihr Programm nicht nach Ihrem Geschmack optimieren. In diesem Fall stellen Sie zuerst fest, welche Methoden am meisten Zeit beanspruchen. Mit diesen Informationen zur Hand beginnen Sie, gezielte Änderungen in Ihren Klassen durchzuführen.
Sie ermitteln also im ersten Schritt, welche Methoden die meiste Zeit in Anspruch nehmen. Enthalten diese Methoden Schleifen, prüfen Sie die inneren Schleifen, ob sie Methoden aufrufen, die final gemacht werden können, eine Methodengruppe aufrufen, die in eine Methode verschmolzen werden kann oder ob Objekte erstellt werden, die anstelle einzelner Schleifen wiederverwendet werden können.
Stellen Sie fest, daß eine lange Kette von beispielsweise 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, um auf das Objekt des betreffenden Methodenaufrufs direkt zu verweisen. Meist werden dadurch Schichten- oder Kapselungsbeschränkungen überschritten. Das und mehr ist der Preis, den man für Effizienz zahlen muß.
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 jetzt 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 bereit, loszulegen.
Zuerst löschen Sie die Methodenkörper (den gesamten Code zwischen den geschweiften Klammern und die Klammern {}) jeder Methode, die Sie ausgesucht haben. Dann ersetzen Sie sie durch ein einzelnes Semikolon (;). Danach fügen Sie den Modifier native in die vorhandenen Modifier der Methode ein. Als nächstes fügen Sie einen static-Initialisierer (Klasse) in 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 beliebig benennen - Einzelheiten folgen.) Das war's.
Das sind alle Schritte, um in Java eine native-Methode zu spezifizieren. 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.
Das betrifft aber nur die Java-Seite der Geschichte. Leider ist die Arbeit in der nativen Sprachumgebung nicht so einfach.
Stellen Sie sich einmal eine Version der Java-Umgebung vor, die keine Datei-E/As bereitstellt. In diesem Fall müßte ein Java-Programm, das ein Dateisystem benutzen muß, zuerst native-Methoden schreiben, um Zugriff auf die dafür erforderlichen 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 namens SimpleFile verschmolzen:
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 beim erstmaligen Laden der Klasse ausgeführt } }
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ß sie wie jede andere Klasse aussieht, mit einer Instanzvariablen, einem Constructor und zwei normalen Methoden. Dann gibt es vier native-Methoden. Das sind normale Methodendeklarationen, in denen lediglich der Codeblock durch ein Semikolon ersetzt und der Modifier native hinzugefügt wurde. Das sind die Methoden, die anschließend in C-Code implementiert werden müssen.
Dann gibt es einen etwas geheimnisvollen Codeteil am Ende der Klasse. Das allgemeine Gebilde hier ist ein static-Initialisierer. Der Code zwischen geschweiften Klammern wird nur einmal beim erstmaligen Laden der Klasse ausgeführt. Das bedeutet, daß die native Codebibliothek, die Sie in der heutigen Lektion noch erstellen, nur einmal ausgeführt wird.
Um Java-Objekte und Datentypen im C-Code manipulieren zu können, müssen Sie spezielle .h-Dateien einbinden. Die meisten dieser Dateien befinden sich in Ihrem Release-Verzeichnis im Unterverzeichnis include. Einige spezielle Formen, die wir benötigen, müssen genau auf die Methoden Ihrer Klasse zugeschnitten werden. An diesem Punkt greift das javah-Werkzeug.
Um die für Ihre native-Methoden benötigten Header zu erzeugen, kompilieren Sie zuerst SimpleFile in javac auf die übliche Weise. Dadurch erhalten Sie eine Datei namens SimpleFile.class. Diese Datei muß im javah-Werkzeug bearbeitet werden, wodurch die nötige Header-Datei entsteht (SimpleFile.h).
Die Ausgabe der SimpleFile von javah ist wie folgt:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <native.h> /* Header for class SimpleFile */ #ifndef _Included_SimpleFile #define _Included_SimpleFile struct Hjava_lang_String; typedef struct ClassSimpleFile { #define SimpleFile_separatorChar 62L struct Hjava_lang_String *path; long fd; } ClassSimpleFile; HandleTo(SimpleFile); extern /*boolean*/ long SimpleFile_open(struct HSimpleFile *); extern void SimpleFile_close(struct HSimpleFile *); extern long SimpleFile_read(struct HSimpleFile *,HArrayOfByte *,long); extern long SimpleFile_write(struct HSimpleFile *,HArrayOfByte *,long); #endif
Die Mitglieder von struct entsprechen Eins zu Eins den Variablen der Klasse.
Damit eine Instanz Ihrer Klasse sanft im C-Land aufsetzen kann, benutzen wir das Makro unhand(). Die this-Pseudovariable von Java erscheint in C beispielsweise als struct HSimpleFile*. Um Variablen dieser Instanz zu benutzen, müssen Sie manuell unhand() deklarieren. Im weiteren Verlauf der heutigen Lektion sehen Sie noch solche Beispiele.
Zwischen der Java- und der C-Welt sind Stubs erforderlich.
Stubs können von javah wie Header automatisch erzeugt werden. Sie müssen über die Stubs-Datei nicht viel wissen. Sie wird kompiliert und mit dem C-Code verknüpft, damit dieser korrekt mit Java kommunizieren kann. Eine Stubs-Datei (in diesem Beispiel SimpleFile.c) wird erzeugt, indem Sie Ihre Klasse mit der Option -stubs durch den javah-Compiler jagen.
Das Ergebnis der Ausführung von -stubs SimpleFile in javah sieht so aus:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <StubPreamble.h> /* Stubs for class SimpleFile */ /* SYMBOL: OSimpleFile/open()ZO, Java_SimpleFile_open_stub */ stack_item *Java_SimpleFile_open_stub(stack_item *_P_,struct execenv *_EE_) { extern long SimpleFile_open(void *); _P_[0].i = SimpleFile_open(_P_[0].p); return _P_ + 1; } /* SYMBOL: OSimpleFile/close()VO, Java_SimpleFile_close_stub */ stack_item *Java_SimpleFile_close_stub(stack_item *_P_,struct execenv *_EE_) { extern void SimpleFile_close(void *); (void) SimpleFile_close(_P_[0].p); return _P_; } /* SYMBOL: OSimpleFile/read([BI)IO, Java_SimpleFile_read_stub */ stack_item *Java_SimpleFile_read_stub(stack_item *_P_,struct execenv *_EE_) { extern long SimpleFile_read(void *,void *,long); _P_[0].i = SimpleFile_read(_P_[0].p,((_P_[1].p)),((_P_[2].i))); return _P_ + 1; } /* SYMBOL: OSimpleFile/write([BI)IO, Java_SimpleFile_write_stub */ stack_item *Java_SimpleFile_write_stub(stack_item *_P_,struct execenv *_EE_) { extern long SimpleFile_write(void *,void *,long); _P_[0].i = SimpleFile_write(_P_[0].p,((_P_[1].p)),((_P_[2].i))); return _P_ + 1; }
Jede Kommentarzeile enthält die Methodenunterschrift für eine der vier native-Methoden, die Sie implementieren. Sie können eine dieser Unterschriften verwenden, um in Java zu wechseln und beispielsweise eine Überschreibungssubklasse einer native-Methode aufzurufen. Meist werden Unterschriften benutzt, um eine Java-Methode aus C aufzurufen und eine Aktion in der Java-Welt zu veranlassen.
Sie erreichen dies, indem Sie die spezielle C-Funktion execute_java_ dynamic_method()in der Java-Laufzeit aufrufen. Ihre Argumente beinhalten das Zielobjekt des Methodenaufrufs und die Unterschrift der Methode. Die allgemeine Form einer qualifizierten Methodenunterschrift ist any/package/name/ClassName/methodName(...)X. (Das letzte Beispiel enthält mehrere davon, wobei SimpleFile der Klassenname ist und es keinen Paketnamen gibt.) Das X stellt die Ausgabeart dar und die drei Punkte (...) enthalten eine Zeichenkette, die die Argumenttypen darstellt. In diesem Beispiel steht für den Buchstaben und die Zeichenkette [T (Array vom Typ T), B (byte), I (int), V (void) und Z (boolean).
Die Methode close(), die keine Argumente enthält und void ausgibt, wird durch die Zeichenkette "SimpleFile/close()V" darstellt. open() gibt einen booleschen Wert aus und wird durch "SimpleFile/open()Z" dargestellt. read() umfaßt ein Byte-Array und eine Ganzzahl, d. h. zwei Argumente, gibt int aus und wird durch die Zeichenkette "SimpleFile/read([BI)I" dargestellt. (Weitere Einzelheiten finden Sie im Abschnitt »Methodenunterschriften« 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 brauchen, 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-E/A-Routinen benötigt). Schließlich stellen Sie den gesamten C-Code in eine neue Datei und nennen sie SimpleFileNative.c. Hier das Ergebnis:
#include "SimpleFile.h" /* for unhand(), among other things */ #include <sys/param.h> /* for MAXPATHLEN */ #include <fcntl.h> /* for O_RDWR and O_CREAT */ #define LOCAL_PATH_SEPARATOR O/O /* UNIX */ static void fixSeparators(char *p) { for (; *p != O\0O; ++p) if (*p == SimpleFile_separatorChar) *p = LOCAL_PATH_SEPARATOR; } long SimpleFile_open(struct HSimpleFile *this) { int fd; char buffer[MAXPATHLEN]; javaString2CString(unhand(this)->path, buffer, sizeof(buffer)); fixSeparators(buffer); if ((fd = open(buffer, O_RDWR | O_CREAT, 0664)) < 0) /* UNIX open */ return(FALSE); /* or, SignalError() could throw an exception */ unhand(this)->fd = fd; /* save fd in the Java world */ return(TRUE); } void SimpleFile_close(struct HSimpleFile *this) { close(unhand(this)->fd); unhand(this)->fd = -1; } long SimpleFile_read(struct HSimpleFile *this, HArrayOfByte *buffer, long count) { char *data = unhand(buffer)->body; /* get array data */ int len = obj_length(buffer); /* get array length */ int numBytes = (len < count ? len : count); if ((numBytes = read(unhand(this)->fd, data, numBytes)) == 0) return(-1); return(numBytes); /* the number of bytes actually read */ } long SimpleFile_write(struct HSimpleFile *this, HArrayOfByte *buffer, long count) { char *data = unhand(buffer)->body; int len = obj_length(buffer); return(write(unhand(this)->fd, data, (len < count ? len : count))); }
Wenn Sie mit dem Schreiben Ihrer .c-Datei fertig sind, kompilieren Sie sie in Ihrem lokalen C-Compiler (normalerweise cc oder gcc). Auf manchen Systemen sind spezielle Kompilierflags erforderlich.
Zum Schreiben von C-Code für native Implementierungen stehen verschiedene (interne) Makros und Funktionen zur Verfügung (einige davon haben wir in SimpleFileNative.c benutzt).
Zum besseren Verständnis betrachten wir einige davon genauer.
Der Code
Object *unhand(handle *) int obj_length(HArray *)
gibt einen Pointer auf den Datenteil eines Objekts und die Länge eines Arrays aus. Der ausgegebene Typ ist aber nicht immer Object *, sondern variiert je nach dem Typ von Handle (oder HArray).
Im folgenden Beispiel wird eine Klasse (durch name bezeichnet) gesucht, die eine Zeichenkette der Länge length im durch type bezeichneten Typ erstellt:
ClassClass *FindClass(struct execenv *e, char *name, bool_t resolve) HArrayOfChar *MakeString(char *string, long length) Handle *ArrayAlloc(int type, int length)
Sie benutzen die Funktion
long execute_java_dynamic_method(ExecEnv *e, HObject *obj, char *method_name; char *signature, ...);
um eine Java-Methode von C aufzurufen. e ist in der aktuellen Umgebung NULL. Das Ziel des Methodenaufrufs ist obj. Die Methode method_name hat die mit signature bezeichnete Methode. Sie kann beliebig viele Argumente haben und einen 32-Bit-Wert (int, Handle * oder einen 32-Bit C-Typ) ausgeben.
Mit dem folgenden Code rufen Sie einen Java-Constructor und eine Klassenmethode von C auf. c ist die Zielklasse. Der Rest ist wie in executemethod():
HObject *execute_java_constructor(ExecEnv *e, char *classname, ClassClass *c, char *signature, ...); long execute_java_static_method(ExecEnv *e, ClassClass *c, char *method_name; char *signature, ...);
Mit folgendem Code senden Sie eine Java-Ausnahme, die bei der Ausgabe Ihrer native-Methode ausgeworfen wird:
SignalError(0, JAVAPKG "ExceptionClassName", "message");
Das ist in etwa wie folgender Java-Code:
throw new ExceptionClassName("message");
Schließlich noch einige nützliche Funktionen zur Umwandlung von Zeichenketten:
void javaStringPrint(Hjava_lang_String *s) int javaStringLength(Hjava_lang_String *s) Hjava_lang_String *makeJavaString(char *string, int length) char *makeCString(Hjava_lang_String *s) char *allocCString(Hjava_lang_String *s) unicode *javaString2unicode(Hjava_lang_String *s, unicode *buf, int len) char *javaString2CString(Hjava_lang_String *s, char *buf, int len)
Die ersten zwei Methoden geben eine Java-Zeichenkette (wie System.out.print()) aus bzw. holen deren Länge. Die dritte bildet aus einer C-Zeichenkette eine Java-Zeichenkette. Die vierte und fünfte wandeln eine Java- in eine C-Zeichenkette um. Die letzten zwei Methoden kopieren Java-Zeichenketten in existierende Unicode- oder ASCII-C-Puffer.
Als letzten Schritt, den Sie in der C-Welt ausführen müssen, kompilieren Sie die Stubs-Datei SimpleFile.c unter Verwendung der gleichen Kompilationsflags, die Sie für SimpleFileNative.c verwendet haben.
Sie haben damit den gesamten C-Code für Ihre ladbare native Bibliothek geschrieben und kompiliert.
Sie sind jetzt an dem Punkt angelangt, an dem Sie alles zusammenstellen und die native Bibliothek erzeugen können.
Sie beginnen jetzt mit der Zusammenstellung Ihrer Arbeit in einer einzigen Bibliotheksdatei. Diese unterscheidet sich je nach dem System, auf dem Java läuft. Das folgende Beispiel in Unix-Syntax soll Ihnen das Grundkonzept aufzeigen:
cc -G SimpleFile.o SimpleFileNative.o -o simple
Die -G-Flag teilt dem Linker mit, daß Sie eine dynamisch verknüpfbare Bibliothek erzeugen. Die Einzelheiten unterscheiden sich je nach System.
Wird Ihre Java-Klasse SimpleFile erstmals in Ihrem Programm geladen, versucht die System-Klasse, die Bibliothek namens simple zu laden. 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 schlau macht. Sie teilt ihm die Folge der zu durchsuchenden Verzeichnisse mit, wenn neue Bibliotheken mit native-Code geladen werden. Da das aktuelle Verzeichnis in Java standardmäßig der Ladepfad ist, kann simple im aktuellen Verzeichnis bleiben.
Heute haben Sie die zahlreichen Nachteile der Verwendung von native-Methoden kennengelernt. Sie haben viele Arten gelernt, die das Schreiben und Ausführen Ihrer Programme beschleunigt, aber auch verschiedene Aspekte übertriebener Ausrichtung auf Effizienz mitbekommen.
Sie haben die Prozedur zum Erstellen von native-Methoden auf der Java- und C-Seite gelernt und Header-Dateien und Stubs erstellt sowie ein komplettes Beispiel kompiliert und verknüpft.
Mit der heutigen Lektion haben Sie alle komplexen Teile der Java-Sprache abgedeckt. Sie wissen jetzt, wie die Java-Umgebung selbst entwickelt wurde und wie diese leistungsstarke Umgebung von Ihnen als Programmierer optimal angewandt und erweitert werden kann.
Zur Belohnung erfahren Sie morgen, was sich alles »hinter der Bühne« abspielt. Sie lernen einige versteckte Leistungsaspekte von Java kennen.
F: Wie kann ich das von Ihnen empfohlene Dokument »Implementing Native Methods« ergänzen?
A: In diesem Dokument befindet sich eine Makefile, andere zusammenhängende Informationen und eine ausführlichere Version des nächsten Beispiels mit Beschreibung. Sie können diese Informationen ergänzen, indem Sie sich ausgiebig im Internet umsehen.
F: Muß die Java-Klassenbibliothek System.loadLibrary() aufrufen, um die integrierten Klassen 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 flexbiler 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 wollen. 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 jemandem, 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, 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. Habe ich 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ünftigen Java-Versionen einbezogen. Außerdem ist bereits geplant, bestimmte »fehlende« Funktionalitäten künftig zu berücksichtigen. 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.
Copyright ©1996 Markt&Technik