Masterthesis Entwicklung eines Automotive Embedded Java Frameworks Enrico Bedau Matrikel-Nr.: 700724 27. Januar 2005 Referent: Prof. Dr. Joachim Wietzke (FH-Darmstadt) Korreferent: Prof. Dr. Thomas Horsch (FH-Darmstadt) Inhaltsverzeichnis 1 Einführung 6 1.1 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 1.2 Zielsetzung 6 1.3 Entwicklung im Automotive Embedded Bereich . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Analyse eines C++ Frameworks 7 8 2.1 Aufgaben eines Frameworks . . . . . . . . . . . . . . . . . . . . . . . 8 2.2 Architektur und Funktionsweise . . . . . . . . . . . . . . . . . . . . . 9 2.3 Verwendete Möglichkeiten der Interprocesscommunication (IPC) . . . 11 2.4 Ablauf der Initialisierungsphase . . . . . . . . . . . . . . . . . . . . . 12 2.5 Verwaltung von Speicher und Objekten . . . . . . . . . . . . . . . . . 12 2.6 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 3 Das Framework Design 3.1 3.2 14 Vergleich möglicher Design Patterns . . . . . . . . . . . . . . . . . . . 14 3.1.1 Task Management . . . . . . . . . . . . . . . . . . . . . . . . 14 3.1.2 Speicherverwaltung . . . . . . . . . . . . . . . . . . . . . . . . 15 . . . . . . . . . . . . . . . . . . . . . 16 3.2.1 Java spezische Besonderheiten Java-IPC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 3.2.2 Garbage Collection . . . . . . . . . . . . . . . . . . . . . . . . 18 3.2.3 Weitere Einschränkungen gegenüber C++ 20 . . . . . . . . . . . 4 Grundlagen für die Entwicklung des Frameworks 4.1 Die Entwicklungsumgebung 4.2 Zugri auf native Bibliotheken über JNI 26 . . . . . . . . . . . . . . . . . . . . . . . 27 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 4.2.1 Warum JNI 4.2.2 Ausführen von C++ Code aus Java-Programmen . . . . . . . 28 4.2.3 Entwickeln einer Shared-Library . . . . . . . . . . . . . . . . . 28 5 Entwicklung des Frameworks 5.1 26 . . . . . . . . . . . . . . . . 31 HMI Prototyp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 5.1.1 Aufteilung des Frameworks . . . . . . . . . . . . . . . . . . . . 31 5.1.2 Schnittstelle zwischen C++ und Java Framework . . . . . . . 32 5.1.3 Implementierung der HMI-Komponente . . . . . . . . . . . . . 37 2 5.2 Das Java Framework . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 5.2.1 Ausführen der Komponenten . . . . . . . . . . . . . . . . . . . 38 5.2.2 Verwalten des Speichers . . . . . . . . . . . . . . . . . . . . . 40 . . . . . . . . . . . . . . . . . . . . . . . . . . 41 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 5.3 Die HMI-Komponente 5.4 JNI Module 5.5 5.4.1 Input Schnittstelle . . . . . . . . . . . . . . . . . . . . . . . . 41 5.4.2 Mostgateway 5.4.3 . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 CD-Player . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 Grasche Oberächen unter Java . . . . . . . . . . . . . . . . . . . . 43 5.5.1 Auswahl eines Toolkits . . . . . . . . . . . . . . . . . . . . . . 43 5.5.2 SWT-Programmierung . . . . . . . . . . . . . . . . . . . . . . 44 6 Vergleiche der Frameworks 6.1 46 Performance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 6.1.1 Leistungsfähigkeit der JNI-Schnittstelle . . . . . . . . . . . . . 46 6.1.2 Startverhalten . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 6.1.3 Reaktionszeiten & Antwortverhalten 50 7 Zusammenfassung & Ausblick . . . . . . . . . . . . . . 51 Abbildungsverzeichnis 2.1 Aufbau des Frameworks . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Aufbau einer Komponente . . . . . . . . . . . . . . . . . . . . . . . . 4 10 11 Listingverzeichnis 3.1 struct-Anweisung aus CMessage.cpp . . . . . . . . . . . . . . . . . . . 21 3.2 Struct-Anweisung in Javaklasse überführt . . . . . . . . . . . . . . . . 21 3.3 Union: AppMessage und MostMessage . . . . . . . . . . . . . . . . . 22 3.4 Union: Raw Daten und beschriebene Daten . . . . . . . . . . . . . . . 22 3.5 Ausschnitt aus der Klasse CMessage . . . . . . . . . . . . . . . . . . 22 3.6 Objektübergabe per Kopie der Referenz . . . . . . . . . . . . . . . . . 24 3.7 Objektübergabe per Referenz . . . . . . . . . . . . . . . . . . . . . . 25 4.1 Java-JNI-Klasse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 5.1 JNI-Klasse CMessagehandler . . . . . . . . . . . . . . . . . . . . . . . 33 5.2 Aufrufe wie diese wurden entfernt . . . . . . . . . . . . . . . . . . . . 34 5.3 Die init-Funktion der JNI-Library . . . . . . . . . . . . . . . . . . . . 34 5.4 Die getMessage-Funktion der JNI-Library . . . . . . . . . . . . . . . 35 5.5 Beispiel Implementierung des Runnable Interfaces . . . . . . . . . . . 39 5.6 Anlegen und Starten einer Komponente . . . . . . . . . . . . . . . . . 39 5.7 Wiederverwenden von Speicher in C++ . . . . . . . . . . . . . . . . . 40 5.8 Auszug aus der Most-JNI-Library . . . . . . . . . . . . . . . . . . . . 42 5.9 Ausführen von Code im UI-Thread . . . . . . . . . . . . . . . . . . . 45 6.1 Testfall 1: Aufrufen einer JNI-Funktion . . . . . . . . . . . . . . . . . 47 6.2 Testfall 1: Aufrufen einer Java-Funktion . . . . . . . . . . . . . . . . 47 6.3 Testfall 1: Rückgabe einer Long-Variablen an Java . . . . . . . . . . . 48 5 1 Einführung 1.1 Motivation Im Automotive Bereich ist der Druck auf die Software Entwickler sehr hoch. Den immer kürzer werdenden Releasezyklen bei den Automobilen, stehen ständig wachsende Softwareprojekte gegenüber. Um bei dieser Entwicklung schritt halten zu können, müssen die Entwicklungszyklen kürzer werden. Für viele erfolgreich abgeschlossenen Projekte im Automotive Bereich wurde hauptsächlich die Programmiersprache C eingesetzt und neue Projekte werden zu einem groÿen Teil in C++ entwickelt. Diese Sprachen sind dafür bekannt, ein sehr schnelles Programm zu ermöglichen. Jedoch begünstigen sie auch Programmierfehler, welche vor allem auf Embedded Systemen nur schwer zu entdecken sind. Hier soll Java Abhilfe schaen. Eine der groÿen Stärken von Java liegt darin, viele der von C und C++ bekannten fehleranfälligen Programmiertechniken nicht zu zulassen. Schon beim Kompilieren werden weitaus mehr Fehler gefunden, als dies bei einem C/C++-Compiler der Fall wäre. Im Rahmen dieser Arbeit galt es zu untersuchen, in wie weit eine Java Entwicklung in der Lage ist, C und C++ Software auf einem Embedded System zu ersetzen. 1.2 Zielsetzung Das Ziel dieser Arbeit bestand darin, Probleme und Risiken zu identizieren und einen Vergleich zwischen einer C++- und einer Java-Software zu ermöglichen. Um einen solchen Vergleich anstellen zu können, wurde auf der Grundlage eines bekannten C++-Frameworks, ein Java Framework entwickelt, welches in der Funktionalität vergleichbar ist. Dabei galt es die Probleme bei einem Umstieg von C++, im Hinblick auf die beschränkten Ressourcen eines Embedded Systems, zu identizieren und wenn möglich, Lösungswege aufzuzeigen. 6 Das Java Framework sollte am Ende der Entwicklung in der Lage sein, eine Audio CD auf einem Embedded System abzuspielen. Ein Vergleich zwischen dem C++- und dem Java-Framework soll zeigen, ob Java den strengen Anforderungen im Automotive Bereich genügen kann. 1.3 Entwicklung im Automotive Embedded Bereich An Software im Automotive Bereich werden sehr hohe Anfordungen gestellt. Da es sich hier zum Teil um Unterhaltungselektronik handelt, erwartet der Kunde von einem Gerät mindestens genau so viel, wie einer Hi-Stereo-Anlage. Kleinste Verzögerungen in der Bedienung oder bei der Anzeige hinterlassen einen schlechten Eindruck von dem Produkt. Es wirkt unfertig und unprofessionell. In der Automobilbrange steht die Sicherheit an erster Stelle. Ein zu spät reagierender Abstandswarner oder eine fehlerhafte Software, welche durch unerwartetes Verhalten auf sich aufmerksam macht, könnte reale Folgen nach sich ziehen. Daher ist auch Elektronik, welche das Fahrzeug nicht direkt beeinusst Sicherheitsrelevant. Die Funktionen eines solchen Gerätes müssen also schnell verfügbar sein und immer gleich reagieren. Eine weitere Besonderheit in dieser Umgebung liegt darin, dass ein spät entdeckter Fehler nur sehr schwer korrigierbar ist. Es ist unter Umständen nötig, tausende Fahrzeuge zurück ins Werk zu rufen. Aber auch in der Testphase ist es schwierig, eine Fehler zurück zu verfolgen. 7 2 Analyse eines C++ Frameworks Im Automotive Bereich wurden schon verschiedene Frameworks entwickelt, welche hauptsächlich in C oder C++ programmiert wurden. Die Erfahrungen aus einem solchen Projekt sollte in die Entwicklung des Java-Frameworks mit einieÿen. Aus diesem Grund wurde erst ein bestehendes Framework analysiert, bevor mit der Entwicklung des Java-Frameworks begonnen wurde. Dabei handelt es sich um ein C++-Framework, da C++ genau wie Java eine objektorientierte Sprache ist und dadurch leichter mit Java verglichen werden kann. Das Projekt begann im Jahre 2003 und wird in an der FH-Darmstadt durchgeführt. Da dieses Framework noch in der Entwicklung ist, basiert diese Analyse hauptsächlich auf dem Stand von November 2004. 2.1 Aufgaben eines Frameworks Die Entwicklung eines Frameworks verfolgt mehrere Ziele, welche alle darauf abzielen, Software kostengünstiger erstellen zu können. Dabei ieÿen verschiedene Konzepte in die Entwicklung ein, welche im Folgenden kurz vorgestellt werden. Wiederverwendbarkeit Ein Framework basiert auf einem abstrakten Design, das allgemein genug ist, um in verschiedenen Softwareentwicklungen zum Einsatz zu kommen. Jedoch muss es auch speziell genug sein, um die Entwicklung wirklich zu vereinfachen, denn ein rein abstraktes Framework würde keine konkreten Methoden bereitstellen, welche die Entwicklung beschleunigen könnte. Das Abwägen zwischen allgemeineren oder konkreten Ansätzen ist eine Hauptaufgabe beim Design eines Frameworks. Richtlinien für die Entwickler Wie der Name Framework schon andeutet, sollte es den Entwicklern einen Rahmen für ihre Arbeit vorgeben. Das bedeutet, ein Framework stellt Richtlinien und Anweisungen zur Verfügung und forciert ihre Einhaltung wenn immer 8 dies sinnvoll möglich ist. Dadurch wird ein einheitlicher Programmierstil festgelegt, welcher die Software später leichter lesbar macht. Auch wird hierdurch die Einarbeitung neuer Mitarbeiter erheblich beschleunigt. Modularer Aufbau Wie es in der Softwareentwicklung schon seit langem Standard ist und auch von objektorientierten Sprachen gefördert wird, so ist auch ein Framework modular aufgebaut. Die Vorteile in Bezug auf Design, Implementierung, Testen und Wartung sind zum Beispiel die hohe Parallelisierbarkeit der Entwicklungsarbeit und die gröÿere Übersichtlichkeit der Software. Die Vorteile sind im Einzelnen jedoch sehr vielfältig und können zum Beispiel in [Bal00] nachgelesen werden. Das Framework deniert, wie die Module aufgebaut sind und wie diese untereinander kommunizieren, in dem es Schnittstellen und abstrakte Klassen bereitstellt. Bereitstellen von Standardfunktionen Immer wiederkehrende Funktionen werden von einem Framework in Form von Bibliotheken bereitgestellt. 2.2 Architektur und Funktionsweise Das Framework teilt die Module in vier Haupttypen auf. Dies sind die HMI (Human Machine Interface), der Main Dispatcher, die Adminkomponente und Devices. Die HMI, die Adminkomponente und der Main Dispatcher kommen im gesamten System nur jeweils einmal vor, wogegen es von den Devices normalerweise mehrere gibt. Main Dispatcher Der ist das Herzstück des Frameworks und für die Koordination der Kommunikation zwischen den einzelnen Modulen verantwortlich. Jegliche Kommunikation zwischen den Komponenten sollte über den Maindispatcher erfolgen. Jedoch wurde diese Designprinzip im Framework teilweise nicht beachtet, da durch die Art der Implementierung Nachrichten auch direkt beim Empfänger abgelegt werden können. Die HMI zer und der Maschine. Sie ist somit für das Darstellen der Inhalte, sowie für das 1 ist wie der Name es bereits sagt, die Schnittstelle zwischen dem Benut- Verarbeiten der Benutzereingaben zuständig und hat auch die Funktion eines Main Controllers. Die 1 In Adminkomponente ist für das Verwalten der einzelnen Komponenten zuständig. dieser Arbeit ist von Benutzer die Rede, wenn der Endbenutzer der fertigen Software gemeint ist 9 Human Machine Interface Gerät 1 Komponente Gerät 2 Main− Dispatcher Gerät X Administrator Komponente Abbildung 2.1: Aufbau des Frameworks Dies ist beim aktuellen Stand des Frameworks nur das Erstellen, Initialisieren, Starten und Stoppen. Geplant, aber zum Zeitpunkt dieser Arbeit noch nicht implementiert ist auch die Möglichkeit des Pausierens und Wiedererwecken, sowie das Überwachen der Komponenten. Bei den Devices kann es sich um alle Arten von Geräten handeln. Dies können zum Beispiel Controller für reale Geräte wie zum Beispiel DVD-Player oder Radio sein, aber auch logische Geräte, welche anderen Geräten höhere Funktionen zur Verfügung stellen. Beschreibung der Basis-Komponenten 2 Eine jede Komponente, auÿer der Adminkomponente stellt dem User verschiedene Eigenschaften, Funktionen und Objekte zur Verfügung. Für das Eventhandling stehen jeder Komponente zwei Queues zur Verfügung, in denen eingehende Nachrichten für die Bearbeitung zwischengespeichert werden. Die erste Queue ist die Systemqueue und für Nachrichten mit hoher Priorität reserviert. Diese Queue wird immer als erstes abgefragt. Die normalen Nachrichten werden in der Normalqueue gespeichert. 2 In dieser Arbeit ist von User die Rede, wenn der Benutzer des Frameworks, bzw. der Entwickler der eigentlichen Software gemeint ist 10 Nachrichten Controller Systemqueue Dispatcher Normalqueue Logische Gerät (LogDev) Abbildung 2.2: Aufbau einer Komponente Im Normalfall ist eine Komponente aus drei Teilen aufgebaut. Dies ist zum einen das logische Gerät (LogDev), welches die logischen Funktionen eines Gerätes für die anderen Komponenten bereitstellt. Es kann also eine standardisiere Schnittstelle zu gleichartigen Geräten, wie zum Beispiel verschiedenen CD-Playern darstellen oder als Proxy für entfernte Geräte dienen. Der Controller (ComponentController) ist für die interne Logik, der eigentlichen Programmlogik verantwortlich. Das Zustellen der Nachrichten zu diesen beiden Komponenten ist die Aufgabe des Dispatchers, welcher die Nachricht aus den beiden Queues entgegen nimmt und in Abhängigkeit vom Inhalt der Nachricht zustellt. 2.3 Verwendete Möglichkeiten der Interprocesscommunication (IPC) Im C++-Framework wird jede Komponente als eigener Prozess durch Fork-Aufrufe erzeugt. Dadurch wird der Vater-Prozess kopiert und ein neuer Sohn-Prozess gestartet. Dadurch erbt der Sohnprozess alle Daten des Vaterprozesses, wodurch bereits initialisierte Variablen und Objekte weiterverwendet werden können. Im Gegensatz dazu muss der eigentliche Programmcode nicht kopiert werden, wodurch Speicherplatz gespart wird und dennoch der Code als gesamtes zur Verfügung steht (siehe [QNX04]). 11 Durch das Aufteilen der Komponenten in Prozesse erhält jeder Prozess einen eigenen zugrisgeschützten Datenbereich. Dies hat den Vorteil, dass Speicherzugrisfehler sich nicht auf andere Speicherbereiche auswirken können und schnell erkannt werden. Jedoch verhindert dies auch den Zugri auf gemeinsame Daten. Für die Kommunikation zwischen den Prozessen verwendet das Framework Shared Memory (siehe auch Literatursec:SharedMemory), da sich diese Form des Datenaustausches in Tests als die Schnellste herausgestellt hat. Mithilfe von Mutexen aus der Posix Thread Implementierung werden die gemeinsamen Daten gegen gleichzeitigen Zugri geschützt. 2.4 Ablauf der Initialisierungsphase Die Initialisierungs- bzw. Startphase des Frameworks läuft in drei Stufen ab. In der ersten Stufe werden die Kontexte für alle Komponenten angelegt und initialisiert. Dies beinhaltet in diesem Fall auch das Anlegen der Nachrichtenqueues aller Komponenten. In der zweiten Stufe werden nacheinander alle Komponenten angelegt und initialisiert. Dabei wird der Komponente ihr jeweiliger Kontext übergeben, der Controller und das LogDev angelegt und die Standardinitialisierung über die Konstruktoren durchgeführt. In der dritten Stufe werden die Prozesse nacheinander über ihre Init-Funktion initialisiert, geforked und anschlieÿend über ihre Run-Funktion gestartet. 2.5 Verwaltung von Speicher und Objekten Das Framework implementiert keine Technik zum globalen Verwalten von Speicher. Der Speicher wird immer dann Allociert, wenn er benötigt wird. Jedoch werden langsame Funktionen, wie das Kopieren von Objekten nur dann eingesetzt, wenn es sich nicht vermeiden lässt. Die Nachrichtenqueues bilden dabei eine Ausnahme. Sie werden beim Starten des Frameworks angelegt und bleiben während der gesamten Laufzeit erhalten. Da sie im Shared Memory abgelegt werden, hat jeder Prozess Zugri auf alle Nachrichtenqueues. 12 2.6 Zusammenfassung Das Framework vermeidet wenn immer es möglich ist, dass Daten kopiert werden, in dem hauptsächlich Zeiger und Referenzen verwendet werden. Lässt sich das Umkopieren von Daten nicht vermeiden, so werden eektive Funktionen, wie zum Beispiel memcopy verwendet. Bei der Analyse des Frameworks ist auch aufgefallen, das die Informationen zu einer Komponente relativ Verstreut sind und dadurch etwas an Übersichtlichkeit verloren geht. Möchte man zum Beispiel eine Komponente zum Framework hinzufügen, so muss man dazu an ca. 10 Stellen in 4 verschiedenen Dateien Änderungen vornehmen. Auch sind einige der Daten redundant. 13 3 Das Framework Design Wie der Name Framework schon andeutet, stellt es einen Rahmen zur Verfügung in dem ein System abläuft. Die Aufgabe eines Framework besteht darin, denierte und stabile Schnittstellen, zu oft benötigten Funktionen, zur Verfügung zu stellen. Im folgenden wird dargestellt, welche Richtlinien bei der Erstellung des Java Frameworks eine Rolle gespielt haben. 3.1 Vergleich möglicher Design Patterns Die wichtigsten Standardaufgaben eines Frameworks bestehen darin den Speicher zu verwalten, die Kommunikation zwischen den einzelnen Teilsystemen sicher zu stellen und den Lebenszyklus der Teilsysteme zu kontrollieren. In diesem Kapitel werden verschieden Design Patterns vorgestellt, welche für diese Aufgaben in Frage kommen. 3.1.1 Task Management Um das Management von Tasks zu beschreiben, sollte zu erst der Begri Task'nnäher erläutert werden. Unter Task verstehen wir im Hinblick auf ein Framework eine Aufgabe oder ein Teilsystem, welches parallel zu anderen Tasks ausgeführt wird. Bei einem Task kann es sich um einen Thread oder einen Prozess handeln, wobei sich ein Framework typischerweise auf eines von beiden beschränkt. Um Verwirrungen zu vermeiden, wird in dieser Arbeit von Tasks gesprochen, wenn es sich um einen Prozess oder ein Thread handeln kann. Die Aufgabe des Frameworks ist es unter anderem, diese Tasks zu erzeugen und zu starten. Dies kann beinhalten, dass weiter für jeden Prozess notwendige Aufgaben, wie z.B. das Anlegen eines Kontextes, durchgeführt werden. Weitere Aufgaben die ein Framework durchführen kann, sind das Überwachen der Tasks und der kontrollierte Neustart im Falle eines Fehlers. Komplexere Frameworks erlauben auch, Tasks zu pausieren und zu einem späteren Zeitpunkt fortzusetzen. 14 3.1.2 Speicherverwaltung Eine wichtige und aufwendige Aufgabe eines Framework besteht darin den notwendigen Speicher für Objekte und Daten zur Verfügung zu stellen. Es gibt für diesen Zweck verschieden Ansätze, welche sich zum Beispiel hinsichtlich Geschwindigkeit und Speicherplatzbedarf unterscheiden. Im Folgenden werden einige dieser Verfahren vorgestellt, welche für den Einsatz auf einem embedded System in Frage kommen könnten. Dynamisches Allocieren und Deallocieren Beim dynamischen Allocieren wird der Speicher immer dann angelegt, wenn er gebraucht wird. Dies hat zur Folge, dass nicht benötigte Speicherbereiche wieder freigegeben werden müssen (deallocieren), wenn sie nicht weiter gebraucht werden. Die Vorteile dieses Verfahrens liegen zum Einen im geringen Speicherbedarf. Es wird immer nur soviel Speicher reserviert, wie auch benötigt wird. Zum anderen kann ein Programm das Reservieren des Speichers über die gesamte Laufzeit verteilen. Der Programmstart verzögert sich nicht, durch zusätzliches initialisieren von Speicher. Ein weiterer Vorteil ist der geringe Overhead, denn es sind keine globalen Strukturen zur Verwaltung des Speichers notwendig. Die dynamische Speicherverwaltung hat aber auch einige gravierende Nachteile. Dadurch, dass zu jeder Zeit Speicher allokiert werden kann, lässt sich oft nur schwer nachvollziehen, wann wieviel Speicher reserviert wurde. Dies kann die Fehlersuche erheblich erschweren. Viele Fehler beim Programmieren lassen sich auf vergessenes oder zum falschen Zeitpunkt durchgeführtes reservieren oder freigeben von Speicher zurückführen. Nicht durchgeführtes reservieren von Speicher fällt normalerweise schnell durch NullPointer-Exceptions auf. Jedoch fallen nicht wieder freigegeben Bereiche oft nicht auf und führen zu Memory Leaks. Dieser Fehler kann sich erst nach längerer Laufzeit bemerkbar machen und ist somit schwer zu nden. Jeder Vorgang, bei dem Speicher reserviert oder freigegeben wird, zwingt das Betriebssystem dazu, in den geschützten Kernel-Mode zu gehen. Da das Umschalten in den Kernel mode relativ zeitaufwendig ist, können viele dieser Vorgänge ein Programm stark ausbremsen. Dies trit besonders auf langsame Systeme zu. Einige der oben vorgestellten Nachteile der dynamischen Speicherverwaltung erschweren gröÿere Softwareprojekte erheblich. Aus diesem Grund wurden verschieden Verfahren entwickelt um diese zu umgehen. Dies ist zum Beispiel Objectpooling, welches nachfolgend näher erläutert wird. 15 Object Pooling Mit Hilfe des Object Pooling versucht man möglichst selten Speicher zu reservieren bzw. frei zugeben, um die oben genannten Nachteile zu minimieren. Die Idee dabei ist, nicht mehr benötigten Speicher nicht freizugeben, sondern wiederzuverwenden. Beim Object Pooling werden nicht benötigte Objekte in einem Pool vorgehalten. Wird ein neues Objekt benötigt, so kann es aus dem Pool entnommen werden und es muss nicht erst ein Speicherbereich allokiert werden. Sobald ein Objekt nicht mehr benötigt wird, kann es wieder in den Pool für freie Objekte übergeben werden. Ein Nachteil des Object Pooling ist, dass nicht jede Objektgröÿe vorgehalten kann. Man muss sich auf eine oder wenige Gröÿen beschränken. Dies kann bei Programmen, welche viele stark unterschiedliche Objektgröÿen benötigen, zu einem hohem Overhead führen. Viele Objekte werden nicht den gesamten ihnen zur Verfügung gestellten Speicher benötigen. Andererseits kann kein Objekt gröÿer sein als das gröÿte Speicherobjekt. Es gibt verschiedene Ansätze, welche man für das Anlegen der Objekte verfolgen kann. Dies kann zum Beispiel direkt für alle Objekte beim Programmstart erfolgen. Dadurch würde der Start länger dauern, jedoch das Programm danach schneller ablaufen. Es ist aber auch möglich nur bei einem leeren Objektpool neue Objekte anzulegen, wobei man meist mehrere Objekte anlegt, um diesen Vorgang zu optimieren. Dies würde die Zeit für das Speicherallokieren auf einen gröÿeren Zeitraum verteilen und den Programmstart beschleunigen. Das Anlegen von neuen Objekten lässt sich aber auch im ersten Fall oft nicht vermeiden, da nicht immer bekannt ist, wie hoch der maximale Speicherbedarf ist. Da das Object Pooling an einer zentralen Stelle, über eine fest denierte Schnittstelle durchgeführt werden muss, ist das Debugging oft einfacher, als bei der dynamischen Speicherverwaltung. 3.2 Java spezische Besonderheiten Da Java im Embedded Bereich immernoch eine besondere Stellung einnimmt, wird in diesem Abschnitt auf die Besonderheiten von Java eingegangen. Das wohl bedeutenste Merkmal von Java ist, dass das kompilierte Programm nicht direkt auf dem Prozessor ausgeführt werden kann, sondern unter einer virtuellen Maschine, der Java Virtual Machine (JVM) läuft. Dies führt zu einigen grundsätzlichen Unterschieden im Vergleich zu direkt ausführbaren Programmen. 16 3.2.1 Java-IPC Da Java eine plattformunabhängige Programmiersprache ist, kann man keine betriebssystem spezischen Mechanismen für die Kommunikation mit anderen Prozessen nutzen. Im folgenden werden die von Java unterstützten Methoden zur Inter Prozess Communication vorgestellt. Sockets Die eigentliche, von Sun für Java vorgesehene Methode zur IPC ist die Kommunikation über Network Sockets. Diese Methode funktioniert wie bei anderen Programmiersprachen auch und soll deshalb in dieser Arbeit nicht im Detail erläutert werden. Es gibt verschieden APIs, welche das Entwickeln von verteilten Anwendungen unter Java vereinfachen und auf Sockets aufsetzen. Dies sind zum Beispiel Remote Method Invocation (RMI) oder Soap und werden von Sun für IPC vorgeschlagen. Die Kommunikation über Network Sockets hat jedoch einen entscheidenden Nachteil. Sie ist verhältnismäÿig langsam. Pipes Eine weitere Methode, welche weniger Overhead produziert als Sockets, ist die Kommunikation über Pipes. Unter Java gibt es verschieden Möglichkeiten mit Hilfe von Pipes Daten zwischen Prozessen auszutauschen. 1. Wird das Java Programm innerhalb einer Pipe gestartet, so kann es mit Hilfe der System.in Funktion die Ausgaben des in der Pipe vorausgehenden Programmes einlesen, bzw. mit System.out dem nachfolgenden Programmdaten übergeben. Jedoch funktioniert diese Pipe nur in eine Richtung und ist somit als Interprozesskommunikation ungeeignet. 2. Wird ein Prozess durch die Funktion Runtime.getRuntime().exec() gestartet, so kann die Ausgabe dieses Prozesses mit Hilfe der Funktion getInputStream() gelesen werden. 3. Auf Posix kompatiblen Betriebssystemen ist es möglich Named Pipes zu nutzen. Dies Funktioniert, da solche Pipes gegenüber der Anwendung als normale 17 Dateien erscheinen und somit die Standard-Dateiklassen FileInputStream und FileOutputStream genutzt werden können. Das Anlegen einer Pipe ist unter Java leider nicht möglich, wodurch das Ausführen von externen Programme oder nativen Code über Java Native Interface (JNI) notwendig ist. Shared Memory Der direkte Zugri auf gemeinsamen Speicher ist unter Java aus verschiedenen Gründen unmöglich. Zum einen ist es unter Java weder möglich Shared Memory anzulegen, noch auf bereits angelegten Speicher zu zugreifen. Genau genommen gibt es in Java keine Funktion um auf Speicher zuzugreifen. Weder auf den eigenen, den der JVM oder anderer Bereiche. Da dies dem Grundgedanken einer Virtuellen Maschine auch zzuwiderlaufen würde, wird es solche Funktionen auch in Zukunft nicht geben. Verschieden Versuche, direkt Speicher aus dem Prozess der JVM auszulesen sind fehlgeschlagen. Es war zwar möglich, innerhalb eines Debuggers die Pattern eines in einer Javavariable gespeicherten Wertes wiederzunden, jedoch nur bei primitiven Typen wie einem Integer und an nicht vorhersagbarer AAdresse Auch ist es nicht ohne weiteres möglich den Speicher der JVM zu lesen, da es sich nicht um Shared Memory handelt und dieser Speicher somit geschützt ist. Man kann Shared Memory jedoch indirekt nutzen, in dem man das Java Native Interface (JNI) nutzt (siehe Literatursec:JNI). 3.2.2 Garbage Collection Ein Eigenschaft von Java, welche das Programmieren erleichtern soll, ist das Fehlen einer delete-Funktion. Es ist nicht möglich ein einmal angelegtes Objekt explizit zu löschen. Für das Löschen eines Objekts ist der Garbage Collector (GC) zuständig. Er wird jedoch nur ausgeführt, wenn ein neues Objekt angelegt werden soll, aber kein Platz auf dem Heap kein freier Platz mehr vorhanden ist. Der Garbage Collector (GC) gibt den Speicher eines Objekts wieder frei, wenn keine Referenz auf das Objekt mehr vorhanden ist. Um dies tun zu können, muss er alle Objekte auf noch vorhandene Referenzen untersuchen. Dies kann die Leistungsfähigkeit eines Systems beeinträchtigen und wird deshalb auch gern als Argument gegen Java aufgeführt. Um diesen Engpass zu beheben wurde in den letzten Jahren viel Entwicklungsarbeit in die GCs gesteckt. Moderne JVMs besitzen verschiedene Verfahren um zu verhin- 18 dern, das der GC die Performance eines Programmes negativ bebeeinusst Die Phasen der Garbage Collection Jede JVM implementiert den Garbage Collector etwas unterschiedlich. Das Grundkonzept ist aber bei nahezu allen gleich. Ist für ein neues Objekt nicht genügend Speicher auf dem Heap frei, übernimmt der Thread, welcher den Speicher angefordert hat die Kontrolle und führt den GC aus. Dazu werden alle Threads gestoppt und wird deshalb auch Stop-The-World Collection (STW) genannt. Die GC wird in drei Phasen ausgeführt, welche Mark, Sweep und Compaction heiÿen. Die Erste Phase ist die Mark Phase, in der alle erreichbaren Objekte markiert. Dazu wird auf dem Stack nach Objektreferenzen gesucht. Die gefundenen Objektreferenzen werden rekursiv nach weiteren Objekten durchsucht. Jeder so auf dem Heap gefundene Speicherbereich wird in einem sogenannten Markbitvektor als referenziert markiert. Objekte, welche nach dieser Phase nicht markiert sind, werden auch nicht mehr referenziert und können gelöscht werden. In der Sweep Phase werden wird nach Speicherbereichen gesucht, welche allociert sind, aber nicht markiert wurden. Diese Speicherbereiche werden dann als frei markiert. Nach der Sweep Phase ist der Speicher fragmentiert und wird in der Phase Compaction wieder defragmentiert. Diese Phase kostet sehr viel Zeit, da ein Objekt ver- schoben und danach alle Referenzen auf das neue Objekt geändert werden müssen. Aus diesem Grund wird die Compaction Phase nur ausgeführt, wenn nach der Sweep Phase immer noch nicht ausreichend Platz auf dem Heap ist. Ist nach der Compaction Phase der Platz auf dem Heap immer noch nicht ausreichend, so wird der Heap vergröÿert. Optimieren der Garbage Collection Ein Garbage Collection Lauf kann unter Umständen sehr viel Zeit in Anspruch nehmen. Man versucht also einen solchen GC-Lauf möglichst zu verhindern oder zu beschleunigen. Der erste Gedanke ist oft, die Garbage Collection auszuschalten und nur bei Bedarf selbst auszuführen. Es ist aber bei keiner der bekannten JVMs möglich, die GC auszuschalten. Auch die GC vorzeitig laufen zu lassen, wenn keine kritischen Operationen 19 anstehen, ist nicht empfehlenswert. Das Ausführen der Funktion System.gc() ist für solche Zwecke gedacht. Jedoch erzwingt diese Funktion einen GC-Lauf nicht, sondern empelt ihn nur. Auch wird beim Ausführen der GC nach diesem Funktionsaufruf immer die Compaction Phase ausgeführt, die sehr zeitaufwendig ist. Die Mark- und Sweep-Phasen sind sehr eektiv und stören den Ablauf eines Programmes normalerweise nicht. Die Compaction Phase sollte jedoch auf jeden Fall vermieden werden. Diese Phase kann auftreten, wenn sehr groÿe Objekte oder Arrays erstellt werden. Es kann also von Vorteil sein diese aufzusplitten. Besitzt ein Objekt, welches bei einem GC-Lauf gelöscht werden soll eine FinalizeMethode, so wird diese ausgeführt. Dies verzögert die GC und sollte deshalb vermieden werden (siehe [Bor04]). Neuere JVMs wie IBMs VM ab Version 1.3.1 oder Suns VM ab Version 1.4 besitzen zwei neue Garbage Collector Methoden, welche die Verzögerung bei der GC verringern können. Der Parallel Collector führt pro vorhandenen Prozessor einen GC Thread aus. Dadurch kann die GC auf Mehrprozessorsystemen erheblich beschleunigt werden. Da ein Embedded System im Normalfall nur einen Prozessor hat, ist dieser Collector dort nicht von nutzen. Der Concurrent Collector von Suns VM kann einen Groÿteil der Mark und Sweep Phasen parallel zu den anderen Threads ausführen und muss nur am Beginn und am Ende der Mark-Phase eine Stop-The-World Phase ausführen. Dadurch werden die Phasen, in denen das System steht, sehr klein und behindern das System nicht. IBMs VM kennt einen Concurrent Mark, welcher während das System läuft, nach unreferenzierten Objekten sucht und dadurch eine Mark Phase bei einem GC überüssig macht. Werden beim Starten eines Java Programmes viele Objekte angelegt, kann die Startzeit verringert werden, in dem die minimale Heapgröÿe erhöht wird. Die minimale Heapgröÿe der für diese Frameworkentwicklung benutzten JVM ist zum Beispiel 1MB und kann mit dem Parameter -Xms4mäuf 4 MB vergröÿert werden. 3.2.3 Weitere Einschränkungen gegenüber C++ Union & Stucts Eine in C und C++ sehr beliebte Möglichkeit, unterschiedliche Sichten auf ein und dieselben Daten zu erhalten, sind Union- und Struct-Anweisungen. Diese Anweisungen gibt es in Java jedoch nicht. Am Beispiel von CMessage aus dem C++-Framework werden in diesem Kapitel einige mögliche Wege beschrieben. 20 Um eine Struct-Anweisung zu ersetzen, gibt zwei Möglichkeiten. Möchte man sehr nahe an der Funktionalität einer Struct-Anweisung bleiben, müÿte man für jeden Struct eine Klasse erstellen. Eine Klasse ohne Memberfunktionen verhält sich genau wie ein Struct (siehe 3.2). Dies hat jedoch den Nachteil, das sehr viele neue Klassen entstehen würden. Da sich in Java auch Klassen in andere Klassen einbetten lassen, würde es an der Lesbarkeit des Source-Codes nichts ändern. struct AppMessage { unsigned char senderType ; Int32 senderID ; Int32 receiverID ; Int32 opcode ; Int32 parameter1 ; Int32 parameter2 ; Int32 parameter3 ; Int8 parameter4 [39]; }; Listing 3.1: struct-Anweisung aus CMessage.cpp class AppMessage { char senderType ; int senderID ; int receiverID ; int opcode ; int parameter1 ; int parameter2 ; int parameter3 ; byte parameter4 [] = new byte [39]; }; Listing 3.2: Struct-Anweisung in Javaklasse überführt Eine andere Möglichkeit besteht in diesem Fall darin, die einzelnen Variablen des Structs direkt als MemberVariable in die Klasse CMessage aufzunehmen. Durch den Zugri über Memberfunktionen kann dann die Zuordnung zu den einzelnen denierten Typen hergestellt werden (siehe 3.5). Auf dem zweiten Wege lassen sich auch relativ einfach Union-Anweisungen ersetzen. In der originalen Nachrichtenklasse CMessage gab es insgesamt zwei UnionAnweisungen. Die erste beschreibt eine Nachricht als Mostnachricht (MostMessage) oder interne Nachricht (AppMessage) (siehe 3.3). Die zweite Union Anweisung beschreibt eine Mostnachricht mit benannten Variablen oder als Array (siehe 3.4). Im Java-Framework ndet man die Variablen der AppMessage als Membervariablen der 21 Klasse CMessage wieder. Möchte man eine Nachricht wie im C++-Framework als Mostnachricht behandeln, so ist dies über die Memberfunktionen möglich (siehe 3.5). union msg { AppMessage appMsg ; MostMessage mostMsg ; } theMessage ; Listing 3.3: Union: AppMessage und MostMessage struct MostMessage { unsigned char senderType ; union { unsigned char bytes [19]; struct { UInt8 FBlockID ; UInt8 Instance ; UInt16 FkID ; UInt8 OPType ; UInt16 Length ; UInt8 data [12]; } rx_msg ; } data ; }; Listing 3.4: Union: Raw Daten und beschriebene Daten class CMessage { private char senderType ; private int senderID ; private int receiverID ; private int opcode ; private int parameter1 ; private int parameter2 ; private int parameter3 ; private byte parameter4 [] = new byte [39]; CMessage () { init (); } void init () { senderType = 0; 22 senderID = 0; receiverID = opcode = 0; parameter1 = parameter2 = parameter3 = } 0; 0; 0; 0; for ( byte counter = 0; counter < 39; counter ++) { parameter4 [ counter ] = 0; } void setSenderID ( int sender ) { this . senderID = sender ; } void setReceiverID ( int receiver ) { this . receiverID = receiver ; } ... void setMost_FBlockID ( byte FBlockID ) { this . senderID = ( FBlockID << 24) + ( this . senderID & 0 x00FFFFFF ); } void setMost_Instance ( byte Instance ) { this . senderID = ( Instance << 16) + ( this . senderID & 0 xFF00FFFF ); } ... } Listing 3.5: Ausschnitt aus der Klasse CMessage Kopieren von Speicherbereichen Unter C und C++ ist es sehr beliebt, groÿe Speicherbereiche wie Arrays oder Objekte mit Hilfe der Funktion memcopy zu kopieren oder mit 23 memset zu initialisieren. Diese Funktionen sind auf nahezu allen Plattformen auf Geschwindigkeit optimiert und deshalb sehr schnell. In Java gibt es solche Funktionen nicht. Das liegt haupsächlich an den der Philosophie von Java, keine direkten Speicherzugrie zu zulassen und an der strengen Typprüfung. Die Felder eines Arrays, bzw. die Membervariablen eines Objektes, müssen immer nacheinander kopiert werden. Aus diesem Grund ist das Kopieren von groÿen Objekten sehr viel langsamer als in C/C++. Eine Möglichkeit für Objekte, eine ähnliche Funktionalität zu erreichen, ist das Implementieren des Interfaces Cloneable (siehe [Mic04a, Online im Internet unter] [Stand: 28.12.2004]). Um Arrays zu kopieren steht die Funktion System.arraycopy zur Verfügung, welche alle Arten von Arrays kopieren kann. Für beide Methoden gilt jedoch, dass sie nicht so eektiv kopieren, wie es unter C/C++ möglich ist. Übergeben von Objektreferenzen Wird in Java eine Funktion aufgerufen, welcher als Parameter ein Objekt übergeben wird, so wird eine neue Referenz erzeugt, welche auf das übergebene Objekt zeigt. Das direkte Übergeben einer Referenz, wie es in C++ möglich ist, funktioniert unter Java nicht. Das folgende Beispiel würde beim Aufruf der Funktion teste() Ist nicht null zurückgeben. void test () { } CMessage msg = new CMessage (); getMessage ( msg ); if ( msg == null ) { System . out . println (" Ist null " ); } else { System . out . println (" Ist nicht null " ); } void getMessage ( CMessage mMessage ) { mMessage = null ; 24 } Listing 3.6: Objektübergabe per Kopie der Referenz Möchte man eine Referenz übergeben, so geht dies nur über den Umweg über ein Objekt oder ein Array, welches die Referenz enthält. Das folgende Beispiel gibt für die Objektrefernz nun null zurück. void test () { CMessage msgRefs [] = new CMessage [1]; msgRefs [0] = new CMessage (); getMessage ( msgRefs ); } if ( msgRefs [0] == null ) { System . out . println (" Ist null " ); } else { System . out . println (" Ist nicht null " ); } void getMessage ( CMessage mMessage []) { mMessage [0]= null ; } Listing 3.7: Objektübergabe per Referenz 25 4 Grundlagen für die Entwicklung des Frameworks Dieses Kapitel gibt eine kleine Einführung in die Softwareentwicklung unter QNX. Eine Anforderung des zu entwickelnden Frameworks war es, eine möglichst Plattformunabhängige Software zu erstellen. Aus diesem Grund wurden alle Teile des JavaFrameworks unter den Betriebsystemen QNX 6.3 (X86 und SH4), Linux (Debian Sid) und Microsoft Windows XP getestet. Dazu waren einige Anpassungen notwendig, welche im folgenden näher erläutert werden. 4.1 Die Entwicklungsumgebung Mit der QNX Momentics Development Suite liefert QNX die QNX Momentics Integrated Development Environment. Diese IDE basiert auf der bekannten Open Source IDE Eclipse, welche ursprünglich von IBM entwickelt wurde. Sie ist für das QNX eigene Betriebssystem Neutrino, sowie Linux und Windows verfügbar. Durch verschieden Änderungen an der IDE hat QNX Eclipse an die eigenen Bedürfnisse angepaÿt. Jedoch ist Momentics dadurch teilweise inkompatibel zum original Eclipse. Plug-Ins funktionieren in vielen Fällen nicht und Updates eingebauter Plug-Ins können zu verschiedenen unerwünschten Eekten führen. Die eingebaute UpdateFunktion sollte nicht genutzt werden, da sie die Momentics IDE beschädigen kann und damit zu einer Neuinstallation zwingt. Das Build-System von Momentics basiert auf der bekannten GNU Compiler Collection und den GNU Build Tools, welche zum Teil an QNX angepaÿt wurden. Dadurch ist es möglich, ein unter QNX entwickeltes Programm nahezu unverändert auch auf anderen Systemen zu kompilieren, auf denen die GNU Compiler Collection vorhanden ist. Dies funktioniert jedoch nicht, wenn hardwarenahe Funktionen genutzt werden (siehe auch 5.4.3 CD-Player ). Eine weitere Einschränkung besteht bei der Nutzung von Photon, dem QNX eigenen Windows System. Photon ist nur für QNX Neutrino verfügbar. Die Momentics IDE erstellt beim Anlegen neuer QNX Projekte ein Makele, welches 26 nur unter Momentics funktioniert, da es verschieden Dateien benötigt, welche nur in der Momentics Development Suite verfügbar sind. Aus diesem Grund wurden alle Makeles selbst erstellt. Dadurch war es auch möglich Eclipse für die Entwicklung zu nutzen, welches schneller reagiert und in neueren Versionen verfügbar ist. Für die Java Entwicklung wird auf Neutrino Systemen der j9 Compiler und die j9 VM von IBM genutzt. Diese lagen auf der Entwicklungsumgebung in Version 2.1 und auf dem Target in Version 2.0 vor. Dies entspricht Java Version 1.3.0. Auf anderen Systemen kam die Sun Java SDK 1.4.1 zum Einsatz. Auf dem Target war es notwendig die j9 VM anzupassen. Da das Framework SWT (siehe Literatursec:SWT) nutzt, benötigt die j9 VM eine Photon SWT Bibliothek, welche für die laufende Neutrino-Version (in diesem Fall 6.3) kompiliert wurde. QNX liefert diese nur für die Entwicklungsumgebung mit. Versuche, ältere Version der Bibliothek unter Verwendung der dazugehörigen Bibliotheken aus Neutrino 6.2 zu nutzen schlugen fehl. Auch führten neuere SWT-Versionen immer zum Absturz der j9 VM. Die einzige Möglichkeit bestand darin, diese selbst zu kompilieren. Da die von der JVM benötigte Version oziell eine Entwicklerversion ist, ndet man diese nicht als Packet. Sie kann aber über das Concurrent Versions System vom EclipseEntwicklungsserver bezogen werden, welcher alle Versionen kennt. 4.2 Zugri auf native Bibliotheken über JNI Um C oder C++ Code aus einer Java-Anwendung heraus aufzurufen, kann die JavaAnwendung eine spezielle Shared-Library laden und darin enthaltene Funktionen ausführen, als wären es statische Java Funktionen. Im folgenden wird kurz erklärt, wie dabei vorgegangen werden kann. 4.2.1 Warum JNI Performanzkritische Bereiche optimieren. Auf Targetspezische Libaries zugreifen (meist nicht in Java verfügbar). Einige plattformabhängige Merkmale stehen unter Java nicht zur Verfügung. Darunter vielen bei der Entwicklung des Java-Frameworks zum Beispiel der Zugri auf die serielle Schnittstelle oder auf CDs nach dem Red Book Standard (Audio CDs). 27 4.2.2 Ausführen von C++ Code aus Java-Programmen Eine Klasse, welche Funktionen aus einer Shared Library importieren möchte, muss diese zu importierenden Funktionen prototypisch mit dem Schlüsselwort native be- kannt machen. public class CMessageHandler public static native int public static native int public static native int } { initSharedMem (); getInt (); sendInt ( int intTest ); Listing 4.1: Java-JNI-Klasse Das Laden der Shared-Libary sollte in einer statischen Funktion erfolgen, da eine Library nur einmal geladen wird. static { System . loadLibrary (" MessageHandler " ); } Es ist zu beachten, dass der Name der Library nicht mit dem Dateinamen der Library übereinstimmt. Der Dateiname ist abhängig vom Betriebssystem und ist auf Posix-Systemen lib{Library-Name}.so und auf Win32-Systemen {Library-Name}.dll. In diesem Zusammenhang kann die folgende Funktion nützlich sein: System.mapLibraryName("MessageHandler") Sie gibt den erwarteten Dateinamen der Library zurück. 4.2.3 Entwickeln einer Shared-Library Im Folgenden wird erklärt, wie die Shared-Library erstellt wird. Compilieren des Java Codes Für das Erstellen der Shared-Library wird eine Headerdatei benötigt, welche aus dem Javacode generiert werden kann. Wird unter Momentics entwickelt, so wird der Code automatisch nach jeder Änderung kompiliert. Der Befehl 28 javah classname erzeugt aus der Javaklasse eine C/C++-Headerdatei, welche Prototypen für alle JNIFunktionen der Klasse enthält. Dies funktionierte in der vorliegenden Win32 Version der Momentics-Umge-bung (6.2.1A) nicht, da mehrere Dateien (javah und verschiedene Header) fehlten. Unter QNX sind diese Dateien aber wie erwartet vorhanden. Erstellen des C++ Codes Am einfachsten ist es, Momentics das Compilieren und Linken des Codes zu überlassen. Dazu erstellt man ein neues Projekt mit Hilfe des Templates QNX C++ Library Project. Darin kann man nun die Headerdatei importieren und den Code implementieren. Das Compilieren von Hand funktioniert genau wie bei Programmen, mit dem zusätzlichen Kommandozeilenparameter -shared. Ein Beispiel mit dem GNU-Compiler: g++ -shared -o libMessageHandler.so CMessageHandler.cpp Das folgende Beispiel zeigt, wie eine Bibliothek für QNX Neutrino auf einem SH4System kompiliert wird: qcc -Vgcc\_ntoshle -lang-c++ -shared -o libMessageHandler.so CMessageHandler.cpp Sollte beim Laden der Library eine Fehlermeldung erscheinen, welche auf Unresolved Symbols verweist, kann durch Entfernen des Kommandozeilenparameter -shared überprüft werden, welche Bibliotheken fehlen. Werden alle Bibliotheken gefunden, so sollte nur eine Fehlermeldung erscheinen, welche auf die fehlende MainProzedur verweist. Deployen der Library Die Library wird wie gewohnt unter dem Projektpfad im jeweiligen Targetverzeichnis erstellt. Für unsere Zwecke (JNI) ist nur die Sharedvariante mit der Endung 29 .so wichtig. Die Library muss auf dem jeweiligen Target in ein Verzeichnis kopiert werden, welches nach Libraries durchsucht wird. Dies ist auf Posixsystemen jeder Pfad, welcher in der Umgebungsvariablen LD_LIBRARY_PATH vorkommt sowie auf Win32-Systemen die Ordner %SYSTEM% \system32 und der Startordner des ladenen Programms. Sollte die Bibliothek nicht gefunden werden, so kann der Pfad zur Bibliothek beim Starten von Java wie folgt angegeben werden. java -Djava.library.path=<Pfad zur Bibliothek> <Zu startende Klasse> Debugging Zum Debugging muss die Shared-Library mit Debugging-Symbolen compiliert werden. Zu erkennen ist sie an der Endung _g. Das Debuggen von optimiertem Code ist nahezu unmöglich. Aus diesem Grund sollte der Optimization Level auf no optimize eingestellt werden. Dies funktionierte bei der vorliegenden Momenticsversion jedoch nicht, wodurch ein manuelles Ändern des Makeles (common.mk) notwendig ist. Die Variable CCFLAGS sollte wie folgend gesetzt sein: CCFLAGS+=-Y _ecpp_ne -O0 Die Debubugversion der Library wird automatisch geladen, wenn die JVM im Debugmodus (-Xdebug) gestartet wird. 30 5 Entwicklung des Frameworks Bei der Entwicklung des Java-Frameworks wurde die Architektur des C++-Frameworks soweit wie möglich übernommen, um das Verhalten der beiden Frameworks später vergleichen zu können. Dies spiegelt sich auch in den Bezeichnern von Klassen, Objekten und Variablen wieder, soweit diese in beiden Frameworks die gleiche Funktion haben. Unterschiede zwischen den Frameworks und die Besonderheiten, welche unter Java zu beachteten sind, werden in diesem Kapitel näher erläutert. 5.1 HMI Prototyp Um erste Erfahrungen zu sammeln, wurde ein Prototyp erstellt, welcher nur einen kleinen Teil des Frameworks in Java abbildet. Um dennoch ein funktionsfähiges Framework untersuchen zu können und erste Vergleiche mit dem originalen C++Framework zu ermöglichen, nutzt der Prototyp das C++-Framework für nicht implementierte Funktionen. 5.1.1 Aufteilung des Frameworks Für die erste Implementierung in Java wurde die HMI ausgewählt, da hierbei viele der für die Entwicklung des Frameworks notwendigen Verfahren zum Einsatz kommen. Der gröÿte Teil des Prototypes besteht aus dem originalen C++-Framework, aus dem der HMI-Teil entfernt wurde und weitere notwendige Änderungen vorgenommen wurden. 31 5.1.2 Schnittstelle zwischen C++ und Java Framework Die Kommunikation zwischen der HMI-Komponente in Java und dem C++Framework erfolgt über Shared-Memory. Die Wahl viel auf diese IPC Form, da das C++ Framework dieses Verfahren nutzt und somit weniger Anpassungen notwendig sind. Auch in Bezug auf Geschwindigkeit ist dies die erste Wahl (siehe 3.2.1). Die Festlegung auf Shared-Memory bedeutet die zusätzliche Entwicklung einer nativen Bibliothek, welche über JNI angesprochen wird, sowie weitere Anpassungen in beiden Frameworks. Ein Problem bei der Entwicklung einer Shared Memory-Schnittstelle zwischen dem C++-Framework und einem externen Programm ist, dass der Kontext, also alle Informationen, wie zum Beispiel welche Daten, an welcher Stelle im Shared Memory liegen, dem externen Programm nicht bekannt ist. Im C++-Framework selbst ist dies jeder Komponente bekannt, da der gemeinsam genutzte Speicher vor dem ersten Fork-Aufruf initialisiert wird. Für das Javaprogramm ist es unmöglich über einen Fork-Aufruf Daten zu erhalten. Dies liegt an der Art, wie Javaprogramme ausgeführt werden. Da diese in einer Virtuellen Maschiene (VM) ausgeführt werden, muss ein ausführbares Programm (die JVM) gestartet werden, welches im Gegensatz zum laufenden Programm oder dynamisch ladbaren Bibliotheken immer einen eigenen Datenbereich anlegt. Auch aus den in 3.2.1 Shared Memory genannten Gründen ist dies nicht möglich. Es gäbe verschieden Möglichkeiten, die notwendigen Daten auszutauschen. Dies könnte zum Beispiel über einen zusätzlichen, am Beginn des gemeinsamen Speichers gelegen Bereichs geschehen, welcher fest deniert ist. Aber auch der Datenaustausch über Pipes oder Sockets wäre denkbar. In diesem Fall wäre der Aufwand für ein solchen Datenaustausch aber übermäÿig hoch, da es auch umfangreichere Änderungen am original Framework bedeuten würde und eine dynamisches Shared Memory für den Prototypen nicht notwendig ist. Da das C++-Framework den gemeinsamen Speicher nicht dynamisch anlegt, sondern die Gröÿe und Lage der Objekte statisch im Sourcecode festgelegt ist, wird in der JNI-Library immer von der gleichen Lage und Gröÿe der Objekte ausgegangen. Dies wird gewährleistet, in dem verschieden Teile des C++-Frameworks für die JNILibrary wiederverwendet werden (siehe 5.1.2 JNI-Library ). Dadurch ist es möglich, die Objekte wie eine Schablone über die bereits im Shared Memory vorhandenen Objekte zu legen. Es ist jedoch zu beachten, dass die Objekte nur einmal initialisiert werden dürfen. Aus diesem Grund initialisiert die JNI-Library keine Objekte und muss immer nach dem C++-Framework gestartet werden. Ein einfaches Start-Skript wurde aus diesem Grund für das Starten der Frameworks verwendet. 32 Im Folgenden wird auf die wichtigsten Bereiche des Frameworks nocheinmal genauer eingegangen. Aufbau der Java JNI-Klasse Für den Zugri auf die native Bibliothek wurde die Klasse CMessageHandler implementiert. Sie stellt die Schnittstellen für die Kommunikation mit dem C++Framework zur Verfügung. public class CMessageHandler { public static native int initSharedMem (); public static native int getMessage ( CMessage objMessage ); public static native int sendMessage ( CMessage objMessage ); static { } System . out . println (" Lade " + System . mapLibraryName (" MessageHandler " )); System . loadLibrary (" MessageHandler " ); } Listing 5.1: JNI-Klasse CMessagehandler Diese Klasse beinhaltet nur die Schnittstellen zur nativen Bibliothek und enthält keinen eigenen Code. Da es sich bei diesen Schnittstellen um statische Funktionen handelt, ist es nicht notwendig eine Instanz dieser Klasse anzulegen. JNI-Library Die Aufgabe der JNI-Library ist es, die Daten, welche in einer Queue der HMIComponente ankommen, entgegenzunehmen und an die Java-HMI weiterzuleiten. Eine weiter Aufgabe ist das Entgegennehmen einer Nachricht vom Java-Framework und das weiterleiten dieser Nachricht an den Main Dispatcher des C++-Frameworks. Zu diesem Zweck wurden einige der Basiselemente des C++Frameworks wiederverwendet. Wenn immer möglich, wurde dazu ein Link benutzt um die Redundanz 33 gering zu halten. Dies war jedoch teilweise nicht möglich, da einige Klassen geändert werdenmusstenn. Die JNI-Library übernimmt im Grunde die Aufgaben der Adminkomponente des C++Frameworks für das Java-Frameworks. Im Gegensatz zur Adminkomponente braucht es jedoch nur die Kontexte anlegen. Die einzelnen Komponenten werden nicht benötigt. Beim Anlegen der Kontexte, muss jedoch darauf geachtet werden, dass Objekte, welche sich im Shared Memory benden, nicht wirklich angelegt werden, sondern nur die richtigenAdressenn erhalten. Um einen Kontext anlegen zu können, werden zusätzlich zur Kontextklasse (CContext) die Klassen CComponentContext, CCommQueue, CBinarySemaphore und CMessage, sowie die Headerdatei Global.h benötigt. Dabei werden jedoch nur von den Klassen CContext, CComponentContext und CCommQueue mit Hilfe der newFunktion neue Objekte im gemeinsamen Speicher angelegt. Diese Funktionsaufrufe müssen entfernt werden. context . mNormalQueuePtr = new ( memPtr ) CCommQueue ( normalQueueSize , * context . mTriggerSemaphorePtr ); Listing 5.2: Aufrufe wie diese wurden entfernt Eine weiter Aufgabe bei der Initialisierung der JNI-Library ist das Abfragen und Speichern der Java-Klasse CMessage. Da dies ansonsten bei jedem Empfangen oder Senden einer Nachricht durchgeführt werdenmüsstee und nach Aussagen von Sun sehr zeitaufwendig ist (siehe [Mic04b] Online im Internet verfügbar [Stand: 28.12.2004]). JNIEXPORT jint JNICALL Java_CMessageHandler_initSharedMem ( JNIEnv * env , jclass mCMessageHandler ) { CContext :: createContexts (); // buffer often used Objects // jclass clsMessage clsMessage = env -> GetObjectClass ( Message ); mHMISystemQueue = & CContext :: getHMIContext () . getSystemQueue (); mHMINormalQueue = & CContext :: getHMIContext () . getNormalQueue (); 34 mHMITriggerSemaphore = & CContext :: getHMIContext () . getTriggerSemaphore (); mMainDispatcherQueue = & CContext :: getMainDispatcherContext () . getNormalQueue (); } return 0; Listing 5.3: Die init-Funktion der JNI-Library Durch das nicht Anlegen der Komponenten, wird auch der Dispatcher der HMIKomponente nicht angelegt. Diese Aufgabe muss die JNI-Library übernehmen. In der Funktion getMessage ist die Funktionalität des Dispatchers, sowie die Konver- tierung von der C++-Nachricht in die Java-Nachricht implementiert. Im Gegensatz zur Dispatcher-Funktion im C++-Framework, blockiert der Aufruf von getMessage immer. JNIEXPORT jint JNICALL Java_CMessageHandler_getMessage ( JNIEnv * env , jclass mCMessageHandler , jobject Message ) { bool hasMessage = false ; // as long as the dispatcher is not interrupted // it will block if no message is available while ( false == hasMessage ) { // try to get an system command hasMessage = mHMISystemQueue -> getMessage ( mMessage ); if ( false == hasMessage ) { hasMessage = mHMINormalQueue -> getMessage ( mMessage ); } if ( true == hasMessage ) { // a command has been found break ; 35 } } mHMITriggerSemaphore -> take (); if ( true == hasMessage ) { jfieldID senderID = env -> GetFieldID ( jfieldID receiverID = env -> GetFieldID ( jfieldID opCode = env -> GetFieldID ( clsMessage , " sender " , "I" ); clsMessage , " receiver " , "I" ); clsMessage , " opCode " , "I" ); ... env -> SetIntField ( Message , senderID , mMessage . getSenderID ()); env -> SetIntField ( Message , receiverID , mMessage . getReceiverID ()); env -> SetIntField ( Message , opCode , mMessage . getOpcode ()); ... } return 0; 36 } return -1; Listing 5.4: Die getMessage-Funktion der JNI-Library Beim Senden einer Nachricht, wird diese in eine C++-Nachricht kopiert und wie im C++-Framework über die add-Funktion der Main-Dispatcher-Queue verschickt. C++Framework Am C++-Framework waren nur sehr wenige Änderungen notwendig, da eigentlich nur der Start der HMI-Komponente unterbunden werden muss, um zu verhindern, dass die Photonoberäche gestartet wird und das die Nachrichten aus der HMI-Queue abgeholt werden. Dazu reicht es, die HMI aus der Admin-Komponente zu entfernen. Da jedoch ein Groÿteil der Source- und Header-Dateien nur für die HMI nötig sind wurden diese entfernt. Dies führte zu einem übersichtlicherem Projekt und einem schnelleren kompilieren. 5.1.3 Implementierung der HMI-Komponente Die übergeordnete Klasse der HMI ist, wie im C++-Framework, die Klasse CHMIComponent. Sie ist zuständig für das Initialisieren, Starten, Stoppen und Pausieren der HMI und enthält einen Dispatcher für das Verteilen von Nachrichten der eigenen Queue. Die Klasse CHMIController ist für den logischen Ablauf zuständig und nutzt die Klasse CCDPlayerGUI zum Darstellen einer CDPlayer-Komponente. Da diese Klassen später wiederverwendet werden sollen, gibt es eine extra Klasse CHMIManager, welche für das Starten und Initialisieren der HMI zuständig ist. Die Klassen CContext und CMessage sind auch im C++-Framework vorhanden und haben auch die gleiche Funktion. Es waren jedoch in beiden Fällen einige Änderungen notwendig. 5.2 Das Java Framework Wie bereits unter 1.2 Zielsetzung erwähnt wurde, sollen verschiedene Vergleiche zwi- schen einem C++-Framework und einem Java-Framework angestellt werden. Um 37 solche Vergleiche durchführen zu können, sollten sich die Frameworks hinsichtlich Funktionsweise und Aufbau möglichst wenig unterscheiden. Da eine Software Architektur unabhängig von einer Programmiersprache ist, konnte die C++-FrameworkArchitektur aus 2.2 Architektur und Funktionsweise vollständig übernommen werden. 5.2.1 Ausführen der Komponenten Vergleich von Prozessen und Threads Aus der Architektur des Frameworks ergibt sich, dass die einzelnen Komponenten parallel ausgeführt werden müssen. Dazu kann eine Komponente entweder als eigenständiger Prozess oder als ein Thread gestartet werden. Das C++-Framework nutzt, Prozesse für die Ausführung der Komponenten (siehe 2.3 der Interprocesscommunication (IPC) ). Verwendete Möglichkeiten Die Wahl viel auf Prozesse, da diese in ei- nem eigenen Speicherbereich ausgeführt werden. Dadurch fallen zum Beispiel Fehler durch ungültige Pointer sehr schnell auf. Sollte eine Komponente unerwartet reagieren, blockiert sein oder durch einen Fehler beendet werden, so laufen die anderen Prozesse weiter Bei der Verwendung von Threads wäre die Wahrscheinlichkeit, das durch einen Fehler auch andere Threads in Mitleidenschaft gezogen werden, sehr viel gröÿer. Unter Java kommen diese Vorteile von Prozessen jedoch kaum zum Tragen. Auch innerhalb einer JVM ist weder möglich auf Speicher direkt zu zugreifen, noch über die Grenzen einer Variablen hinweg zuschreiben. Ein Thread kann auf ein Objekt also nur zugreifen, wenn es eine Referenz auf dieses besitzt. Lediglich auf statische Funktionen und Variablen kann immer zugegrien werden. Führt ein Fehler in einem Thread zum Abbruch, können die anderen Threads ungehindert weiter laufen. Prozesse haben also unter Java kaum Vorteile, jedoch einige Nachteile. Da ein Javaprogramm nicht alleine als eigenständiger Prozess laufen kann, muss für jeden neuen Prozess auch eine neue JVM gestartet werden. Diese benötigt Zeit zum Starten und Speicher. Bei Tests auf dem Target, mit einem einfachen Hello World Beispiel, ergab sich eine Speicherbelegung von 3MB pro JVM + einmalig 1,5MB für gemeinsame Bibliotheken (siehe auch 6.1 Performance ). Dies ist eine nicht unerhebliche Belastung für ein Embedded System. Weiterhin ist es Java Prozessen nicht möglich, über gemeinsamen Speicher, Daten auszutauschen (siehe 3.2.1 Shared Memory ). Dies könnte zum Beispiel über JNI oder auch über Sockets geschehen. Beide Verfahren sind aber wesentlich langsamer, als ein Zugri innerhalb einer JVM. 38 Aus diesen Gründen ist es sinnvoller Java-Threads zu nutzen, wenn es sich nicht um über Rechnergrenzen verteilte Anwendungen handelt. Implementierung der Admin Komponente Das Starten der einzelnen Komponenten übernimmt die Admin Komponente. Um eine Komponente als Thread auszuführen gibt es zwei Möglichkeiten. Man kann die Komponentenklasse von Thread erben lassen und die run-Methode überschreiben, oder das Runnable Interface implementieren. Für das Java Framework wurde die zweite Variante gewählt, da eine Klasse unter Java nur von einer Klasse erben kann, jedoch mehrere Interfaces implentieren kann. Durch das Wählen der Interface Methode bleibt es weiterhin möglich, von einer anderen Klasse zu erben. public class CMiniCommander implements Runnable { public void run () { // Hier beginnt das Leben des Threads ... } } Listing 5.5: Beispiel Implementierung des Runnable Interfaces Um eine Komponente zu starten, wird in der Adminkomponente ein ComponentContext angelegt. Dieser Kontext benötigt drei Argumente - eine eindeutige ID, die Gröÿe der Systemqueue und die Gröÿe der normalen Queue. Mit Hilfe der add-Methode wird der ComponentContext der Auistung aller Kontexte hinzugefügt. Nun kann die eigentliche Komponente erzeugt werden. Dabei bekommt sie ihren Kontext übergeben, um auf ihr Queues zugreifen zu können. Im letzten Schritt wird die Komponente gestartet. CComponentContext hmiContext = new CComponentContext ( CContext . HMIID , 10 , 100); CContext . add ( hmiContext ); CHmiComponent hmi = new CHmiComponent ( hmiContext ); new Thread ( hmi ). start (); Listing 5.6: Anlegen und Starten einer Komponente Im Gegensatz zum C++-Framework müssen im Java-Framework nur zwei Dateien geändert werden, wenn eine neue Komponente hinzugefügt wird. Zusätzlich zu den 39 Änderungen in der Admin Komponente muss in die Klasse CContext noch eine Komponenten ID hinzugefügt werden. Dies könnte vermieden werden, wenn für die Komponenten ID ein Identier in Form eines String benutzt werden würde. Dies würde das Abfragen von CContext nach einem konkreten Kontext jedoch etwas langsamer machen. 5.2.2 Verwalten des Speichers Objekt Pooling bringt unter Java weitaus weniger Performanzezuwachs als weithin angenommen wird. Wie Dr. Click unter [Cli03] beschreibt, gibt es bei kleinen Objekten sogar einen Performanzeverlust. Nur bei sehr groÿen Objekten von mehreren Kilobytes kann es einen Geschwindigkeitszuwachs geben. Es ist in Java nicht möglich eine Objekt anzulegen und den reservierten Speicher für ein anderes Objekt zu nutzen, wie es zum Beispiel in C++ möglich ist (siehe 5.7). In Java würde dieses Vorgehen zu einer Exception führen, da nur eine Verallgemeinerung, also der Cast von einer Unterklasse zu ihrer Superklasse möglich ist. Die Objekte im Pool müssen also vom gleichen Typ sein, wie die benötigten Objekte. CBigClass * bigObject = new CBigClass (); CSmallerClass * smallerObject = ( CSmallerClass *) bigObject Listing 5.7: Wiederverwenden von Speicher in C++ Aus diesem Grund wurde die Strategie verfolgt, alle Objekte die benötigt werden möglichst beim Starten anzulegen und diese wieder zu verwenden. Um keine neuen Nachrichten anzulegen, wenn man eine Nachricht aus einer Queue abholt, gibt es dann zwei Möglichkeiten. Man könnte die Nachricht nicht wirklich aus der Queue entfernen, sondern warten bis die Bearbeitung der Nachricht abgeschlossen ist und erst dann mit der nächsten Nachricht fortfahren. Dies hat jedoch den Nachteil, das bei einem Fehler, dieser nur schwerer aundenbar ist. Ein weitaus gröÿeres Problem tritt aber auf, sollte innerhalb einer Komponente ein Thread gestartet werden, welcher für die Verarbeitung der Nachricht benötigt wird. Dieser Thread würde den Thread der Komponente blockieren und könnte zu einem Deadlock führen. Eine andere Möglichkeit ist das Übergeben eines Nachrichtenobjektes an die Queue, wenn eine Nachricht abgeholt wird. Bei dieser Variante muss kein Objekt kopiert werden, da nur Referenzen auszutauschen sind. Auch wird der Dispatcher nicht blockiert, da er sofort mit der nächsten Nachricht weitermachen kann. Da im Framework alle Objekte wiederverwendet werden sollen ist dies daeektivstete Verfahren, welches ohne Aufruf eines Pooling Managers auskommt. 40 5.3 Die HMI-Komponente Die HMI-Komponente besitzt einen Controller, welcher für das Starten der SWTUmgebung, sowie das Verwalten und das Umschalten auf die verschiedenen GUIKomponenten verantwortlich ist. Eine GUI-Komponente ist in diesem Fall eine grasche Darstellung eine Gerätes, wie zum Beispiel dem CD-Player. Der HMI-Controller ist dafür verantwortlich, Benutzereingaben an die GUI-Komponente weiterzureichen, welche darauf mit dem Darstellen neuer Inhalte oder auch dem Verschicken von Nachrichten an andere Komponenten reagieren kann. 5.4 JNI Module Für ein funktionales Framework mit den unter 1.2 Zielsetzung genannten minimalen Anforderungen war es nötig, verschiedene JNI-Module zu erstellen. 5.4.1 Input Schnittstelle Das Target besitzt für Benutzereingaben ein Gerät, welches über 6 Knöpfe und einen drehbaren Knopf verfügt. Dieses wird über die serielle Schnittstelle angeschlossen und nennt sich Mini-Commander. Unter QNX Neutrino ist jedoch die Java Standarderweiterung javax.comm nicht verfügbar. Aus diesem Grund wurde eine JNI Bibliothek entwickelt, welche die Daten vom Mini-Commander entgegen nimmt und dem Framework die gedrückte Taste als Zahlencode zurückgibt. Auf der Java-Seite wurde die Klasse CMiniCommander als Komponente implementiert, welche jedoch keinen Kontext besitzt, da sie keine Nachrichten empfangen muss. Die Schnittstelle zur JNI-Bibliothek wird über die Funktionen initSharedMem() und getPressedKey() hergestellt. In diesem Fall sind die Funktionen als private deklariert, da sie sonst von jedem Objekt auch ohne Referenz ausgeführt werden könnten. Die Funktion getPressedKey() blockiert den Aufruf solange, bis eine Taste gedrückt wurde. Dadurch verbraucht die Mini-Commander Komponente keine Rechenzeit, wenn auf eine Taste gewartet wird. 41 5.4.2 Mostgateway Das Mostgateway wurde in Java etwas anders implementiert, als im C++-Framework. In der C++-Implentierung wird im Maindispatcher ein weiterer Thread gestartet, welcher für das Verschicken und Empfangen der Mostnachrichten verantwortlich ist. Dies stellt einen Bruch mit der Frameworkarchitektur dar, da für jedes Gerät eine Komponente vorgesehen ist. Im Java-Framework wurde das Mostgateway als eigenständige Komponente (CMostComponent) implementiert. Ein weiterer Unterschied gegenüber dem C++-Framework besteht in der Art wie das Gateway Nachrichten abfragt. Es wird Polling verwendet, um abwechselnd nachzusehen, ob eingehende Nachrichten für das Framework oder ausgehende Nachrichten vom Framework vorliegen. Da dies wertvolle Rechenzeit verbraucht, wurden hierfür blockierende Aufrufe implementiert. Wie in 5.8 zu sehen ist, werden zwei Instanzen der Klasse CMostIO erstellt. Ein Aufruf der getMessage-Funktion blockiert nun den Thread. Um eine Nachricht senden zu können muss dies durch einen anderen Thread ausgeführt werden. Würden jedoch ein zweier Thread auf die Most Bibliothek zugreifen, könnte das zu unvorhersehbaren Resultaten führen, da sie nicht Thread sicher ist. Jede Instanz der Klasse CMostIO önet einen eigenen Stream zur Most Bibliothek, wodurch sie sich nicht gegenseitig stören können. CMostIO mostSender ; CMostIO mostReceiver ; CMessage rMessage ; CMessage sMessage ; JNIEXPORT jint JNICALL Java_CMostDispatcher_initMost ( JNIEnv * env , jclass , jobject ) { int resultSender = mostSender . init (); int resultReceiver = mostReceiver . init (); } if ( resultSender || resultReceiver ) return 1; else return 0; /* * Class : CMostDispatcher * Method : runMost * Signature : () I 42 */ JNIEXPORT jint JNICALL Java_CMostDispatcher_getMostMessage ( JNIEnv * env , jclass , jobject Message ) { mostReceiver . getMessage ( rMessage ); // Copy Most to JavaMessage .... } JNIEXPORT jint JNICALL Java_CMostDispatcher_sendMostMessage ( JNIEnv * env , jclass , jobject Message ) { // Copy Java to Mostmessage .... } mostSender . sendMessage ( sMessage ); Listing 5.8: Auszug aus der Most-JNI-Library Die Klasse CMostIO wurde zu kleinen Teilen aus der Klasse CMostIO (in der Datei most_thread.cpp) des C++-Frameworks übernommen. Da nur das Target einen Mostbus besitzt, wird das Fehlen der Most-Bibliothek nur gemeldet, führt aber nicht zu einem Fehler. Alle Pakete, welche an die Mostkomponente geschickt werden, werden in diesem Fall verworfen. 5.4.3 CD-Player Der CD-Player wurde auch als Framework-Komponente implementiert. Es handelt sich ihr um eine Hardwarenahe-Komponente. Aus diesem Grund wurden für Momentics und Linux/Windows unterschiedliche Versionen erstellt. Für das Framework selbst, ergibt sich jedoch kein Unterschied. 5.5 Grasche Oberächen unter Java 5.5.1 Auswahl eines Toolkits Beim SWT handelt es sich um ein Toolkit zum Erstellen von graschen Benutzeroberächen. Aus den folgenden Gründen wurde SWT anderen vorhanden Lösungen 43 den Vortritt gegeben: AWT Das Abstract Windowing Toolkit (AWT) benutzt native Widgets des unterliegenden Betriebssystem zur Darstellung von Oberächen. Dadurch ist es sehr schnell. Jedoch ist es in seinen Fähigkeiten sehr beschränkt, da es aus Portablilitätsgründen nur einen relativ kleinen Befehlsumfang hat. Swing Das Toolkit Swing baut auf AWT auf und ist sehr viel exibler. Da es aber die Widgets selbst zeichnet, ist es auch sehr langsam. SWT Das Standard Widget Toolkit (SWT) ist ursprünglich eine Eigenentwicklung von IBM, welche nun unter einer Open-Source-Lizenz steht. Mit Hilfe von JNI nutzt SWT die Betriebssystemfunktionen, um Widgets darzustellen. Es ist dadurch exibler als AWT und schneller als Swing. J9 Es gibt für die Targetplattform keine JVM von Sun. Aus diesem Grund wurde die JVM von IBM (j9) ausgewählt, welche als einzige ohne weitere Anpassungen nutzbar ist und auch von QNX selbst eingesetzt wird. Unter IBMs JVM kommt hauptsächlich das auch von IBM entwickelte Standard Widgets Toolkit zum Einsatz. Andere Benutzeroberächen, wie Swing und AWT laufen hingegend nur mit Einschränkungen. 5.5.2 SWT-Programmierung Die Programmierung mit dem Standard Widget Toolkit (SWT) hat einige Besonderheiten, welche eine Anpassung des Frameworks notwendig gemacht haben. Aus diesem Grund wird in diesem Kapitel näher auf SWT eingegangen. Das Standard Widget Toolkit basiert auf einem single-threaded UI Modell. Das bedeutet, dass der Thread, welcher das Display startet, der UI-Thread wird. Nur dieser Thread hat Zugri auf die grasche Oberäche. Für die HMI-Komponente bedeutet dies, dass sie entweder alle Aktivitäten in der Dispatcherschleife der SWT-Oberäche durchführt, oder einen Zugriüberber die syncExec()-Funktion von SWT zu realisieren. Da die HMI der Controller des Frameworks ist und ein langer Update der GUI-Oberäche das ganze System ausbremsen könnte, wurde die zweite Variante ausgewählt. Um eine Funktion im UI-Thread auszuführen, kann eine Klasse angelegt werden, welche von der Runnable-Klasse erbt, oder das Runnable Interface implementiert. Eine weitere Methode besteht darin, beim Anlegen einer Instanz von der Klasse Runnable die virtuelle Funktion run() zu implementieren (siehe 5.9). In der Run-Funktion wird 44 der Code eingefügt, welcher auf den UI-Thread zugreifen soll. In der Methode handleMessage wird dieser Code asynchron ausgeführt. Die Funktion blockert dadurch den Thread der HMI-Komponente nicht. Runnable mGUIMessageHandler = new Runnable (){ public void run () { switch ( mMessage . getParam1 () & 0 xff ) { case 0 xc3 : mGUIHandler . ButtonA (); break ; }; } } ... public void handleMessage ( CMessage msg ) { mMessage = msg ; mGUIHandler . mDisplay . asyncExec ( mGUIMessageHandler ); } Listing 5.9: Ausführen von Code im UI-Thread 45 6 Vergleiche der Frameworks 6.1 Performance Die Zeit wurde unter Java mit Hilfe der Funktion System.currentTimeMillis() gemessen. Die Auösung der Zeitmessung lag somit im Millisekundenbereich. Auf neueren Java VMs gibt es eine Funktion, welche auch im Nanosekundenbereich messen kann. Diese stand auf dem Target jedoch nicht zur Verfügung. Da die benötigte Zeit für einen Funktionsaufruf im ns-Bereich liegt, wurde die Aufrufe zwischen 1 bis 1 Millionen mal wiederholt und daraus die Dauer eines Aufrufs errechnet. Jeder Test wurde mindestens 5 mal durchgeführt, um zu verhindern, dass Ausreiÿer in die Testergebnisse mit einieÿen. Der erste Aufruf einer Funktion aus einer JavaKlasse wurde nicht gewertet, da dieser immer 18ms Zeit kostete. Dies muss an dem Just In Time Comiler von Java liegen, welche die Klasse vorher optimiert. 6.1.1 Leistungsfähigkeit der JNI-Schnittstelle Das Testverfahren Die JNI-Schnittstelle von Java ist für die korrekte Übergabe von Daten verantwortlich. Dabei muss sie zur Laufzeit, je nach Datentyp und Zugrisart verschiedene Tests ausführen und die Datenintegrität sicherstellen. Es ist also zu erwarten, das jeder Zugri von Java auf die JNI-Bibliothek und vor allem von der JNI-Bibliothek auf Java mit einem Performanzeverlust behaftet ist. Um zu ermitteln, wie viel Zeit an der JNI-Schnittstelle verloren geht, wurde ein Testprogramm entwickelt, welches einen Vergleich anstellt zwischen den Zugrien auf interne Java- und externe JNI-Konstrukte. Das folgende Testbespiel zeigt, wie die Zeit für eine bestimmte Anzahl an JNI Aufrufen gemessen wird. In diesem Testfall soll untersucht werden, wie viel länger der Aufruf einer JNI-Funktion mit einer Long-Variablen (ein 64-Bit-Integer) gegenüber 46 einer gleichartigen Java-Funktion benötigt. Dabei muss berücksichtigt werden, das der Aufruf der JNI-Funktion vom Java-Compiler nicht optimiert werden kann, da diese dem Compiler nicht vorliegt. Jedoch kann der Java-Compiler, den Aufruf der Java-Funktion optimieren. Es wurde jedoch bei jedem Test anhand der Ergebnisse sicher gestellt, dass der Test Aussagekraft besitzt und nicht etwa durch Compileroptimierungen ganze Schleifen wegoptimiert wurden sind. public long JNITest_long ( int intNumOfTests ) { int counter ; Date Date long long dtStartTime = new Date (); dtEndTime = new Date (); lngDiffTime ; lngBuffer ; dtStartTime . setTime ( System . currentTimeMillis ()); for ( counter = 0; counter < intNumOfTests ; counter ++) { mDiffTime = CJNIHandler . getLong (); } dtEndTime . setTime ( System . currentTimeMillis ()); lngDiffTime = dtEndTime . getTime () - dtStartTime . getTime (); } return lngDiffTime ; Listing 6.1: Testfall 1: Aufrufen einer JNI-Funktion public long JavaTest_long ( int intNumOfTests ) { int counter ; Date Date long long dtStartTime = new Date (); dtEndTime = new Date (); lngDiffTime ; lngBuffer ; dtStartTime . setTime ( System . currentTimeMillis ()); for ( counter = 0; counter < intNumOfTests ; counter ++) { lngBuffer = CJavaHandler . getLong (); } 47 dtEndTime . setTime ( System . currentTimeMillis ()); lngDiffTime = dtEndTime . getTime () - dtStartTime . getTime (); return lngDiffTime ; } Listing 6.2: Testfall 1: Aufrufen einer Java-Funktion JNIEXPORT jlong JNICALL Java_CJNIHandler_getLong ( JNIEnv * env , jclass cls ) { return lngTest ++; } Listing 6.3: Testfall 1: Rückgabe einer Long-Variablen an Java Diese Tests wurden für primitive Typen wie Integer, Long und Char durchgeführt und auch mit Objekten, wie String und einem Nachrichtenobjekt der Framework-Klasse CMessage. Die Testergebnisse Die folgenden Ergebnisse zeigen, wie lange es dauert, einen Wert von einer Funktion zu erhalten. Bei primitiven Datentypen benötigt die JNI-Funktion etwas mehr als doppelt solange wie die interne Java-Funktion. Die Gröÿe der Variablen spielt dabei eine untergeordnete Rolle. Anders sieht das bei Objekten, wie einem String aus. Hier benötigt die JNI-Funktion mehr als 12 mal länger, als die interne Java Funktion. Dies kann damit erklärt werden, das die Prüfung eines ungeschützten Puers, welcher ein String in C++ darstellt, sehr aufwendig ist. Da die Länge des Strings in diesen Dimensionen vernachlässigbar. Char Int Long String(10) String (10000) JNI Zeit in ns 3,36 3,42 3,31 12,0 12,0 Java Zeit in ns 1,46 1,46 1,45 0,96 0,96 Verhältnis JNI/Java 2,30 2,34 2,28 12,5 12,5 Die Übergabe von Werten an die JNI-Bibliothek ist für primitive Typen in etwa genauso schnell wie das Erhalten von Werten (siehe Tabelle 6.1.1). Das Übergeben von Objekten, wie einem String ist hier jedoch doppelt so schnell. Im Vergleich mit dem Aufruf einer Java-Funktion ist das Übergeben eines Strings aber immernoch etwa 5 mal langsamer. 48 Char Int Long String(10) String (10000) JNI Zeit in ns 3,63 3,60 3,69 6,08 6,09 Java Zeit in ns 1,40 1,40 1,41 1,19 1,19 Verhältnis JNI/Java 2,59 2,57 2,61 5,11 5,12 Alle nicht im obigen Beispiel aufgeführten Objekte verhalten sich genau so, wie man es nach den Tabellen 6.1.1 und 6.1.1 erwartet. So ist das Verhalten eines Arrays von einem primitiven Datentyp, mit dem eines Strings vergleichbar. Greift man auf die Variablen eines Objektes zu, so muss man für jede Variable, auf die zugegrien wird, die oben angegebene Zeit veranschlagen. Aus der obigen Tabelle lassen sich verschiedene Schlüsse für das Java-Framework ziehen. Da der Aufruf einer JNI-Funktion relativ teuer ist, aber die übergebene Gröÿe der Variablen kaum eine Rolle spielt, sollten die folgenden Regeln beachtet werden. Daten immer in einem Aufruf übergeben werden. es vermieden werden, Daten durch 1. Wenige Variablen von einem kleinen Datentyp, wie Integer oder Byte können zu einem Gröÿerezusammengefasstÿt werden. 2. Gröÿere Datenmengen per Array übergeben 3. Keine Objekte übergeben, aus denen mehrere Membervariablen benötigt werden. Diese besser in einem Array zusammen fassen. 6.1.2 Startverhalten Das Startverhalten des Java-Frameworks ist Erwartungsgemäÿ etwas anders, als das vom C++-Framework. Bei Messungen mit einem einfachen Hello World Programm hat sich gezeigt, das auf dem Target 8,9 s vergehen, bis die JVM gestartet ist. Um alle Komponenten des Frameworks zu starten, benötigt das Java-Framework 4,6 s. Das C++-Framework braucht dazu nur 0,96 s. Dies liegt vor allem an der SWTOberäche, welche alleine 3,5 s benötigt um zu starten. Mit dem Start der JVM benötigt das Java-Framework somit 13,5 s zum starten. Da die Komponenten in verschiedenen Threads laufen, können diese jedoch schon mit ihrer Arbeit beginnen, bevor die SWT-Oberäche gestartet wurde. 49 6.1.3 Reaktionszeiten & Antwortverhalten Um die Reaktionszeiten der Frameworks vergleichen zu können, wurden versucht die Laufzeiten von Nachrichten unter Last zu messen. Da es sich beim Verschicken von Nachrichten jedoch nur um das Umkopieren von Referenzen handelt, ergaben sich hierbei keine verwertbaren Ergebnisse. Das Verhalten des Java-Frameworkes wurde überprüft, in dem das System durch dauerhaftes Nachrichten generieren stark belastet wurde. Dabei generierte die CDPlayerKomponente ein einer Endlosschleife Nachricht zum Update der CD-Spielzeit. Das Framework reagierte aber auch hier ohne gröÿere Verzögerung. Lediglich die grasche Oberäche wurde etwas träger. 50 7 Zusammenfassung & Ausblick Die Entwicklung des Java-Frameworks hat gezeigt, dass Java auf Embedded Systemen möglich ist. In vielen Bereichen ist Java genauso schnell, wie ein in C++ geschriebenes Programm. So kann man Subjektiv keinen Unterschied zwischen dem C++- und dem Java-Framework ausmachen, sobald das Framework gestartet ist. Für den Automotive Bereich dürfte die lange Startphase der Virtuellen Java Maschine das gröÿte Problem darstellen. Da dies für die Entwickler eine feste Gröÿe darstellt, können nur noch schnellere Prozessoren oder in Hardware gegossene VMs daran etwas ändern. Während der Entwicklung hat sich auch gezeigt, dass Java auf einem Embedded System nicht alle seine Stärken voll ausspielen kann. Durch die Notwendigkeit, die JNI-Schnittstelle zu benutzen, kann man sehr schnell die Vorteile der Javaprogrammierung verlieren. Schon bei den relativ wenigen implementieren Funktionen des Frameworks war es unumgänglich, drei JNI-Bibliotheken zu erstellen. Dabei hat sich gezeigt, das ein Fehler in einer JNI-Bibliothek das gesamte Framework zum Absturz bringen kann. Auf Embedded Systemen verliert man also teilweise die leite Rückverfolgung von Fehlern, die Robustheit der JVM gegeAbstürzeze und die klaren und fehlervermeidenden Programmiertechniken. Es stellt sich die Frage, ob Java in diesem Umfeld jetzt schon eingesetzt werden sollte. Aus der Sicht eines Entwicklers muss ich dies bejahen, da die Programmierung dennoch schneller und strukturierter voran geht. Aus der Sicht eines Autofahrers sehe ich die lange Startphase des Java-Frameworks jedoch ein Problem, welches vorher gelöst werden muss 51 Literaturverzeichnis [Bal98] Lehrbuch der SoftwareTechnik - Software SoftwareQualitätssicherung, Unternehmensmodellierung. Helmut: Balzert, Managment, Nummer Band 2. Spektrum Akademischer Verlag, Heidelberg - Berlin, 1998. [Bal00] Helmut: Balzert, Entwicklung. Lehrbuch der SoftwareTechnik - Software Nummer Band 1. Spektrum Akademischer Verlag, Heidelberg - Berlin, 2000. [Bor04] Borman, Garbage. Sam: Sensible Sanitation Understanding the IBM Java IBM, http://www-106.ibm.com/developerworks/ibm/library/i- garbage1/, 12 2004. [Cli03] Click, Dr. Cliff: Java One: Performance Myths Exposed. Azul Sys- tems, http://servlet.java.sun.com/javaone/resources/content/sf2003/conf/sessions/pdfs/1522.pdf, 2003. [Mic04a] Microsystems, Sun: The Java Tutorial: Java Native Interface. http://java.sun.com/j2se/1.4.2/docs/api/index.html, 12 2004. [Mic04b] Microsystems, Sun: The Java Tutorial: Java Native Interface. http://java.sun.com/docs/books/tutorial/native1.1/index.html, 12 2004. [QNX04] QNX Software Systems Ltd: cumentation, 12 2004. 52 QNX Momentics Development Suite Do-