26
Java RMI & Java NI Systemprogrammierung WS 08/09 Dozent: Prof. Dr. Helmut Weber Autoren: Mohamed Amine Regragui und Nadia Douiri 26.01.2009

Java RMI & NI - cs.hs-rm.deweber/sysprog/proj0809/java-rmi-ni.pdf · ist möglich, wenn X ein dem Server bekanntes Interface implementiert). In diesem ... Da die in unserem Interface

  • Upload
    donhi

  • View
    237

  • Download
    0

Embed Size (px)

Citation preview

Java RMI & Java NI

Systemprogrammierung WS 08/09

Dozent: Prof. Dr. Helmut Weber

Autoren: Mohamed Amine Regragui und Nadia Douiri

26.01.2009

2

Inhaltsverzeichnis 1 RMI .................................................................................................................................................. 3

1.1 Was ist RMI? ............................................................................................................................ 3

1.2 Wozu wird RMI benötigt? ....................................................................................................... 3

1.3 Was ist RPC? ............................................................................................................................ 3

1.4 RMI Architektur & Funktionsweise ......................................................................................... 4

1.5 Beispiel .................................................................................................................................... 5

2 Java Native Interface ..................................................................................................................... 20

2.1 2.1 Was ist JNI? ..................................................................................................................... 20

2.2 2.2 Wozu JNI? ....................................................................................................................... 20

2.3 2.3 Vorgehensweise/ Beispiel .............................................................................................. 20

2.4 2.4 Datentypen ..................................................................................................................... 24

2.5 2.5 Signaturen....................................................................................................................... 25

2.6 2.6 Strings ............................................................................................................................. 26

3

1 RMI

1.1 Was ist RMI?

Remote Method Invocation (RMI, deutsch etwa „Aufruf entfernter Methoden“), gelegentlich auch als Methodenfernaufruf bezeichnet, ist der Aufruf einer Methode eines entfernten Java-Objekts und realisiert die Java-eigene Art des Remote Procedure Call. „Entfernt“ bedeutet dabei, dass sich das Objekt in einer anderen Virtuellen Maschine befinden kann, die ihrerseits auf einem entfernten Rechner oder auf dem lokalen Rechner laufen kann. Dabei sieht der Aufruf für das aufrufende Objekt (bzw. dessen Programmierer) genauso aus wie ein lokaler Aufruf, es müssen jedoch besondere Ausnahmen abgefangen werden, die zum Beispiel einen Verbindungsabbruch signalisieren können

1.2 Wozu wird RMI benötigt?

� RMI stellt eine Kommunikationsmöglichkeit zwischen Programmen auf verschiedenen Rechnern / Virtuellen Maschinen dar

� RMI erlaubt den Methodenaufruf über Rechnergrenzen hinweg

� RMI ermöglicht den Austausch von Objekten (Daten und Anwendungslogik) in einem Netzwerk

� RMI lässt die eigentliche Netzwerkkommunikation für den Entwickler in den Hintergrund treten

1.3 Was ist RPC?

Remote Procedure Call (RPC, sinngemäß „Aufruf einer fernen Prozedur“) ist eine Technik zur Realisierung von Interprozesskommunikation. Sie ermöglicht den Aufruf von Funktionen in anderen Adressräumen. Im Normalfall werden die aufgerufenen Funktionen auf einem anderen Computer als das aufrufende Programm ausgeführt. Es existieren viele Implementierungen dieser Technik, in der Regel sind sie untereinander nicht kompatibel.

4

Ablauf

RPC ist eine Möglichkeit ein Client-Server-Modell zu implementieren. Die Kommunikation beginnt, indem der Client eine Anfrage an einen bekannten Server schickt und auf die Antwort wartet. In der Anfrage gibt der Client an, welche Funktion mit welchen Parametern ausgeführt werden soll. Der Server bearbeitet die Anfrage und schickt die Antwort an den Client zurück. Nach Empfang der Nachricht kann der Client seine Verarbeitung fortführen. Beim Einsatz von RPC können durch Kommunikationsfehler unterschiedliche Fehlerkonstellationen auftreten, die beachtet und bearbeitet werden müssen.

1.4 RMI Architektur & Funktionsweise

1. Der Server registriert ein Remote Object bei der RMI-Registry unter einem eindeutigen Namen.

2. Der Client schaut bei der RMI-Registry unter diesem Namen nach und bekommt eine Objektreferenz, die seinem Remote Interface entsprechen muss.

3. Der Client ruft eine Methode aus der Objektreferenz auf (dass diese Methode existiert, wird durch das Remote Interface garantiert). Dabei kann ein Objekt einer Klasse X übergeben werden, die der JVM des Servers bisher nicht bekannt ist (das ist möglich, wenn X ein dem Server bekanntes Interface implementiert). In diesem Fall lädt die Server-JVM die Klasse X dynamisch nach, beispielsweise vom Webserver des Client

4. Die Server-JVM führt die Methode auf dem Remote Object aus, wobei evtl. dynamisch geladener Fremdcode benutzt wird (z. B. Methoden von X), der im Allgemeinen Sicherheitsrestriktionen unterliegt. Dem Client werden die

5

Rückgabewerte dieses Aufrufes gesendet, oder der Client bekommt eine Fehlermeldung (z. B. bei einem Verbindungsabbruch).

1.5 Beispiel

RMI Berechnungsserver In diesem ersten Beispiel sollen alle wichtigen Schritte zu einer lauffähigen RMI Applikation erläutert und dokumentiert werden.

Ziel ist es, einen einfachen Berechnungsserver zu implementieren, der Aufträge von Clients entgegen nimmt, diese ausführt und das Ergebnis an den Client zurücksendet. I n t e r f a c e s

Als erstes müssen wir das Interface des Berechnungsservers definieren. Dieses legt alle remote Möglichkeiten fest, die der Server anbietet. Unser Berechnungsserver soll im Moment die 4 Grundrechnungsarten unterstützen. Bedingungen:

� Da die in unserem Interface definierten Methoden über RMI aufgerufen werden sollen, muss unser Interface das Interface java.rmi.Remote erweitern.

� Zusätzlich muss jede Methode die Ausnahme RemoteException auslösen.

Das Interface kann nun übersetzt werden: javac MyCalculatorInterface.java

I m p l e m e n t a t i o n d e s R e m o t e I n t e r f a c e Im nächsten Schritt wird nun eine Implementation für dieses Remote Interface bereitgestellt.

Bedingungen:

� In unserem Beispiel soll die Service-Klasse über die Klasse java.rmi.server.UnicastRemoteObject an RMI gebunden werden. Dies ist nicht unbedingt notwendig. Wir werden später andere Möglichkeiten kennen lernen.

� Falls eine Klasse UnicastRemoteObject erweitert, muss diese Klasse auch einen Konstruktor definieren, der eine RemoteException auslösen kann. Sobald dieser

6

Konstruktor den Konstruktor der Basisklasse über super() aufruft, wird automatisch die RMI Bindung und die remote Objektinitialisierung vorgenommen.

Nun kann auch dieses Java File übersetzt werden: javac MyCalculatorServer.java S t u b s u n d S k e l e t o n s Mit Hilfe des RMI Compilers rmic können nun die Stub und Skeleton Dateien erzeugt werden. Dem RMI Compiler wird als Argument die Class-Datei der Service-Implementation übergeben. Es muss darauf geachtet werden, dass auch das definierte Interface im selben Verzeichnis ist.

rmic -keep MyCalculatorServer Die Option –keep gibt an, dass die erzeugten Java-Source Files nach dem Kompilieren nicht gelöscht werden sollen. Nach dem Aufruf des RMI Compilers befinden sich die folgenden Dateien im Verzeichnis:

MyCalculatorInterface.java MyCalculatorInterface.class MyCalculatorServer.java MyCalculatorServer.class MyCalculatorServer_Skel.java MyCalculatorServer_Skel.class MyCalculatorServer_Stub.java

7

MyCalculatorServer_Stub.class

Hier einige weitere Informationen zum RMI Compiler:

Usage: rmic <options> <class names> wobei <options> die folgenden Werte annehmen kann:

-keep die generierten Skeleton- und Stub Javadateien nicht löschen. -keepgenerated (entspricht –keep) -g generieren von Debugging Informationen -depend neu übersetzen von veralteten Dateien -nowarn keine Warnungen ausgeben -verbose Ausgabe der Compiler-Meldungen

-classpath <path>

spezifiziert den Pfad, in dem nach Eingabedateien gesucht werden soll

-d <directory>

gibt den Zielpfad für die generierten Dateien an

-J <runtime flag> Argumentübergabe an den Java Interpreter

-v1.1 erzeuge Stubs/Skeletons für JDK 1.1 Protokoll

-v1.2 erzeuge Stub für Java 2 (Kein Skeleton)

-vcompat erzeuge Stubs/Skeletons die mit beiden JDK-Versionen

kompatibel sind

8

S e r v e r Die RMI Services müssen innerhalb eines eigenen Serverprozesses verwaltet werden. Zu diesem Zweck implementieren wir eine einfache Serverklasse, die im Konstruktor ein neues MyCalculatorServer–Objekt erzeugt und dieses bei der RMI Registry anmeldet.

Übersetzung des Servers: javac MyRmiServer.java C l i e n t Der Quellcode für den Client könnte wie folgt aussehen:

9

Auch der Client wird wieder kompiliert: javac MyRmiClient.java A u s f ü h r e n d e s R M I S y s t e m s Nun ist alles vorbereitet, um das komplette RMI System in Betrieb zu nehmen. Sie benötigen nun 3 Konsolenfenster um das RMI System auszuprobieren:

1. Im ersten Fenster wird die RMI Registry gestartet

2. Im zweiten Fenster wird der Server gestartet

3. Im dritten Fenster wird der Client gestartet Zuerst wird wie gesagt die Registry gestartet. Wechseln Sie dazu in das Verzeichnis, in dem sich die Klassendateien befinden. Dadurch können Sie eine Verteilung der Klassendateien über HTTP oder FTP vermeiden. Löschen Sie vor dem Start der Registry den Classpath um allfällige Exceptions zu vermeiden.

set CLASSPATH= <enter> rmiregistry <enter>

Anschliessend kann der Server gestartet werden:

java –cp . MyRmiServer Über die Option –cp . wird der CLASSPATH für diese Anweisung zurückgesetzt auf das lokale Verzeichnis.

Nun kann schlussendlich noch der Client gestartet werden:

java –cp . MyRmiClient

10

Wenn alles klappt sollte im Client-Fenster die folgende Ausgabe erscheinen:

[MyRmiClient] Resultat: 7 [MyRmiClient] Resultat: 9 [MyRmiClient] Resultat: 12 [MyRmiClient] Resultat: 4

Falls Sie die RMI Registry stoppen und neu starten müssen Sie den Service neu anmelden, da rmiregistry.exe den Namensdienst nur im Speicher aufbaut und nicht persistent speichert.

Wir haben nun eine funktionsfähige RMI Anwendung entwickelt und in Betrieb genommen. Obwohl alle Komponenten auf demselben System laufen erfolgt die Kommunikation zwischen den JVM über den Netzwerkstack des Rechners.

Weiteres Beispiel: Aufgabestellung

In dieser Aufgabe soll, im Gegensatz zu der bisherigen dienstorientierten Sichtweise, eine objektorientierte Sichtweise eingenommen werden. Zeichenketten-Objekte beinhalten einen Zustand (die Zeichenkette) und können von Clients mit RMI-Methodenaufrufen bedacht werden. Zusätzlich existiert ein Dienst ZKManager, der eine Menge von Zeichenketten-Objekten beherbergt und nach außen bereitstellt und dessen Funktionen ebenfalls über RMI aufgerufen werden. (a) ZKManager:

Die Schnittstelle des Dienstes soll die folgenden Funktionen bereitstellen: � Erzeugen eines Zeichenkettenobjektes mit einem String-Namen und einem

initialen Inhalt, die beide als Parameter übergeben werden, liefert eine Referenz auf das erzeugte Objekt

� Löschen eines Zeichenkettenobjektes

� Öffnen eines Zeichenkettenobjekts bei übergegebenem String-Namen und Rückgabe einer Referenz auf das Zeichenkettenobjekt

(b) Zeichenkette:

Die Schnittstelle von Zeichenkettenobjekten soll die folgenden Methoden bereitstellen: � Initialisieren der Zeichenkette mit einem Anfangswert

� Zeichenweises Doppeln

� Verdoppeln der Zeichenkette

� Spiegeln der Zeichenkette

� Liefern der Länge der Zeichenkette

� Liefern des Inhalts der Zeichenkette

11

(c) Implementierung des Servers:

Die Implementierung des Servers soll beide Schnittstellen aus (a) und (b) implementieren. Bei der Initialisierung soll sich der ZKManager in der Registry registrieren.

(d) Implementierung eines Clients: Implementieren Sie ein Client-Programm mit einer kleinen Benutzer-schnittstelle, so dass der ZKManager zur Erzeugung von Zeichenketten-objekten in Anspruch genommen werden kann wie auch Methodenaufrufe auf den vorhandenen Zeichenkettenobjekten durchgeführt werden können. (Damit kann dann insbesondere der Inhalt eines Zeichenkettenobjekts abgefragt und ausgegeben werden.)

Lösung:

ZKManager Interface:

import java.io.Serializable; import java.rmi.Remote; import java.rmi.RemoteException; public interface ZKManager extends Remote, Serializ able{ /*Erzeugen eines Zeichenkettenobjektes mit einem S tring-Namen und einem initialen Inhalt, die beide als Parameter uebergeben werden, liefert eine Referenz auf das erzeugte Objekt*/ public TextObject createTextObject(String name, St ring content) throws RemoteException; /*Loeschen eines Zeichenkettenobjektes*/ public void deleteTextObject(String which) throws RemoteException; /* Oeffnen eines Zeichenkettenobjekts bei uebergeb enem String-Namen und Rueckgabe einer Referenz auf das Zeichenkettenobjekt*/ public TextObject openTextObject(String which) thr ows RemoteException; /*String wird exportiert d.h in die Registry aufge nommen.*/ void exportZK(String name) throws RemoteException; /*String wird importiert d.h aus der Registry gele sen.*/ TextObject importZK(String name) throws RemoteExce ption;

}

12

interface TextObject:

/* *Damit Objekte über RMI versendet werden können, m üssen diese serialisiert werden *D.h. seine Datenstruktur in einem Bytestrom einge packt und auf der anderen Seite wieder ausgepackt werden kann. *Dafür muss java.io.Serializable importiert werden . */ import java.io.Serializable; /*Um das Remote Interface Methoden (Stub) zu defini eren muss java.rmi.Remote importiert werden.*/ import java.rmi.Remote; /*Da es zu Kommunikationsproblemen kommen kann muss java.rmi.RemoteException importiert werden.*/ import java.rmi.RemoteException; public interface TextObject extends Remote, Seriali zable{ /* TextObject initialisieren*/ public void initText(String initInhalt) throws Rem oteException; /* Zeichenweise verdopeln */ public void signs() throws RemoteException; /* String dublizieren */ public void dublicate() throws RemoteException; /* String spiegeln */ public void mirror() throws RemoteException; /* String Laenge berechnen */ public int laenge() throws RemoteException; /* Inhalt des Strings zurueckgeben */ public String getInhalt() throws RemoteException; /* Name des Strings zurueckgeben */ public String getName() throws RemoteException;

}

ZKmanager Implementierung: import java.rmi.NotBoundException; import java.rmi.RemoteException; import java.rmi.registry.Registry; import java.rmi.server.UnicastRemoteObject; import java.util.HashMap; import java.util.Map; import RMI.TextObject; import RMI.ZKManager;

13

public class ZKManagerImpl implements ZKManager { private static final long serialVersionUID = 69412 15904200499870L; private Map<String, TextObject> objectList = new H ashMap<String, TextObject>(); private Registry registry; public ZKManagerImpl() { super(); } @Override public TextObject createTextObject(String name, St ring content) { /*Objekt der Liste übergeben.*/ TextObject result = new TextObjectImpl(name, cont ent); this.objectList.put(name, result); return result; } @Override public void deleteTextObject(String which) throws RemoteException { this.objectList.remove(which); } @Override public TextObject openTextObject(String which)thro ws RemoteException{ /*Objekt aus der Liste auslesen.*/ return objectList.get(which); } @Override public void exportZK(String name) throws RemoteExc eption { String regName = "TextObject:" + name; TextObject ausgabe = objectList.get(name); /*Exports the remote object to make it available to receive

incoming calls using the particular supplied port(0 ).*/ TextObject stub =(TextObject) UnicastRemoteObject.exportObject(ausgabe, 0);

/*Replaces the binding for the specified name in this registry

with the supplied remote reference.*/ registry.rebind(regName, stub); } @Override public TextObject importZK(String name) throws Rem oteException { String regName = "TextObject:" + name; TextObject tObj = null; try { /*Returns the remote reference bound to the spec ified

name in this registry.*/

14

tObj = (TextObject) registry.lookup(regName); } catch (NotBoundException e) { e.printStackTrace(); } return tObj; } public void setReg2(Registry registry) { this.registry = registry; } }

Zeichenkette Implementierung: import RMI.TextObject; public class TextObjectImpl implements TextObject { private static final long serialVersionUID = 54218 00288095467931L; private String name; private String inhalt; /*Konstruktor*/ public TextObjectImpl(String n, String i){ this.name = n; this.inhalt = i; } /* Methoden aus TextObject implementieren.*/ @Override public String getInhalt() {

System.out.println("\n(Server)Inhalt: '" + this.inh alt + "' an Client gesendet");

return this.inhalt; } @Override public String getName(){

System.out.println("\n(Server)name '" + this.name + "' an Client gesendet");

return this.name; } @Override public void initText(String initInhalt) {

System.out.println("\n(Server)Iniziiere Inhalt mit '" + initInhalt + "'");

this.inhalt = initInhalt; }

15

@Override public void signs() { String result = ""; for(int counter=0; counter<inhalt.length(); count er++){ /* Schneidet dem String ein Teil ab. */ result += inhalt.substring(counter, counter+1) + inhalt.substring(counter, counter+1); } System.out.println("\n(Server)" + this.inhalt + " Zeichenweise

verdoppelt zu '" + result + "'"); this.inhalt = result; } @Override public void dublicate() {

System.out.print("\n(Server)" + this.inhalt + " Zei chenkette verdoppelt zu '");

String result = this.inhalt + this.inhalt; System.out.println(result + "'"); this.inhalt = result; } @Override public int laenge() {

System.out.println("\n(Server)" + this.inhalt + " L aenge: " + this.inhalt.length());

return this.inhalt.length(); } @Override public void mirror() { String result = ""; for(int counter = inhalt.length(); counter > 0; c ounter--){ /*Schneidet dem String ein Teil ab in entgegenge setzter

Richtung.*/ result += inhalt.substring(counter-1, counter); } System.out.println("\n(Server)" + this.inhalt + " Zeichenweise

gespiegelt zu '" + result + "'"); this.inhalt = result; } }

Implementierung von Server: import java.io.IOException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.UnicastRemoteObject; import RMI.ZKManager; public class MyServer {

16

public static void main(String[] args) { ZKManagerImpl zkM = new ZKManagerImpl(); try { String name = "derZKManager"; /*Create and exports a Registry instance on the local host

that accepts requests on the specified port.*/ /*Registry über RMI starten.*/

Registry registry = LocateRegistry.createRegistry(Registry.REGISTRY_POR T);

/*Remote Objekt exportiert sich selbst ü ber den

Konstruktor.*/ /*Export the remote object to make it av ailable to receive

incoming calls using the particular supplied port(0 ).*/ ZKManager stub =(ZKManager) UnicastRemoteObject.exportObject(zkM, 0);

/*Bindet das Objekt an den Namen name un d trägt es so in

der Registrierung ein. */ registry.rebind(name, stub); System.out.println("ZKManager bound, Die nstname: " + name); /*Registry dem ZKManager zuweisen.*/ zkM.setReg2(registry); } catch (Exception e) { System.err.println("ZKManager exception: "); e.printStackTrace(); } System.out.println("starten des Dienstes..." ); try { /*Programm bleibt solange angehalten bis D aten über die

Pipeline gesendet werden. Falls Daten gesendet we rden wird der erste Byte-Block als int and das Programm gegeben.*/

int r = System.in.read(); System.out.println(r); } catch (IOException e) { e.printStackTrace(); } } }

Implimentierung von Client: import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.Scanner; import java.util.StringTokenizer;

17

import RMI.TextObject; import RMI.ZKManager; public class MyClient { public static void main(String[] args) { /*Einlesen von Eingaben von der Tastatur.*/ Scanner ein= new Scanner(System.in); TextObject c = null; try { /*Bei der Registry eingetragener Name des Servers*/ String name = "derZKManager"; /*Registry suchen und sich mit der Reg istry verbinden*/ /*Return a reference to the the remote object Registry

for the local host on the specified port.*/ Registry registry = LocateRegistry.getRegistry("localhost", Registry.RE GISTRY_PORT); /*Nach Servernamen in der Registry suc hen. Returns the remote reference bound to the specified name

in this registry.*/ ZKManager zkM = (ZKManager) registry.l ookup(name); int eingabe=-1; while (eingabe!=0){ /*Auswahl - Menu*/ System.out.print("Was wollen Sie machen?\n" +" (0) Beenden\n" +" (1) Zeichenkettenobjekt erstellen\n" +" (2) Inhalt des Zeichenkettenobjektes byteweise verdoppeln\n" +" (3) Inhalt des Zeichenkettenobjektes verdoppeln\n" +" (4) Inhalt des Zeichenkettenobjektes spiegeln\n" +" (5) Laenge des Zeichenkettenobjektes ermitteln\n" +" (6) Zeichenkettenobjekt loeschen\n" +" (7) Zeichenkettenobjekt exportieren\n" +" (8) Zeichenkettenobjekt in der Registry nachschlagen\n" +" Ihre Wahl: "); eingabe = ein.nextInt(); /*Hier werden Zugriffe auf die einzelnen Me thoden

durchgeführt. */ switch(eingabe) { case 1: {

18

System.out.print("\n\n(Client)Geben Sie Namen.Inha lt ein: ");

String initString = ein.nextLine();

initString = ein.nextLine(); StringTokenizer strTok = new StringTokenizer(initString, "."); String initName =

strTok.nextToken(); String initInhalt =

strTok.nextToken(); /*ZKManager in Anspruch nehmen um

neue Objekte zu erzeugen und auf Methoden zugreifen.*/ c = zkM.createTextObject(initName,

initInhalt); System.out.println("(Client)ZK-Obje kt mit Namen: '" + initName + "' und Inhalt: '" + init Inhalt + "' wurde erstellt.\n\n"); }break; case 2: { c.signs(); System.out.println("\n(Client)Inhal t byteweise verdoppelt: " + c.getInhalt() + "\n"); }break; case 3: { c.dublicate(); System.out.println("\n(Client)Inhal t verdoppelt: " + c.getInhalt() + "\n"); }break; case 4: { c.mirror(); System.out.println("\n(Client)Inhal t gespiegelt: " + c.getInhalt() + "\n"); }break; case 5: { System.out.println("\n(Client)Laeng e ermittelt: " + c.laenge() + "\n"); }break; case 6: { System.out.print("\n(Client)Geben Sie Namen des Objektes ein: "); String delName = ein.nextLine(); delName = ein.nextLine();

19

zkM.deleteTextObject(delName); System.out.println("\n(Client)Objek t: " + delName + " wurde geloescht."); }break; case 7: { System.out.print("\n(Client)Geben Sie Namen des zu exportierenden Objektes ein: "); String expName = ein.nextLine(); expName = ein.nextLine(); zkM.exportZK(expName); System.out.println("\n(Client)Exportiert: '" + c.ge tName() + "'\n"); }break; case 8: { System.out.print("\n(Client)Geben Sie Namen des zu importierenden Objektes ein: "); String impName = ein.nextLine(); impName = ein.nextLine(); c = zkM.importZK(impName); System.out.println("\n(Client)Importiert: '" + c.ge tName() + "'\n"); }break; default: } } } /*Fehler abfangen und ausgeben.*/ catch (Exception e) { e.printStackTrace(); } } }

20

2 Java Native Interface

2.1 Was ist JNI?

Das Java Native Interface (JNI) ist ein Teil des JDK und ermöglicht die Kooperation von Java-Programmen mit Applikationenen und Libraries anderer Programmiersprachen (C/C++). Umgekehrt erlaubt das Invocation API aus beliebigen C oder C++ Programmen Java -Programme (bzw. die Virtual Machine)aufzurufen.

2.2 Wozu JNI?

Man benötigt Plattform-spezifische Features, die nicht durch die vorhandenen JAVA-Klassen bereitgestellt werden. Z.B.:

Zugriff auf einen Bandroboter über die SCSI-Schnittstelle um eine grafische Oberfläche zur Bedienung des Roboters zu entwickeln.

Es ist eine Bibliothek mit Routinen vorhanden, die nicht in JAVA programmiert wurden. Z.B.:

Ausnutzung einer speziellen Hardware für numerische Berechnungen.

Ein kleiner zeitkritischer Programmabschnitt soll in einer hardwarenahen Sprache programmiert werden, um das Programm zu beschleunigen. Z.B.:

Textsuche in einem Editor Skalarprodukt in einem numerischen Programm

Dreiecksberechnung in einem FEM-Code

2.3 Vorgehensweise/ Beispiel

Wie ist das Ganze nun auf das obligatorische "HelloWorld" anzuwenden ?

class helloworld { private native void callnative(); public static void main(String[] args) { new helloworld().callnative(); } static { System.loadLibrary("hello"); } }

21

[Listing 1] helloworld.java

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

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

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

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

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

javah -jni HelloWorld

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

JNIEXPORT void JNICALL Java_Helloworld_callnative(JNIEnv *, jobject);

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

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

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

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

#include <jni.h> #include <stdio.h> #include "helloworld.h" JNIEXPORT void JNICALL Java_helloworld_callnative(JNIEnv *env, jobject obj) {

22

printf("HelloWorld\n"); return; }

[Listing 2] hello.c

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

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

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

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

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

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

Die Includepfade sind ggf. anzupassen.

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

LD_LIBRARY_PATH=. export LD_LIBRARY_PATH

neu zu setzen.

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

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

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

23

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

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

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

Parameterübergabe

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

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

class classtwo { static int iii = 4; static float fff = (float)2.50; static double ddd = 0.10; static String sss = "Vor der Berechnung von"; private static native double jnipower(int in_iii, String in_sss, float in_fff); public static void main(String[] args) { double res = jnipower(iii,sss,fff); System.out.println("Ergebnis des JNI Aufrufs=" + res); } static { System.loadLibrary("two"); } }

[Listing 3] classtwo.java

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

24

2.4 Datentypen

Java Datentyp JNI Datentyp

byte jbyte

short jshort

int jint

long jlong

float jfloat

double jdouble

char jchar

boolean jboolean

Object jobject

void void

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

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

25

2.5 Signaturen

TypeSignature JavaType

Z boolean

B byte

C char

S short

I int

J long

F float

D double

V void

L<vollständiger Klassenname>

Klasse

[<type> Array

(<Parameter>) <Rückgabewert>

Methode

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

Beispiel:

(IJDF)Z ist die Signatur einer Methode, welche vier Argumente

( int, long, double, float) hat und einen boole‘schen Wert zurückgibt

26

2.6 Strings

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

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

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

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

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

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

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

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