Vom Beginn an war ein Ziel in ByteCodeDeluxe, die Struktur des Bytecodes deklarativ auszudrücken. Services, wie zum Beispiel ein Bytecode Parser, arbeiten auf dieser Struktur um ihre Arbeit zu verrichten. Auf diese Weise wird vermieden, dass die Struktur des Bytecodes an verschiedenen Stellen dupliziert wird (zum Beispiel einmal für die Lese- und einmal für die Schreibroutine).
External DSL
Dabei wurden verschiedene Ansätze durchdacht. Eine Möglichkeit wäre eine externe DSL gewesen. Die Aufgabe an sich ist schon reizvoll. Ich habe in der Vergangenheit mit ANTLR und JavaCC gearbeitet und man landet schnell bei dem Gedanken ein vergleichbares Tool für Binärdateien zu implementieren. Dies aber ist definitiv "out of scope" und würde das angestrebte Ergebnis, einen Java-ByteCodeEditor, deutlich hinauszögern. Schnell würde die Bibliothek zum Hauptaugenmerk der Implementierungsarbeit werden und Wünsche nach einem Editor und anderem Toolsupport würden laut. Deswegen wurde dieser Ansatz verworfen.
JDT - PropertyDescriptor
Die Idee zur letztlich verwendete Implementierung stammt aus dem JDT. Eine Sourcedatei wird hier durch einen AST (im Package "org.eclipse.jdt.core.dom") beschrieben. Die Elemente diesen ASTs, zum Beispiel "MethodDeclaration", "NumberLiteral", "WhileStatement", tragen jeweils wiederum einen sogenannten "PropertyDescriptor". Diese Descriptoren beschreiben den grundsätzlichen Aufbau der Elemente. Das sieht dann im Code, hier für ein WhileStatement, so aus:
/** while( Expression ) Statement */ public class WhileStatement extends Statement { public static final ChildPropertyDescriptor EXPRESSION_PROPERTY = new ChildPropertyDescriptor(WhileStatement.class, "expression", Expression.class, MANDATORY, CYCLE_RISK); public static final ChildPropertyDescriptor BODY_PROPERTY = new ChildPropertyDescriptor(WhileStatement.class, "body", Statement.class, MANDATORY, CYCLE_RISK); private static final List PROPERTY_DESCRIPTORS; static { List propertyList = new ArrayList(3); createPropertyList(WhileStatement.class, propertyList); addProperty(EXPRESSION_PROPERTY, propertyList); addProperty(BODY_PROPERTY, propertyList); PROPERTY_DESCRIPTORS = reapPropertyList(propertyList); }
Jedes Element des DOMs ist also in der Lage über seine statische Struktur Auskunft zu geben (es hat einen Namen, weiß ob es optional ist usw.). Es gibt Methoden wie "getStructuralProperty", welcher man einen Descriptor gibt und man bekommt das echte AST-Element, welches mit dieser Struktur im fraglichen Node verknüpft ist. Letztlich also eine Metabeschreibung des ASTs. Nun kann man Logik auf dieser Metabeschreibung implementieren anstatt sie direkt auf dem AST zu implementieren, die AST-Struktur leakt also nicht nach draußen.
ByteCodeDeluxe 0.1 - Structure
In ByteCodeDeluxe sind die PropertyDescriptoren ersetzt worden durch die Model-"Structure". Genau wie ein PropertyDescriptor im JDT beschreibt eine Structure einen Elementtyp des Baumes. Mit Hilfe dieser Metaebene der Beschreibung läßt sich die Struktur des Bytecodes relativ kompakt darstellen. Um zum Beispiel die Struktur einer "Method-Info" (hier der Weg zur Spec) abzubilden bedarf es folgenden Codes:
/** method_info { * u2 access_flags; * u2 name_index; * u2 descriptor_index; * u2 attributes_count; * attribute_info attributes[attributes_count]; * } */ public class MethodInfo extends AbstractNode { public static final FlagStructure ACCESS_FLAGS = new FlagStructure("access_flags", 2, MethodFlags.values()); public static final ArrayIndexStructure NAME_INDEX = new ArrayIndexStructure("name_index", 2, ClassFile.CONSTANT_POOL); public static final ArrayIndexStructure DESCRIPTOR_INDEX = new ArrayIndexStructure("descriptor_index", 2, ClassFile.CONSTANT_POOL); public static final ArrayLengthStructure ATTRIBUTES_COUNT = new ArrayLengthStructure("attributes_count", 2); public static final ArrayStructure ATTRIBUTES = new ArrayElementCountingStructure("attributes", new StringSelectionStructure("attribute", 2, ClassFile.CONSTANT_POOL, AttributeInfo.ATTRIBUTES_MAP), ATTRIBUTES_COUNT, false); public static final Structure[] STRUCTURE = new Structure[] { ACCESS_FLAGS, NAME_INDEX, DESCRIPTOR_INDEX, ATTRIBUTES_COUNT, ATTRIBUTES }; public Structure[] getStructure() { return STRUCTURE; } public String getName() { return ((ArrayIndex) getChild(NAME_INDEX)).getTarget(). getValueString(); } }
Mit diesem Werkzeugkoffer ist die Spec relativ schnell runter geschrieben. Ein Parser traversiert dann nur noch die (statische) Struktur und instantiiert Nodes die dann als Model dienen und die Daten speichern. Dadurch haben die Nodes allerdings eine Doppelfunktion ... einerseits sind sie die Strukturbeschreibung des Bytecodes, andererseits dienen sie als Modelelemente und halten die eingelesenen Daten. Durch diese Doppelfunktion neigen sie dazu Funktionalität anzuziehen und damit zu Hybriden zu werden (nicht ganz Klasse nicht ganz Datenstruktur ... an dieser Stelle möchte ich einmal einen Hinweis auf das wundervolle Buch "Clean Code" geben). Auch wird man den Gedanken nicht los, ob es nicht doch noch ein wenig kompakter geht.
ByteCodeDeluxe 0.2 - Structure
In der Version 0.2 sollen die großen zwei Schwächen der alten Struktur, Doppelfunktion der Strukturnodes und suboptimale Lesbarkeit der Bytecodebeschreibung, ausgemerzt werden. Die Beschreibung soll nicht mehr als Datencontainer dienen und deutlich verkürzt werden.
Die Idee ist, die Struktur mit Hilfe von Java-Klassen zu beschreiben und reflektiv auszulesen, also den Javasource quasi direkt als deklarative Beschreibungssprache zu nutzen. Annotations können verwendet werden um das Modell mit zusätzlichen Informationen anzureichern. Services können dann auf dieser Struktur arbeiten.
Dieses Vorgehen bringt offensichtlich einen Performanceoverhead mit sich. Aber: auch der alte Ansatz ist mit Sicherheit nicht besonders performant, für die Modelle werden sehr viele Objekte instantiiert. Außerdem ist die Performance in diesem Kontext von untergeordneter Bedeutung. Für den Editor werden immer nur einige wenige class-Dateien geöffnet und wenn das eine hundertstel Sekunde (oder auch eine zehntel Sekunde) dauert ist das nicht weiter schlimm.
Wie könnte eine solche Annotation nun konkret aussehen? Ich bin noch dabei an den Details zu arbeiten, aber ich stelle mir in etwa folgendes vor:
/** method_info { * u2 access_flags; * u2 name_index; * u2 descriptor_index; * u2 attributes_count; * attribute_info attributes[attributes_count]; * } */ @Structure( { "accessFlags", "nameIndex", "descriptorIndex", "attributesCount", "attributes" }) public class MethodInfo { @Flags(MethodAccessFlags.class) U2 accessFlags; @ConstantPoolIndex(ConstantUtf8Info.class) U2 nameIndex; @ConstantPoolIndex(ConstantUtf8Info.class) U2 descriptorIndex; @TableSize(tableName = "attributes") U2 attributesCount; AttributeInfo[] attributes; }
Die Informationsmenge ist in etwa so hoch wie im Beispiel von Version 0.1, allerdings deutlich kompakter geschrieben. Ein Parser würde nun reflektiv die Klassenstruktur mit den Annotations lesen und benutzen um den Input zu parsen und die Modelelemente zu instantiieren.
Mit Hilfe dieser Struktur ist der Code nun beinahe so kompakt als wäre es eine DSL. Nur muss weder ein Parser geschrieben werden, noch ein eigenes Toolset entwickelt werden ... die Java-Reflection-API und jede beliebige Java-IDE reicht als Toolset und die Beschreibung des Bytecodes ist rein deklarativ!

0 Kommentare:
Kommentar veröffentlichen