DYNAMIK AUF DER VM Unterstützung dynamischer Sprachen auf der Java Virtual Machine Marcus Rieche für jedes Programm eine eigene virtuelle Umgebung ausführen und der Virtual Machine selbst nur wenig Rechte vom Betriebssystem her geben. Dadurch können sich Programme weder untereinander noch dem System schaden, abgesehen von legitimen Zugriffen. z.B. dem löschen von Dateien innerhalb eines nicht System kritischen Bereiches. ABSTRACT Die JVM ist ein interessanter Ansatz, um die Portierung von Programmen auf unterschiedliche Hardware Plattformen durch zu führen. Die JVM ist dabei auf Java optimiert. Die Programmiersprache Java ist jedoch nicht für alle Problemstellungen die beste Wahl, womit sich der Wunsch ergibt auch andere Sprachen auf der JVM zu unterstützen. Insbesondere dynamische Sprachen erfreuen sich derzeit großer Beliebtheit und daher wird im folgenden betrachtet, was man machen könnte und müsste, um diese auf der JVM zu unterstützen. 1.2. Dynamik kann man auch dadurch erreichen, dass man Verknüpfungen im Stack und im Object Tree frei ändern kann. Dazu muss lediglich eine API bereitgestellt werden, welche diese Änderungen unterstützt. 1. EINLEITUNG Das würde aber bedeuten, dass es von da an keine Garantien mehr gibt. Es gäbe keinen Code Abschnitt mehr, welcher nicht verändert werden könnte. Das wäre für Geschäftskritische Anwendungen, wie z.B. Bankingoder Abrechnungsysteme nicht unbedingt wünschenswert. Punktuell können dort dynamische Elemente gewünscht sein. z.B. für die Berechnungen von Preisen. Sollte Schadcode an dieser Stelle ausgeführt werden, wird dieses später wohl durch Null oder Negativ Zeilen in der Datenbank auffällig werden. Freie Änderungen könnten nach Auslieferung der Ware zu einer Löschung des Lieferscheines führen, womit es dann auch keine Spuren mehr gäbe. Bevor Anpassungen der JVM betrachtet werden, werden kurz zwei andere Ansätze angedeutet, um den Vorteil des gewählten Implementierungsansatzes zu verdeutlichen. 1.1. FREIER ZUGRIFF AUF DIE REFLECTION INFORMATIONEN REGISTER BASIERTE VM Es gibt bereits Implementierungen, welche eine Register basierte Virtual Machine umsetzen. Diese Maschinen stellen ein vollständiges Assembler bereit. Es wird also ein idealisierter PC im PC ausgeführt. Mit Hilfe der Assembler Implementierung lassen sich alle Probleme genau so wie auf dem Hardware Vorbild lösen. Außer dem Schadcode Szenario können sich auch von Entwickler Seite her leichter Fehler einschleichen, wenn es keine Kontrolle über veränderbare Punkte gibt. Es könnten z.B. Querabhängigkeiten bestehen. Problematisch könnten allerdings die Sicherheitsmechanismen sein. Die Entwicklung der Betriebssysteme zeigt, dass man relativ einfach Systeme entwickeln kann, die viele Freiheiten gewähren und man dann Konzepte etablieren muss, um diese Freiheiten wieder einzuschränken. Als Beispiel seien genannt der Buffer Overflow Schutz ,die Isolierung von Prozessen und Pakete wie SELinux. Das diese Mechanismen erst in den letzten Jahren zum Standard wurden, zeigt das bei der Implementierung eines auf Registern basierten Systems sich diese Sicherheitsaspekte nicht sofort erschließen. 1.3. MOTIVATION Beiden Ansätzen ist gemein, dass sie zu viele Freiheitsgrade bieten. Weiterhin ist die Java Virtual Machine bereits Praxis erprobt und daher ist es sinnvoll Punktuelle Erweiterungen vorzunehmen, ohne das Gesamtkonzept zu gefährden. Für eine konkrete Implementierung hier kann man 1 2. ANPASSUNGEN DER JVM Programmierung hat sich etabliert. Aber manchmal gibt es den Wunsch Funktionalitäten aus mehr als einer Klasse in einer neuen Klasse zu vereinen. Dynamische Sprachen zeichnen sich unter anderem dadurch aus, dass sie ihren Programmablauf zur Laufzeit verändern können und Schnittstellen sich zur Laufzeit ändern können. Dazu unterstützen manche Programmiersprachen die Mehrfachvererbung. Diese hat jedoch einen Nachteil: Es können Methoden über unterschiedliche Pfade mehrfach geerbt werden Beide Problemstellungen werden nun an einem Beispiel erläutert. Darauf folgt eine weitere Anforderung an die Java Virtual Machine bezüglich Methoden. 2.1. Klasse A DYNAMISCHE AUFRUFE .Klasse B In 1.2 wurde bereits angedeutet das es sinnvoll sein könnte in einem Abrechnungssystem die Preisfunktion dynamisch anzupassen. Konkret könnte man hier eine Online Shopping System betrachten. Klasse D Hier erbt die Klasse D die Methoden der Klasse A über die Klassen B und C. Es gibt also Mehrdeutigkeiten. Es wird immer wichtiger für Händler schnell auf den Markt zu reagieren und somit auch eine flexible Preispolitik zu führen. Nach dem Händler A in seinem Shop Kampfpreise eingeführt hat, sieht sich Händler B im Zugzwang und muss nun auch eine Aktion anbieten. Dabei denkt er an eine „drei zum Preis von zwei“ Aktion. Sein System unterstützt eine derartige Rabattierung jedoch nicht. Er kann bisher nur auf jeden Artikel einen festen Prozentsatz gewähren. Diese Mehrdeutigkeiten sollen mit Traits vermieden werden. Traits kann man sich wie Puzzle Teile vorstellen. Mehrere Puzzle Teile ergeben ein Gesamtbild. Zum Beispiel besteht eine Klasse, welchen einen Kreis abbilden soll, aus einem Teil, welcher die Eigenschaften des Kreises repräsentiert und einem Teil, welcher die Visualisierung implementiert. Um das System auf solche eine Aktion um zu rüsten, müssten die Entwickler im Shopping System die Preisfunktion anpassen. Danach muss das System runter gefahren werden, die neue Software eingespielt werden und dann das System erneut gestartet werden. So fern die Aktion 1-2 Wochen laufen soll, kann man die jeweiligen Umstellungen in der Nacht durchführen. Es werden also zwei Traits benötigt. Der Trait Circle stellt fertig implementierte Methoden zur Verfügung, mit denen man den Umfang und die Fläche eines Kreises berechnen kann. Der Trait CircleDrawing stellt eine draw Methode zur Verfügung, welche die Visualisierung für eine spezifische Technologie übernimmt. Jetzt möchte Händler B allerdings daraus eine Tagesaktion machen. Morgen soll etwas anderes kommen. Genaues weiß er erst am Morgen. Seine Kunden sollen das Gefühl haben etwas zu verpassen, wenn Sie nicht jetzt etwas kaufen. Traits sind Zustandslos und haben somit keine Variablen. Beiden Traits fehlt also die Fähigkeit den Radius speichern zu können und stellen daher Methoden ohne Implementierung bereit, um auf fehlende Informationen zugreifen zu können. z.B. getRadius(). Die Methode getRadius() muss nun beim zusammenfügen der Traits in einer Klasse implementiert werden. Dabei kann die Implementierung auch durch die Einbindung eines weiteres Traits geschehen. In diesem Fall muss die Methode jedoch durch die Klasse selbst implementiert werden. Neu Implementierung der Preisfunktion und Deployment der neuen Software sind für dieses Szenario etwas zu starr. Daher ist es hier wünschenswert einen dynamischen Eingriffspunkt zu haben, mit dem man zur Laufzeit die Preisfunktion überladen kann. 2.2. Klasse C TRAITS Die einfache Vererbung in der Polymorphen Wenn unsere Kreis Klasse nun eine andere 2 Visualisierungstechnologie unterstützen soll, muss lediglich ein anderer Trait eingebunden werden. • invokevirtual Aufruf von Objektmethoden Mit den Traits haben wir nun also ein Baukastensystem mit dem wir das Problem der mehrdeutigen Abhängigkeiten nicht mehr haben. Denn Traits kennen keine Vererbung. • 2.3. • Aufruf von Interface Methoden METHODEN Die Java Virtual Machine ist auf die Benutzung von Klassen ausgelegt. Jedoch sind nicht alle Sprachen objektorientiert. Aber selbst dann könnte es erforderlich sein einzelne Methoden zur Laufzeit nach zu laden, um Klassenmethoden zu überschreiben. invokestatic Aufruf von statischen Methoden • invokespecial Aufruf von Methoden, welche nicht ins obige Konzept passen. Zu den class Dateien sollte es also auch method Dateien geben, welche ausschließlich von Klassen unabhängige Methoden enthalten. Die Dateinamen und die Verzeichnisstruktur ergeben dabei einen Namespace in dem die Methoden organisiert sind. 2.4. invokeinterface 3.1. GEDÄCHTNIS FÜR INVOKE BEFEHLE Wenn man obige Aufrufe zur Laufzeit verändern möchte, kann man dieses mit einem einfachen Ansatz erreichen. Man spendiert ihnen ein Gedächtnis. Immer dann wenn dieses leer ist, wird das Standardverhalten, wie man es gewohnt ist, ausgeführt. Um einen Aufruf um zu lenken muss in diesem Gedächtnis ein Referenz zu einer Methode hinterlegt werden. DYNAMISCHES NACHLADEN VON CODE Im weiteren muss der Bytecode Loader für die method Dateien angepasst sein und auch die Möglichkeit bieten Code zur Laufzeit nachzuladen. Dies kann z.B. durch eine Überwachung des Programmverzeichnisses geschehen. Bei Veränderung bzw. hinzufügen von Dateien wird der enthaltene Code in das laufende Programm eingefügt. Sofern vorhandener Code überschrieben wird muss der Bytecode Loader auch eine Versionsverwaltung führen und somit nur für neu erstellte Objekte den neuen Code benutzen. Alte Objekte sollten sich möglichst unverändert verhalten. Wenn wir mit diesem Kniff die bestehenden invoke Befehle verändern, dann bekommen wir allerdings wieder ein Sicherheitsproblem. Daher sollten neue invoke Befehle eingeführt werden: Denkbar wäre auch über eine Annotation zu erlauben auch vorhandene Objekte dem neuen Code entsprechend anzupassen. • invokedynamicvirtual • invokedynamicinterface • invokedynamicstatic • invokedynamicspecial Weiterhin wäre ein Kommandozeilentool hilfreich mit dem man eine laufende Virtual Machine manipulieren und Byte Code nach laden kann. Damit kann man Bytecode erzeugen, welcher nur an bestimmen Punkten zur Laufzeit verändert werden kann. 3. IMPLEMENTIERUNG DYNAMISCHER AUFRUFE Für die unabhängigen Methoden wird eigentlich auch noch eine Möglichkeit benötigt diese explizit aufzurufen, wenn sie nicht über die dynamische Zuordnung oben aufgerufen werden. Daher fehlt noch der Befehl In der Java Virtual Machine gibt es vier invoke Befehle, welche für die Ausführung eines Befehls verantwortlich sind: invokedynamicmethod 3 Jedoch gelten auch hier die selben Sicherheitsbedenken und es müsste daher auch der Befehl Gültigkeitsbereich angegeben werden. 4. IMPLEMENTIERUNG VON TRAITS invokemethod folgen. 3.2. Traits könnte man nun nativ implementieren, jedoch aufgrund ihrer Eigenschaft nur Methoden zu implementieren kann man auf bestehenden Konzepten in der Java Virtual Machine aufbauen. Eigentlich sind Traits Interfaces, welche teilweise Implementierungen für ihre Methoden bereitstellen. MANIPULATION ZUR LAUFZEIT Referenzen auf neue Methoden werden in den meisten Fällen nicht auf im ursprünglichen Code vorhandene Methoden gesetzt. Für einen Trait benötigt man die Möglichkeit unabhängige Methoden mit einem Interface in Verbindung zu bringen. Es handelt sich hier um Interface Aufrufe die auf Methoden umgelenkt werden sollen. Daher kommt hier invokedynamicinterface, statt dem alten invokeinterface in Frage. Der Zeiger wird zur Laufzeit gesetzt, also genau dann wenn die dazugehörigen Method Dateien geladen werden und über die Kontext Information mit dem Interface verbunden werden. Wenn einzelne Methoden einer Klasse überschrieben werden sollen, sollte der Namespace der nach zu ladenen Methode den Packagepfad und Klassennamen enthalten. Dann gäbe es einen klaren Bezug zwischen der Methode und der Klassen-/Objektmethode. Die neue Referenz kann dann in das Gedächtnis des invoke Befehls geschrieben werden. Dabei ist nun zu unterscheiden, ob nun jeder dynamische Aufruf eine eigene Referenz haben kann oder ob es für den Methodenaufruf an sich eine zentrale Referenz im Speicher geben soll. Diese Betrachung ist an dieser Stelle bezüglich der Performance wichtig. Beide Lösungen haben Vorteile. Eine zentrale Referenz ist einfacher zu pflegen. Verteilte Referenzen müssen erst gefunden werden. Jedoch macht es dann keinen Sinn alle Referenzen auf den selben Wert zu setzen. Es könnte also nützlich sein den Kontext der neuen Methode genauer zu definieren. Und zwar nicht nur darüber, welche Methode überladen werden soll, sondern auch an welcher Stelle im Programm. Der Implementierung der neuen Methode muss also ein Gültigkeitsbereich mitgegeben werden. Möchte man nun die Traits für die Verwendung zusammensetzen muss entweder eine Komposition zum Zeitpunkt der Übersetzung des Programmes definiert werden oder man muss diese Komposition zur Laufzeit zusammen setzen. Dafür muss es möglich sein Interfaces zur Laufzeit zu einer Klasse hinzuzufügen. Der Vorteil ein Interface zur Laufzeit in ein Objekt einzufügen besteht darin, dass man keine leere Implementierung vorhalten muss. Möchte man z.B. dynamisch zur Laufzeit eine geeignete Visualisierung für ein geometrisches Objekt auswählen, müsste mit der statischen Methode zur Übersetzungszeit das Trait Interface mit einer leeren Methode implementieren. Das kann dazu führen, dass diese leere Methode tatsächlich produktiv genutzt wird. Fügen wir das betroffene Interface erst bei der Verlinkung mit dem gewünschten Trait durch, ist eine Verwendung der leeren dummy Methode ausgeschlossen. Somit eröffnen sich drei verschiedene Wege eine neue Methode in den Programmablauf einzuschleusen: 1. Der Byte Code Loader findet eine neue method Datei und setzt entsprechend dem Namespace alle Referenzen. 2. Der Byte Code Loader findet in der method Datei einen Gültigkeitsbereich und setzt diesem entsprechend die Referenzen. Problematisch dabei sind dann allerdings noch die Typprüfungen, wenn man dann eine Methode der Kompositionsklasse aufruft, welche aber erst durch einen Trait bereit gestellt wird. 3. Die method Datei wird über ein Kommandozeilen Tool in die JVM geladen und mit diesem kann auch ein Es ergeben sich somit Situationen in denen Ausnahmefehler auftreten können. Das aufrufen 4 einer leeren Referenz könnte jedoch vom System abgefangen werden, in dem geprüft wird, ob mit dem Interface auch dazu gehörige Methoden geladen werden bzw. ob passende Methoden in der Klasse vorhanden sind. 5. FAZIT Wenn man den invoke Methoden Block um dynamische Aufrufe ergänzt lassen sich bereits einige dynamische Konzepte umsetzen. Die Art und Weise mit der eine Methode zur Laufzeit überladen wird, sollte recht bedacht gewählt werden. Wenn man es zulässt nur einzelne Aufrufe an einer ganz bestimmten Stelle im Programm zu ersetzen, kann dabei die Übersichtlichkeit verloren gehen. Es könnte unklar sein, welche konkrete Funktion das Programm zu einem bestimmten Zeitpunkt eigentlich erfüllt. Insgesamt erhält man mit obigen Ansätzen kein völlig neues System, aber einige sinnvolle Erweiterungen eines etablierten und getesteten Systems. Quellen 1. http://openjdk.java.net/projects/mlvm/subpr ojects.html 2. http://blogs.sun.com/jrose/entry/notes_on_a n_architecture_for 3. http://www.parrot.org/ 4. http://www.dalvikvm.com/ 5. JSR 292: http://jcp.org/en/jsr/detail?id=292 5