SwingHelpViewer – ein einfacher Online-Hilfe-Viewer

Seit einiger Zeit schraube ich an diversen Anwendungen auf Basis von Java Swing, die wirklich demnächst die Release-Reife erreichen sollten. Die meisten aus der Reihe “für RISC OS, aber nicht unter RISC OS lauffähig” (da Java-basiert, und es unter RISC OS keine kompetente JVM gibt). Hier kann man ein paar wenige Details zu diesen geplanten Anwendungen nachlesen.

Wie dem auch sei, solche klassischen FatClient-Anwendungen brauchen typischerweise eine Online-Hilfe. Allein um dem Benutzer mitzuteilen, welche 3rd-Party-Bibliotheken genutzt werden und unter welcher Lizenz diese stehen.

Standard unter Java ist für solche Zwecke JavaHelp, vor Urzeiten von Sun entwickelt, aber seit fast so langer Zeit auch nicht weiter gepflegt. Diverse ungefixte Bugs, unklare Lizenzsituation (alle Sourcefiles reden von GPLv2, irgendwo steht “GPLv2 with Classpath Exception”, und es gibt auch Hinweise, dass es dual lizenziert ist sowohl unter GPLv2 als auch unter CDDL), merkwürdige indexbasierte Suche die außer Englisch recht wenig versteht, unflexibel in der Integration. Aber durchaus mit coolen Features für die damalige Zeit, inklusive einer JSP-basierten Server-Variante.

Allein die Lizenzsituation bewog mich, schnell mal was Eigenes zu bauen. Mit dem typischen “wie schwer kann das schon sein” auf den Lippen begann ich während eines langweiligen Fußballspiels (es könnte der VfB Stuttgart involviert gewesen sein), schnell mal einen Hilfe-Viewer ähnlich des JavaHelp-Viewers zu bauen. Ungefähr 10 Stunden später hatte ich einen Viewer, der ein JavaHelp-HelpSet (.hs-Datei) parsen und sowohl TOC als auch Index eines solchen HelpSets erfolgreich darstellen. Es war also tatsächlich nicht so schwer, am längsten hat noch das XML-Gefrickel gedauert, weil ich mir in den Kopf gesetzt hatte, ganz ohne 3rd-Party-Dependencies auszukommen, und das DOM-Zeugs in Java SE ist halt sehr mit-der-Hand-am-Arm.

Code aufräumen, Icons malen und jetzt seit neuestem eine einfache Volltextsuche dazuzubauen hat nochmal ein paar Stunden gekostet, alles in allem schätze ich mal 3 PT an Gesamtaufwand. Swing ist wohl doch nicht so schlecht wie viele seit etwa 1999 nicht müde werden zu behaupten.

Das Ergebnis dieser Bemühungen kann man auf GitHub inspizieren. Und damit es wirklich von jedem auf welche Weise auch immer verwendet werden kann, habe ich als Lizenz die “Unlicense” ausgewählt. Ist bestimmt auch wieder inkompatibel mit irgendwas, aber es ist die freieste Lizenz, die ich gefunden habe.

Wird es für den SwingHelpViewer eine Weiterentwicklung geben? Natürlich liegen einige Erweiterungen auf der Hand (indexbasierte Suche z.B. über Apache Lucene, ein Bookmark-System, Unterstützung für weitere JavaHelp-Features wie “merged help sets” und Direkthilfe-Action und Hilfe-Cursor und so, Unterstützung für andere Hilfedaten wie CHM, HLP oder Eclipse Help, Einbindung anderer Anzeigekomponenten die mehr von HTML und CSS verstehen als JEditorPane), aber der jetzige Zustand dürfte für meine Zwecke wohl reichen.

Wer die Herausforderung sucht, könnte z.B. mal schnell eine Java 1.2-kompatible Version des Codes bauen. Im Moment läuft alles ab Java 7, wenn man den Diamond Operator rauswirft müsste Java 5 gehen, wenn man die Generics rauswirft Java 1.4, und wenn man das XML-Parsing anders löst (wer erinnert sich noch an crimson.jar…), dürfte man bei Java 1.2 angekommen sein. Der Code ist sehr “old-school”, und Swing ist ja im Prinzip seit Java 1.2 unverändert.

Was GitHub angeht, ist SwingHelpViewer mein erstes dort publiziertes Projekt. Wenn ich die jetzige Geschwindigkeit beibehalte, werden meine über die Jahre entstandenen programmiertechnischen Ergüsse spätestens bis 2050 komplett auf GitHub gelandet sein.

Apple setzt auf ARM

Auf der WWDC (die jährliche Apple Entwickler-Konferenz) wurde in der Keynote wie schon seit Längerem allgemein erwartet (erste Gerüchte etwa seit 2011) verkündet, dass zukünftige Macs als CPU ein selbstentwickeltes SoC auf ARM-Basis enthalten werden. Höchstwahrscheinlich ein performanceoptimierter Abkömmling der A12Z-und-Nachfolger-Reihe aus dem iPad Pro.

Viel wurde schon geschrieben über Apples erneuten Pferdewechsel bei der CPU für seine Desktoprechnerlinien – MOS 6502 im Apple I und II, Motorola 68xxx im Mac der ersten Generation, Motorola/IBM PowerPC in den PowerMacs ab Mitte der 90er, und schließlich Intel x86/x64 seit Mitte der Nullerjahre. Und jetzt also ARM. Für alle historisch interessierten schließt sich damit der Kreis, der mit der Ausgründung von ARM Ltd. aus Acorn als Joint Venture mit Acorn und VLSI Anfang der 90er begann, und Apple den ARM610 als CPU im Newton PDA einsetzte – damals mit mageren 20 MHz getaktet, der finale Newton hatte dann einen schnellen StrongARM intus.

Meine Einlassung hier soll auch nicht als Expertenmeinung im MacOS-Universum eingestuft werden – ich habe wenig Plan von und Einblick in MacOS und der derzeit dort so eingesetzten Software. Der Presse entnehme ich, dass im Prinzip ausschließlich mit den MacBooks der Gewinn eingefahren wird und der Rest eher so nebenher läuft. Das traditionelle Profilager der Kreativen, von DTP über Illustrationen bis Bitmap-Bearbeitung scheint im großen Spiel der Dinge inzwischen eher nachrangig. Photoshop, Final Cut Pro, InDesign, Illustrator, QuarkXPress, Premiere Pro, Finale…sowieso allesamt auch unter Windows erhältlich und wohl nicht mehr der entscheidende Faktor bei der Useranzahl und damit auch dem Umsatz. Die Lifestyle-Kundschaft, die einfach alles aus dem Apple-Universum kauft was nicht bei drei auf dem Baum ist – von MacBook über iMac, Apple Watch, HomeKit, iPhone bis iPad und Apple TV – ist der Umsatztreiber.

Was treibt also Apple zu diesem Wechsel, der ja nicht ohne Risiko ist? Ich kann da einige Gründe ausmachen. Wie man die jeweils gewichtet, liegt im Auge des Betrachters. Wir werden erst hinterher schlauer sein.

Zunächst ganz schnöde: der Wechsel spart vermutlich einen Haufen Geld. Intel-CPUs sind ja doch eher teuer, und Apple musste immer Chipsätze und Zusatzchips drumrum entwickeln oder zukaufen. Die ARM-Entwicklung erfolgt ja sowieso aufgrund von iPhone und iPad, warum dann nicht etwas nach oben skalieren. In Zeiten von “viele Cores helfen viel” ist ein ARM-Core da sicher keine unbedingt schlechte Basis. Es heißt ja, dass die letzte Inkarnation des iPad Pro CPU- und GPU-technisch mit den mobilen Core i-CPUs locker mithalten kann.

Wichtig erscheint mir auch, dass Intel in den letzten Jahren seinen ehemaligen Vorsprung bei der Prozesstechnik eingebüßt hat. Die Auftragsfertiger wie TSMC oder Samsung haben Intel mindestens eingeholt, eher überholt. Angeblich hat TSMC den 3nm-FinFET-Prozess 2022 produktionsreif. Allerdings ist das nicht ohne Risiko, denn andere große Auftragsfertiger haben beim großen Technologierennen schon abreißen lassen müssen – Globalfoundries dürfte der bekannteste Fall sein, früher ganz vorne dabei als Ausgründung von AMD, aber inzwischen nur noch Massenfertiger ohne Spitzenfertigungstechnologie. Jedenfalls ist die Verfügbarkeit von Auftragsfertigern mit topaktueller Prozesstechnologie eine Voraussetzung für den Erfolg von “fabless CPU designers” wie ARM, Qualcomm, Apple oder AMD. Dass dieses Pendel mal wieder in Richtung Intel zurückschwingt ist jetzt nicht völlig ausgeschlossen.

Viel wird über die Vereinheitlichung der Plattformen iOS und macOS geschrieben. Aus meiner Sicht ist das ein schwaches Argument, denn wo genau ist denn hier die zugrundeliegende CPU-Architektur entscheidend? Entscheidend ist doch hier vielmehr, ob sich die Benutzeroberflächen angleichen und das Software-Ökosystem aus Bibliotheken und Frameworks möglichst identisch ist. Ob dann am Ende der Compiler x86-Code oder ARM-Code draus macht, ist doch weitgehend irrelevant. Vielleicht werden die allermeisten Programme bis in 10 Jahren sowieso alle im Browser laufen und in JavaScript und WebAssembly geschrieben sein, dann haben sich solche Details eh erübrigt.

Ein aus meiner Sicht wichtigeres Argument ist, dass sich Apple der einfachen Vergleichbarkeit – besonders was den Preis angeht – seiner Hardwareplattform entzieht. Heutzutage ist komplett transparent, dass Apple gegenüber der x86-Welt für seine MacBooks einen nicht unerheblichen Aufpreis nimmt, der sich nicht in erhöhter CPU- oder GPU-Leistung niederschlägt. Der Wechsel zu ARM nützt hier zweifach: zum einen kann Apple sehr einfach Coprozessoren aller Art im SoC unterbringen, die speziell auf die neueste Modeerscheinung der IT hingedengelt wurde – sei es Spracherkennung, Bildverarbeitung, Video-Encoding oder irgendwas-mit-KI. Zum anderen kann Apple zukünftige Herausforderungen durch einen cleveren Mix aus Software und Hardware erschlagen, ohne dass jemand genau abgrenzen kann, ob jetzt die geniale Programmierung oder die geniale Hardware letztlich verantwortlich ist.

Dadurch, dass Apple von ARM eine Architektur-Lizenz hat, also völlig eigenständige Cores entwickelt und nicht auf die fertigen Cortex-Axx- oder Neoverse-Cores zurückgreifen muss, können die CPUs für MacOS und den muss-nicht-unbedingt-allzu-stromsparend-sein-Anwendungsfall maßgeschneidert werden. Das ist bei iOS ja auch passiert – während die Android-Fraktion eher auf viele Core gesetzt hat, hat Apple eher die Single-Core-Leistung in den Vordergrund gestellt. Welche Reserven die Architektur hat, wenn man die TDP in Richtung Intel Core i9 erhöhen kann, bleibt abzuwarten. Der A12Z mit 8 Cores in big.LITTLE-Konfiguration, je 128 KiB 1st level instruction und data cache und 8 MiB 2nd level cache ist ja noch eher auf Sparsamkeit und lüfterloses Design ausgelegt. Bechmarks sahen den A12Z ja ungefähr auf Augenhöhe eines Mobile-i5 mit integrierter Intel-Grafik. Technisch ist das der Stand von 2018, da der A12Z nur ein leicht erweiterter A12X ist. Mal sehen, mit was Apple um die Ecke kommt, wenn es ernst wird mit dem ersten MacBook-on-ARM.

Apple hat in der Vergangenheit durch das Backen ganz eigener Brötchen ja große Erfolge gefeiert und ein immer geschlosseneres Ökosystem geschaffen. iOS, Objective-C, Swift, Metal, Apple Store, iTunes, Apple TV, HomeKit – solange die Nutzeranzahl eine kritische Masse überschreitet und das Ökosystem technisch und aus Anwendersicht plausibel bleibt, ist es eine clevere Art von “Vendor Lock-In”.

Ein Risiko der Apple-Strategie steckt natürlich in dem, was in der Industrie unter “zu große Fertigungstiefe” läuft. Apple muss alles selbst entwickeln, vom Betriebssystem (iOS und MacOS) über CPU, GPU und Zusatzprozessoren bis hin zu Compilern (Objective-C, Swift) und natürlich der Hardware selbst. Das birgt eine irre Komplexität und hohen Aufwand in sich, um in jedem einzelnen Bereich Spitze oder zumindest nahe dran zu sein. Firmen, die das früher mal versucht haben, waren dauerhaft nicht erfolgreich (Sun, SGI, Acorn, mit Abstrichen auch die klassischen Heimcomputerhersteller wie Commodore und Atari).

Eine gewisse Ironie steckt im Zeitpunkt des jetzt angekündigten Wechsels. Gerade hat – dank AMD – die x86-Welt wieder deutlich an Schwung gewonnen nach fast einem Jahrzehnt gefühlter Stagnation. Ein wenig vergleichbar mit dem damaligen Wechsel von PowerPC zu Intel, als kurze Zeit später IBM ein wahres Feuerwerk an High-Performance-PowerPCs abbrannte. Aber wie wird es sich längerfristig entwickeln? Apple hat auf jeden Fall genügend finanzielle Ressourcen, um seine ARM-SoCs auf Augenhöhe mit den x86-CPUs von Intel und AMD weiterzuentwickeln. Aber das alleine ist keine Garantie, wie man gerade an Intel sieht, die trotz gigantischer finanzieller Ressourcen eher alt gegen AMD aussehen. Kopf schlägt Kapital, zumindest manchmal – oder wie jeder Fußballfan weiß: “Geld schießt keine Tore”, und trotzdem heißt der Deutsche Meister auch 2020 wieder Bayern München. Und andererseits ist Freiburg auf einem einstelligen Tabellenplatz in der Bundesliga, während der Hamburger SV, der VfB Stuttgart und Hannover 96 in der zweiten Liga spielen. Es bleibt also spannend.

Stümpern mit Niveau

Ab und zu – die Frequenz ist uneinheitlich, die Rahmenbedingungen unbekannt – neige ich dazu, irgendein IT-Thema mal genauer zu betrachten und ein paar Experimente durchzuführen. Quasi um immer wieder das “innere Komplexitätsgefühl” neu zu justieren. Ich stelle eine gewisse Häufung beim Thema “Protokolle” fest, vielleicht die masochistische Ader die mich schon zur Implementierung einer CD-/DVD-/BluRay-Brenner-Software gebracht hat. Ziel ist immer, bei einer Sache von Quasi-Null-Wissen auf mindestens 3 zusätzliche Seiten im persönlichen Handbuch des nutzlosen Wissens zu kommen. Quasi qualifiziertes Halbwissen, das jede Fachdiskussion unter einer Stunde locker überlebt.

Diesmal das Thema: IPP, das “Internet Printing Protocol”, das im Rahmen einer Diskussion im Kontext meines Lieblingsbetriebssystems RISC OS eher zufällig aufgekommen ist. Früher dachte ich, IPP sei quasi nur “Druckdatenstrom per HTTP an den Drucker übermitteln”. Sowas wie “JetDirect über HTTP”. Oh Mann, lag ich falsch. Oder besser: es ist eine sehr simplifizierte Beschreibung der Wirklichkeit, und qualifizierte mich damit als “Quasi-Null-Wissenden”.

In Wahrheit hat IPP interessantes Potenzial für die RISC OS-Welt, nämlich “driverless printing”. Man kann per standardisierten Requests den Drucker zu seinen Eigenschaften befragen (Papierzufuhroptionen, Nachbearbeitungsoptionen, Duplex oder nicht und wenn ja wie), zu seinem Status (Tonerstand/Tintenstand, Papierstau etc.) und es gibt ein plattformübergreifendes Rasterformat (“PWG Raster Format” – PWG ist die “Printer Working Group”, die hinter IPP steht), das zum Druck verwendet wird. Zusätzlich vermelden netzwerkverbundene Drucker ihre Anwesenheit über das Bonjour-Protokoll, aber auch direkt verbundene USB-Drucker werden unterstützt. Das Gesamtkunstwerk nennt sich IPP Everywhere. Also die Lösung aller Probleme der Art “klar gibt es da diesen Drucker XYZ, aber für Betriebssystem ABC gibt es leider keine Treiber” – heute behilft man sich da (zumindest im RISC OS-Markt) mit der oberen Preisklasse der PostScript- und PDF-fähigen Drucker, muss aber immer noch tricksen um mit speziellen Druckerfeatures arbeiten zu können.

Die IT wäre nicht die IT, wenn nicht zwei konkurrierende andere Lösungen gleichzeitig um die Gunst des Anwenders (interessiert den das eigentlich?) buhlen würden: AirPrint (von Apples Gnaden) und Wi-Fi Direct Print. So weit, so ärgerlich. Zurück zu IPP. Dessen Charme ist die Verfügbarkeit eines gut dokumentierten Standards, bei den beiden Konkurrenten sieht es da nicht ganz so gut aus, dafür waren diese früher am Markt und sind (deshalb?) deutlich weiter verbreitet – IPP Everywhere wird heute eigentlich nur von einer Reihe von HP-Druckern unterstützt.

Wie auch immer, ich fasste ein Ziel ins Auge: mit meinem IPP-fähigen (aber natürlich nicht IPP Everywhere-kompatiblen, weil Baujahr 2011) Oki C530 ein PDF zu drucken, indem über IPP der PDF-Datenstrom an den Drucker geschickt wird.

Wie anfangen? Der IPP-Guide erzählt von 40 Spezifikationen mit insgesamt fast 2000 Seiten, das schien jetzt etwas ambitioniert. Aber es gibt JIPP, eine Java-Implementierung von IPP von HP, verfügbar auf GitHub. Unter der guten MIT-Lizenz. Da waren Beispiele dabei. Wie schwer kann es also sein?

Erste Hürde: das Github-Projekt hostet nur Source-Releases. Kein Problem, man kann ja selber bauen. Stellt sich raus: ist Kotlin-Code, gebaut mit Gradle. Hm. Bin ich jetzt für beide Dinge nicht so der Experte, vielleicht ein Experiment für ein anderes Mal. Google führt mich zu einer sinnvollen Paketierung des Codes inklusive aller seiner Abhängigkeiten (wie der Kotlin-Runtime – auch nicht gerade schmal das Dingens). Also schnell ein IDE-Projekt aufgesetzt und los geht’s. Ich arbeite mit Version 0.6.16 – nur falls jemand ein paar Jahre später über diesen Artikel stolpert und bis hierher durchgehalten hat.

Das jprint-Beispiel (inzwischen übrigens – ohne meinen Input und erst vor wenigen Stunden! – signifikant verbessert, an einigen der Stolperfallen) erst mal als Basis genommen. Ein PDF an die bekannte IPP-URL des Druckers geschickt. Eine dubiose Exception war das Ergebnis (aus dem HTTP-OutputStream: java.io.IOException: Error writing request body to server) – wies darauf hin, das irgendwas mit der URL nicht stimmte. OK. Ein paar Varianten durchprobiert (offenbar gibt es typische Implementierungen der Form ipp://irgendneip/ipp und ipp://irgendneip/ipp/lp und http://irgendeineip/ipp und http://irgendeineip:631/ipp – an dieser Stelle die Nebenerkenntnis, dass IPP schlicht HTTP-mit-631-als-Default-Port ist). Manchmal den gleichen Fehler, manchmal einen anderen Fehler (java.net.SocketException: Software caused connection abort: recv failed). Dubios. Weiter rumprobiert. Stellt sich raus: auch bei derselben URL kommt manchmal die eine Exception, manchmal die andere. Aha. Dann doch kein Connection-/URL-Problem? Mal den Code genauer anschauen. Als Transportmechanismus wird eine Klasse namens “IppPacket” verwendet. Da gibt es einen alternativen Konstruktor, der auch eine “ippVersion” (ein int wie es sich gehört) entgegen nimmt. Könnte es sein, dass mein Drucker (aus der Zeit von IPP v1.1) mit dem Default von JIPP (IPP v2.x) einfach nicht klarkommt? Mehrfaches Ausführen des Codes mit stets derselben URL führt tatsächlich manchmal zu einer Exception-losen Verarbeitung des Kommandos mit der Fehler-Rückmeldung “server-error-version-not-supported”, was den Verdacht erhärtet.

Etwas später ist klar: dummerweise erlaubt der Konstruktor von IppPacket, der eine explizite Versionsangabe erlaubt, nicht dieselben Typen an Restparametern, insbesondere nicht den “Operation”-Enum, sondern auch nur einen int. OK, ein paar RFCs zum IPP gelesen und herausgefunden, dass das Kommando getPrinterAttributes 0x0B ist und printJob 0x02. Minuten später sehe ich, dass der Enum innen drin die ints eh auch definiert hat. Grmpf. Ist bestimmt so ein Kotlin-Automatismus. Bleibt die Frage: wie spezifiziert man jetzt in einem int eigentlich “1.1” als Version? Na per 8+8 Bit natürlich. Also 0x0101 für meine Zwecke. Prima. Erstmal getPrinterAttributes probieren statt gleich das PDF zu schicken. Das Kommando wird jetzt zuverlässig verschickt, aber JIPP kann das Resultat nicht unfallfrei parsen – ob das eine Schwäche von JIPP ist oder eine Abweichung von der Spezifikation seitens meines Druckers ist noch ungeklärt, aber Gott sei Dank hat der JIPP-Programmierer bei solchen Parse-Fehlern daran gedacht, die Rohdaten des Replys, der nicht parsebar waren, herauszudumpen. Also konnte ich einfach inspizieren, was mein Drucker so als Fähigkeiten zurückmeldet.

Nachdem das Kommando-Schicken nun völlig problemlos funktionierte, machte ich mich an die eigentliche Aufgabe, den Druck eines PDFs – der Oki unterstützt sowohl direktes Drucken per PostScript als auch per PDF, und PDFs finden sich auf der Platte entschieden leichter. Ein einseitiges Dokument identifiziert und per offiziellem PDF-MIME-Type “application/pdf” zum Drucker geschickt. Kommt zurück: “client-error-attributes-or-values-not-supported”. Eine sprechende Fehlermeldung! Sehr schön. Inspektion der bei getPrinterAttributes zurückgemeldeten Fähigkeiten zeigt: “application/pdf” wird auch gar nicht akzeptiert, nur “application/octet-stream”, “application/vnd.hp-PCL” oder “application/postscript”. Einfach mal “application/octet-stream” geraten, und siehe da: es wird gedruckt.

Na das war ja einfach.

Zu klärende offene Fragen: 1. Nützt mir das jetzt was für IPP unter RISC OS? 2. Wie erzeuge ich möglichst einfach das PWG-Raster-Format aus einem beliebigen Quellformat? 3. Warum drucke ich nicht einfach PDF über einen CUPS-Druckerserver, der ja auch IPP out-of-the-box kann?

Antworten: 1. Noch nicht. 2. Sieht nicht so schwierig aus, auch keine ultrakomplexe Kompression, sondern simples PackedBits-Format und anständig dokumentiert. 2a. Aber auch JPEG wird als Format im Standard zwingend vorausgesetzt – noch einfacher! 3. Weil das ja jeder kann.

Interessanter Beifang: AppImage als Paketiervariante für Linux-Anwendungen. Spannend, kannte ich noch nicht.

Graal(VM) – Versuch eines Überblicks

Die Themen Graal bzw. GraalVM beschäftigen mich schon eine ganze Weile, und wie ich erneut auf dem Java Forum Stuttgart 2019 bemerkt habe, hat das Graal-Universum derart viele Facetten, die sich zudem alle in einem unterschiedlichen Stand der Nützlichkeit und Nutzbarkeit befinden, so dass ich hier mal versuchen werde, einen Überblick zu geben.

Das Graal-Universum (auch gerne “GraalVM ecosystem” genannt) ist ein Oracle-Projekt, hervorgegangen aus zahlreichen Forschungsprojekten der Oracle Labs in Kooperation mit der Uni Linz – früher hieß das Projekt “Metropolis” (und noch früher Maxine VM) und hatte ursprünglich neben Forschung angeblich nur das Ziel, einen JIT-Compiler für die Hotspot-JVM in Java neu zu schreiben, weil vor allem der in C++ geschriebene “C2” (auch als “HotSpot-Compiler der Server-JVM” bekannt) das Ende seiner Erweiterbarkeit und Wartbarkeit erreicht hatte. Inzwischen ist sehr viel mehr daraus geworden.

Übrigens steht “Graal” für “General Recursive Applicative and Algorithmic Language”. Was ein Glück, dass niemand auf die daraus eigentlich resultierende Schreibweise “GRAAL” besteht. Ein weiterer wichtiger Hinweis: das Wording wurde über die Zeit verändert, bitte nicht verwirren lassen von Graal vs. GraalVM vs. Graal-Compiler vs. Graal-JIT. Derzeit scheint man sich auf “GraalVM” für das Gesamtpaket entschieden zu haben, mit “Graal” als Kurzform, wenn man drüber spricht.

Die (JVM-)Welt vor GraalVM

Viel wurde geschrieben zur JVM, zu Bytecode, zu Interpretern, zu JIT-Compilern und der dynamischen Recompilierung à la HotSpot. Vor GraalVM war die HotSpot-JVM von Sun/Oracle die übliche Laufzeitumgebung für Bytecode aller Art (Dinge wie Excelsior JET, IBM/Eclipse J9, Azul Zing oder sogar gcj aus früheren Zeiten waren und sind ja eher Exoten). Die HotSpot-JVM hat im Prinzip drei Möglichkeiten, um Bytecode zu behandeln: entweder er wird interpretiert, oder er wird mit dem C1-Compiler (auch “client compiler” genannt) in Maschinencode übersetzt (das geht schnell, ist aber wenig optimierter Code), oder er wird mit dem C2-Compiler (auch “server compiler” genannt) in Maschinencode übersetzt (das dauert etwas länger, ist aber sehr gut optimiert, und kann zur Laufzeit, wenn eine Methode beispielsweise sehr häufig aufgerufen wird, weiter optimiert werden – das ist der “HotSpot”-Aspekt der JVM). Das Zusammenspiel aus C1-C2 wird oft auch “tiered compilation” genannt.

Nun ist das C++-Duo C1-C2 schon gut 20 Jahre alt und nach verschiedenen Aussagen diverser Beteiligter am Ende von Weiterentwicklung und Wartbarkeit angekommen. C2 wurde in den letzten Jahren nur noch mit spitzen Fingern angefasst, es wurden keinerlei Verbesserungen an der Architektur oder der prinzipiellen Optimierungsmöglichkeiten vorgenommen, nur Bugfixing und diverse Optimierungen im Bereich Intrinsics (also das Erkennen spezifischer Codepatterns und Verwendung von optimiertem Maschinencode um diese direkt zu ersetzen), was aber als Backend-Optimierung für jede unterstützte Prozessorarchitektur separat gemacht werden musste (oder der Einfachheit halber gleich unterblieb).

Man könnte argumentieren, dass das (also Stabilität und wenige Änderungen) auch gut so ist: durch die Ausführung als native code in der JVM besteht schließlich immer die Gefahr, bei einem unerwarteten Fehler die komplette JVM mit einem Coredump nach einem Segmentation Fault herunterzureißen. Und schließlich muss man ja zugeben, dass der C2 insbesondere Java-Bytecode in wirklich effizienten Maschinencode überführt, der für viele Anwendungen den Performancevergleich mit statisch compilierten Sprachen wie C oder C++ nicht zu scheuen braucht, und teilweise aufgrund seiner dynamischen Natur auf Basis von Informationen, die erst zur Laufzeit zur Verfügung stehen, diese sogar performancetechnisch übertreffen kann.

Diese uns allen bekannte JVM-Welt hat aber natürlich auch bekannte Schwächen, so dass das Festhalten am Status Quo kaum als eine valide Vorgehensweise für die Zukunft erscheint. Da ist zum einen der nicht zu vernachlässigende Performancenachteil beim Startup einer Anwendung – es dauert seine Zeit, bis die entscheidenden “heißen” Codepfade durch C2 entsprechend optimiert wurden, ein Prozess, der oft als “heating up the JVM” oder ähnlich bezeichnet wird. Und während dieser Zeit genehmigt sich die JVM ja durchaus auch erkleckliche Mengen an Speicher, und der Garbage Collector darf regelmäßig aufräumen. So schön das Prinzip einer virtuellen Maschine auch sein mag, in gewissen Anwendungsszenarien wie z.B. Desktop-Anwendungen oder Cloud-Services sind die Nachteile unübersehbar.

Dazu kommt, dass sich mit der Zeit herausgestellt hat, dass die Techniken des C2 zu nah an Java und der Art und Weise, wie javac Bytecode erzeugt, gebaut sind. Das führt zu suboptimalen Ergebnissen bei der Ausführung von Bytecode aus anderen Quellen, sei es Scala, JRuby, Jython oder Clojure.

Und dann ist da natürlich das Problem des “Systembruchs” bei der Weiterentwicklung des JITs – man muss immer zwei Welten verstehen, entwickeln, analysieren und debuggen – die native C++-Welt und die Java-Welt. Qualifiziertes Personal, das in beiden Welten zuhause ist, ist naturgemäß schwer zu finden.

Jedenfalls kann man nicht wegdiskutieren, dass das Großthema Performance in den Teilbereichen “startup” und “footprint” mit der bisherigen Strategie kaum ausreichend gelöst werden kann, vor allem wenn im Serverbereich nicht mehr länger große, langlaufende Prozesse das übliche Architekturmuster sind, sondern stattdessen kurzlaufende, containerisierte, über ihre schiere Anzahl skalierende Microservices. Bisher wurden diese Themen nur im Rahmen des gewohnten Umfelds angegangen – Class Sharing, Modularisierung des JDK, AOT seit Java 9. Mit begrenztem Erfolg.

Graal der JIT-Compiler aka “GraalVM compiler”

Angeblich beginnt die Geschichte von Graal(VM) etwa 2009 mit einer Portierung von C1/C2 nach Java durch Thomas Würthinger. Heute kann man den Graal-JIT-Compiler recht einfach in JDK 8 oder JDK 11 integrieren, kann also von jedem Java-Entwickler sehr einfach ausprobiert werden. So kann man in Ruhe für seinen spezifischen Workload herausfinden, ob die fortgeschrittenen Optimierungstechniken tatsächlich einen Laufzeitvorteil bedeuten, und wie groß dieser gegebenenfalls ausfällt.

Vor geraumer Zeit (Java 8, zunächst aber nicht öffentlich dokumentiert, offiziell dabei seit Java 11) wurde das JVMCI (JVM Compiler Interface, JEP 243) eingeführt, eine (hoffentlich langzeit-)stabile Schnittstelle der JVM zum Andocken alternativer JIT-Compiler. Graal kann genau diese Schnittstelle nutzen.

Wo bietet Graal nun Vorteile gegenüber dem bisherigen C2? Insbesondere die sogenannte “Escape Analysis” scheint sehr viel leistungsfähiger zu sein. D.h. der Compiler ist viel besser beim Erkennen von Objekten, deren Gültigkeitszeit z.B. auf Methodenebene beschränkt ist und deshalb problemlos auf dem Stack statt auf dem Heap allokiert werden kann. Das hat doppelten Nutzen, denn die Stack-Allokation ist schneller und der GC muss hinterher nicht aufräumen. Besonders bei Scala-Code scheint diese Verbesserung für erhebliche Performanceverbesserungen zu sorgen. Aber auch das Inlining von Code soll mit Graal besser funktionieren.

Die Tatsache, dass Graal in Java geschrieben ist, hat interessante Konsequenzen. Zum einen wird nun der JIT-Compiler selbst in der JVM ausgeführt und optimiert sich so mit der Zeit selbst. Speicherallokation für den Compiler findet auf dem JVM-Heap statt anstelle aus dem Native-Pool zu kommen. Da nun auch die Performance des Compilers einer Warm-Up-Zeit unterliegt, verändert sich auch das Performance-Profil zur Ausführungszeit. Und der GC ist natürlich potenziell stärker beschäftigt, weil er sich nun sowohl mit der eigentlichen Anwendung als auch mit dem Compiler beschäftigen muss.

Um dieses Szenario zu nutzen, kann man einfach die GraalVM oder einen der OpenJDK-Builds vom GraalVM-Team nutzen und entsprechend konfigurieren. Im Prinzip ein “drop-in replacement” für das gewohnte JDK.

Und was ist libgraal?

libgraal ist der Versuch, den Graal-JIT-Compiler mittels GraalVM-native-image (dazu später mehr) in eine Bibliothek zu überführen, die das Performance-Problem des Compile-Vorgangs des Graal-JIT-Compilers in der JVM lösen soll. Da schon alles in nativem Code vorliegt, muss sich der JIT-Compiler nur noch um den Anwendungscode kümmern und nicht mehr um sich selbst. Im Prinzip wird dadurch Graal zu einem – was das Laufzeitverhalten angeht – sehr ähnlichen JIT-Compiler wie der klassische C1 und C2 in der HotSpot-JVM. Langsame JIT-Compilierung während der JVM-Warm-Up-Phase wird dadurch verhindert.

Die aktuellen Versionen von GraalVM sowie die GraalVM-OpenJDK-Builds enthalten allesamt libgraal out-of-the-box.

GraalVM und die AOT-Compilierung

Seit Java 9 gibt es im JDK ein als experimentell markiertes Tool namens jaotc, das (eine recht frühe Version von) Graal als Backend verwendet. Die Implementierung erfolgte im Rahmen von JEP 295, dort kann man viele der Einschränkungen nachlesen. Im Prinzip gibt man jaotc vor, welcher Code vorcompiliert werden soll, und es wird ein Binär-Blob erzeugt, der dann den sonst durch den JIT-Compiler erzeugten Maschinencode enthält. Man spart also die Zeit, die der JIT-Compiler sonst zur Laufzeit braucht, um den Code zu compilieren.

Es gibt bei jaotc jede Menge Einschränkungen, die den Einsatz in der Praxis wirkungsvoll verhindert haben – zumindest in der Breite. Immerhin gab es bei Java 10 einige Bemühungen, um diverse gravierende Einschränkungen wie “funktioniert nur unter Linux-x64” aufzuheben. GraalVM geht heute aber noch einen großen Schritt weiter mit “native-image”.

GraalVM und das “Native Image”-Feature

Um Memory-Footprint und Startup-Zeit zu minimieren, unterstützt GraalVM die Möglichkeit, ein “Native Image” zu erzeugen. Simpel gesagt: ein standalone ausführbares Programm, das keine separate JVM benötigt. Die bisherigen Releaseversionen von GraalVM inklusive der derzeit aktuellen 19.1.0 erlauben die Erstellung eines solchen “Native Images” für Linux und macOS. Dazu wird neben der GraalVM-Distribution auch noch eine native Toolchain für das Betriebssystem benötigt, für Linux beispielsweise gcc nebst Bibliotheken wie glibc und zlib. Für die noch sehr experimentelle Windows-Unterstützung wird eine Installation von “Windows SDK for Windows 7” benötigt.

Es gibt ein paar Dinge neu zu durchdenken, wenn man ein “Native Image” erzeugt. Beispielsweise die Frage, was eigentlich schon zur Compilezeit aufgelöst wird und was erst zur Laufzeit. Stichwort “Initialisierung statischer Member”. Man überlege sich, inwieweit sich die Semantik des Programms ändert, wenn in einem static initializer eine Variable mit “new Date()” belegt wird und dies einmal zur Compilezeit passiert. Für solche Fälle werden Compile-Time-Switches angeboten.

Prinzipbedingt gibt es einige Limitierungen beim native-image-Feature. Wichtig ist die “closed-world assumption”. GraalVM muss bei der statischen Compilierung schließlich entscheiden können, welche Klassen für das Image eingebunden werden müssen. Das schließt übliche Java-Mittel wie dynamisches Nachladen von Klassen zur Laufzeit aus, deren Notwendigkeit auch erst zur Laufzeit auftaucht – typische Service-Lookup-Konstruktionen, beispielsweise im OSGi-Umfeld, gehören zu diesem Problemkreis. Ein Feature, um über einen Tracing Agent auch zur Laufzeit dynamisch nachgeladene Klassen sicher zu identifizieren, befindet sich derzeit in der Entwicklung. Aber auch das würde natürlich vollständig dynamisches Nachladen nur dann erfassen, wenn alle entscheidenden Codepfade auch tatsächlich durchlaufen werden, während der Tracing Agent aktiv ist.

Erstaunlicherweise ist trotzdem partieller Support für Reflection verfügbar, was uns zum nächsten Teilthema führt…

GraalVM und die Substrate VM

Auch nativer Code braucht natürlich, wenn die Semantik eines Java-Programms möglichst unangetastet bleiben soll, eine gewisse Laufzeitumgebung. Die wird im Falle eines GraalVM-erzeugten “Native Image” durch die eingebettete Substrate VM zur Verfügung gestellt. Dazu gehören Runtime-Routinen und der Garbage Collector. Die Komponenten der Substrate VM sind in Java geschrieben.

GraalVM und Truffle

Bisher waren die Punkte sehr Java-spezifisch, oder genauer gesagt Bytecode-spezifisch – alles problemlos nutzbar für das JVM-Bytecode-Ökosystem, also neben Java auch Kotlin, Scala, Groovy, Clojure und Konsorten. So weit, so unspektakulär. Andere Programmiersprachen haben häufig Bytecode-spezifische Forks als Implementierung wie Jython oder JRuby, und das JDK hatte bekanntlich seinen eigenen JavaScript-Interpreter intus namens Rhino (ab Java 6) und Nashorn (ab Java 8).

Aber GraalVM bietet mehr. Das Truffle-Framework erlaubt die einfache Implementierung von Interpretern, die dann vollautomatisch zu einem Bestandteil der GraalVM werden können – quasi eine automatische Generierung eines Compilers aus einem Interpreter.

Interpreter für Sprachen wie JavaScript, Ruby, R und Python sind bereits heute in verschiedenen Stadien der Fertigstellung bereit zu Experimenten. Die JavaScript-Implementierung scheint am weitesten fortgeschritten und umfasst bereits den Sprachumfang von ECMAScript 2019. Das war übrigens das Hauptproblem mit den Java-internen JavaScript-Lösungen – sie konnten mit der schnellen Weiterentwicklung von ECMAScript nicht mithalten und waren schnell veraltet und schwer wartbar.

Eine gute Zusammenfassung in wenigen Worten: “Truffle makes it easy, Graal makes it fast.” Wer schon mal versucht hat, für eine einfache Sprache einen Interpreter zu entwickeln, weiß, dass das durchaus machbar ist. Ein optimierter Interpreter ist schon schwieriger. Aber einen gescheit optimierenden Compiler dafür zu schreibe, ist sauschwer. Soll der auch noch mit in anderen Sprachen geschriebenem Code kommunizieren, wird es langsam wirklich komplex. Das GraalVM-Ökosystem verspricht hier eine einfach handhabbare und trotzdem sehr leistungsfähige und performante Lösung.

GraalVM als polyglotte VM

Nun wäre es ja schon ein echter Fortschritt, die GraalVM als universelle Laufzeitumgebung für allerlei Sprachen zur Verfügung zu haben. Beispielsweise könnte man sich für die gängigen Interpreter-Sprachen wie eben Ruby oder Python oder auch Lua die zeitaufwändige Entwicklung einer effizienten Laufzeitumgebung schlicht sparen – warum immer wieder neu einen JIT-Compiler implementieren, wenn es in GraalVM schon einen gibt? Aber wenn das nun schon möglich ist, wie wäre es denn dann, wenn man die Sprachen beliebig mischen könnte?

Das wäre natürlich phantastisch, und GraalVM bietet genau das. Es existieren Schnittstellen, um die Sprachen kontrolliert miteinander verbinden zu können. Und zwar, weil alle in demselben VM-Kontext laufen, quasi ohne zusätzlichen Overhead. So werden Java-Objekte für JavaScript-Skripte zugreifbar, Methoden aufrufbar, das volle Programm. Die technische Magie dazu steckt im “polyglot interoperability protocol” des Truffle-Frameworks, das genau diese Dinge standardisiert und damit diese Interoperabilität automatisch sicherstellt.

Der alte Traum von pick-and-mix bezüglich unterschiedlicher Programmiersprachen mit ihren spezifischen Stärken und leistungsfähigen Bibliotheken, hier wird er Wirklichkeit.

Wirklich cool ist beispielsweise eine für GraalVM erweiterte Variante von VisualVM. Man kann damit den laufenden Code in Ruby, R oder JavaScript genau untersuchen hinsichtlich der Threads und der Objekte auf dem Heap.

Und was ist Sulong?

Sulong ist eine weitere Schicht der GraalVM-Welt – ein High-Performance-Interpreter für LLVM Bitcode. Das erlaubt es der GraalVM, Programmcode, der in diversen Sprachen auf Basis der LLVM-Compiler-Infrastruktur erzeugt wird (besonders prominent: C, C++, Objective-C, Swift und Rust), ebenfalls auszuführen. Mit voller Interoperabilität mit allen anderen unterstützten Sprachen des GraalVM-Universums.

Ich erkenne hier eine leichte Ironie, schließlich hat Azul Systems für ihre Zing JVM einen neuen JIT-Compiler namens “Falcon” entwickelt, der auf der LLVM-Compiler-Infrastruktur basiert. Quasi genau andersrum.

Community Edition vs. Enterprise Edition

Oracle ist nicht gerade bekannt dafür, Projekte aus reiner Menschenfreundlichkeit kostenlos unter die Leute zu bringen. Demzufolge strebt man auch im Graal-Universum einen RoI an, und der heißt “GraalVM Enterprise Edition”. Es gibt einige Optimierungen, die nur in der Enterprise-Variante zur Verfügung stehen, Stand heute scheint das vor allem die Autovektorisierung zu sein. Man kann die Enterprise-Variante jederzeit zu Evaluierungszwecken herunterladen und so prüfen, ob das beim eigenen Workload etwas bringt – die Evaluierungsversion darf jedoch nicht in Produktion verwendet werden.

Ob und wann Optimierungen von der Enterprise-Version in die Community-Version wandern, muss abgewartet werden. Oracle wird sicherlich anstreben, immer einen Respektabstand zwischen den beiden Versionen zu wahren und vor allem Optimierungen für die Großen der Branche möglicherweise exklusiv für die Enterprise Edition reservieren. Letztlich muss der Performance-Gewinn durch die Enterprise-Edition ja im realen Leben gegen eingesparte Kosten z.B. im Cloud-Umfeld gerechnet werden – der Business-Case muss möglichst leicht nachvollziehbar sein, wenn man Lizenzen verkaufen will.

Bezüglich der Lizenzsituation bleibt festzuhalten, dass praktisch alle GraalVM-Komponenten unter GPL2 mit Classpath-Exception stehen.

Fazit, Status Quo, Ausblick, Glaskugel

Das Potenzial ist gewaltig – viele Schwächen des Java-Ökosystems können mit GraalVM gelöst oder zumindest gemildert werden. Dazu kommt mit der Idee der polyglotten VM der alte Traum der völligen Austauschbarkeit von Programmiersprachen und Bibliotheken in greifbare Nähe. Mit der “Native Image”-Möglichkeit wird Java auch wieder (oder zum ersten Mal?) eine sehr gute Lösung für klassische Apps und Anwendungen neben den unbestreitbaren Vorteilen für servserseitig kurzlaufende Services in einem containerisierten Cloudumfeld.

Was hält uns davon ab, genau jetzt vollständig in die GraalVM-Welt einzutauchen? Aus meiner Sicht ist das GraalVM-Ökosystem im Moment in einem uneinheitlichen Zustand. Während das Szenario “GraalVM als JIT-Plugin für die HotSpot-JVM” stabil ist, ist der Rest eher von wechselhafter Stabilität. Vor allem das Feature “native image” scheint im Moment eher limitiert und zurecht als “Early Adopter technology” gekennzeichnet, nicht zuletzt durch die Beschränkung auf Linux und macOS – Windows ist für diesen Anwendungsfall als “experimental” markiert. Wenn man sich die GitHub-Issues zu diesem Thema durchliest, wird klar, dass da noch eine Menge Feinarbeit zu leisten ist. AOT hingegen scheint weitgehend stabil zu sein, ist es doch seit Java 9 Teil des JDK.

Was die Polyglot-Fähigkeiten angeht, scheint die JavaScript-Implementierung recht stabil und zudem vollständig zu sein – Node.js ist ein sehr wichtiges Zielszenario, und man braucht durch die Abkündigung der Nashorn-Engine in Java 11 auch beizeiten einen Ersatz dafür. Bei Ruby, R, Python und LLVM-Bitcode steht eher noch ein Fragezeichen bezüglich Vollständigkeit, Performance und Stabilität. Vom technischen Standpunkt aus ist eine Out-of-the-box-Lauffähigkeit von realen Python-Programmen natürlich auch deutlich schwerer zu bewerkstelligen als bei JavaScript, weil das Python-Ökosystem bei vielgenutzten Bibliotheken voller native code zwecks besserer Performance steckt. NumPy und SciPy seien hier als Beispiele genannt. Bei R ist es ähnlich, wo die performance-relevanten Berechnungen oft in nativem Fortran-basierten Code passiert.

Angesichts der kürzlich erfolgten Abkündigung von Nashorn, der JavaScript-Engine im JDK, dürfte eine stabile und performante JavaScript-Fähigkeit ganz oben auf der Prioritätenliste zu sein. Außerdem scheint GraalVM als Ausführungsplattform für das Node.js-Universum auch attraktiv zu sein, vor allem natürlich wegen der einfachen und performanten Interoperabilität zwischen Java und JavaScript.

Die Native-Image-Fähigkeit ist wie gemacht für Microservices, und so gibt es bereits drei seriöse Frameworks, die GraalVM zu diesem Zweck nutzen: Quarkus, Micronaut und Helidon. Die Benchmarks bezüglich Memory-Footprint und Startup-Zeiten sehen schon beeindruckend aus.

Sehr interessant ist die Möglichkeit, die Native-Image-Fähigkeit für Plattformen zu verwenden, wo keine virtuelle Maschine zulässig ist – OK, “Plattformen” ist hier meines Wissens zu Unrecht die Mehrzahl, denn ich kenne nur iOS, das unter dieser willkürlichen Apple-Entscheidung zu leiden hat. Hier ein Artikel, der die Möglichkeiten beleuchtet. Und zurecht anmerkt, dass es schon viele vergebliche Versuche gab, iOS für das Java-Ökosystem einfach zugänglich zu machen.

Nicht zuletzt wäre die Native-Image-Fähigkeit für die wenigen verbliebenen Java-Entwickler, die Desktop-Anwendungen schreiben, eine schöne Sache. Eine einfache exe hat bei der Distribution von Software einfach Vorteile gegenüber dem Bundling einer kompletten Java-Runtime-Umgebung, möglicherweise nebst Einsatz von Obfuscator und exe-Wrapper.

Ein weiterer hier nicht weiter beleuchteter Anwendungszweck ist das Embedden von GraalVM-Technologie in andere Plattformen. Es werden die Oracle-Datenbank und MySQL derzeit genannt, mit diesem Anwendungsfall habe ich mich nicht näher beschäftigt.

Die Entwicklung schreitet im Moment zügig voran – wer am Ball bleiben will, sollte regelmäßig die Release Notes für neue Versionen durchlesen, man bekommt dann ein ganz gutes Gefühl für die Reife von manchem Szenario.

In der optimal vergraalten Welt der Zukunft wäre es ein Leichtes, mit dem Truffle-Framework seine persönliche Lieblingssprache in verhältnismäßig kurzer Zeit aus dem Boden zu stampfen und sofort eine komplette Toolchain inklusive hochoptimierender VM und nativen Code erzeugendem Compiler zur Verfügung zu haben. Inklusive voller Interoperabilität mit den Ökosystemen aller relevanten Programmiersprachen von Java über C++ bis Python. Die Grundbausteine dafür sind vorhanden und durchaus schon von hoher Qualität, aber es fehlt noch reichlich Feinschliff. Ich fürchte aber, dass die Lösung des Gesamtproblems der 80-20-Regel folgt. Wenn nicht gar 90-10.

Aber es wäre in einem ausgereiften Endzustand schon nahe am heiligen Gra(a)l. Ich hoffe, man verzettelt sich nicht in der Vielfalt der Möglichkeiten. Jedenfalls wäre Manpower in der GraalVM-Ecke m.E. deutlich besser aufgehoben als bei mancher heißdiskutierter Spracherweiterung seit Java 9. Ich sage mal “Text Blocks”. “Type Inference”. Private Methoden in Interfaces. Oder JDK-Erweiterungen wie jshell oder Epsilon GC, deren überragende Nützlichkeit sich auf den ersten und auch zweiten Blick nicht direkt erschließt.

Andere Quellen

Erster Anlaufpunkt ist natürlich die GraalVM-Homepage. Sehr empfehlenswert sind YouTube-Videos von Thomas Würthinger und Christian Thalinger (als früherer Entwickler des C2 natürlich spezialisiert auf den GraalVM-JIT aka GraalVM compiler, der früher verwirrenderweise Graal JIT oder Graal compiler hieß).

Wer ganz tief einsteigen will – die Sourcen werden auf GitHub gehostet.

Anlass für diesen Blog-Eintrag war ein Übersichtsartikel auf Heise Developer, der es doch etwas an Tiefe vermissen lies und durch Vermischung der unterschiedlichen Aspekte beim Uneingeweihten möglicherweise für Verwirrung sorgt. Ich überlasse es dem geneigten Leser zu entscheiden, ob mein Artikel eventuell noch wirrer und verwirrender ist.

Java Forum Stuttgart 2019

Über meine Gründe, das Java Forum Stuttgart zu besuchen, habe ich bereits 2017 was geschrieben, und daran hat sich nichts geändert. 2018 habe ich aufgrund wenig vielversprechender Themenauswahl mal sausen lassen, also war es 2019 mal wieder an der Zeit.

Die Rahmenbedingungen waren dieselben: Liederhalle, top organisiert, Verpflegung OK (auch wenn ich wohl nie verstehen werde, warum jemand Coke Light anbietet statt Coke Zero, aber über Geschmack kann man nicht streiten – vielleicht war es ja wenigstens billiger), abwechslungsreiches Vortragsprogramm. Und ein Code of Conduct scheint heutzutage wohl unvermeidlich, natürlich in gendergerechter Sprache anstatt in richtigem Deutsch. Man könnte den ganzen Sermon inhaltlich auch schneller auf den Punkt bringen: “Es gelten die Regeln der westlichen Zivilisation und des allgemeinen Anstands”. Oder “Verhalte Dich stets so, dass Deine Mutter auf Dich stolz wäre.”

Zum eigentlichen Thema. Gab es diesmal einen der typischen Themenschwerpunkte, den klassischen Hype, auf den sich alle stürzen und zu dem es zig Vorträge gibt? Immerhin vier Mal war Graal(VM) (Teil-)Thema, was m.E. zeigt, dass das Thema “Performance” insbesondere im Bereich Startup-Zeiten und Speicherverbrauch in der Java-Welt wie schon vor fast 25 Jahren topaktuell bleibt. Ansonsten scheint jeder – auch ohne Vorliegen zwingender Gründe – sein Heil in der Microservice-Welt – gerne auch in der Cloud – zu suchen. Der Mobile- und IoT-Hype schein hingegen etwas abgeflacht zu sein.

Zum Vortrag “Cross-Platform Mobile-Apps mit Java – 2019” von Dr. Daniel Thommes, Geschäftsführer der NeverNull GmbH. Ein Technologieüberblick, durchaus ins Detail gehend, zur ewig aktuellen Frage “write once, run anywhere, und bitte plattformübergreifend single source und an jede Plattform optimal angepasst”. Daran ist bekanntlich schon Swing gescheitert. Und die Zeichen stehen ehrlich gesagt nicht besonders gut, dass es für so divergierende Anforderungen wie “App auf iOS, App auf Android, und Desktop auf Windows und Linux” jemals eine einheitliche und schöne Lösung geben könnte. Aber der Stand der Dinge hat mich schon etwas erschreckt, denn die “Lösungen” wie Transpiler, portierte Runtimes oder halbgare eigene UI-Toolkits sind doch eher ein Armutszeugnis. Es war ein Non-Sponsored-Talk, was den Vortragenden erkennbar in Gewissensprobleme stürzte und er den Eindruck, die eigene Lösung “MobileUI” seiner Firma zu sehr in den Vordergrund zu stellen, gewissenhaft zu vermeiden suchte. Tut so einem Vortrag nicht gut. Wobei man die Basics durchaus gut beherrscht, wie die “Java Forum Stuttgart”-App – natürlich auf relativ niedrigem Komplexitätsniveau, aber eine Lösung, die die einfachen Sachen sehr gut und sicher beherrscht ist ja auch wertvoll – zeigt. Ebenfalls fehlte erkennbar Tiefe für die diversen altbekannten Fallstricke der Cross-Plattform-UI-Entwicklung. Wenn etwas leidlich Komplexes in Demos nicht angeschnitten wird, liegt das erfahrungsgemäß daran, dass es nicht anständig funktioniert. Aber das ist natürlich nur wilde Spekulation kraft meiner 20-jährigen Erfahrung mit UI-Toolkits aller Art.

Michael Wiedeking war – fast selbstverständlich – auch wieder dabei, diesmal mit “Der eilige Graal” mit einer gewohnt locker präsentierten Reise durch die wunderbare Welt von JIT-Compilern, Hotspots, Bytecode und natürlich Graal. Der Vortrag krankte etwas an der unscharfen Abgrenzung zwischen Graal, GraalVM, Graal-JIT, Graal AOT und Graal native image-Feature. Für den Uneingeweihten ging es da manchmal zu sehr durcheinander, dank ausreichender Vorbildung konnte ich jedoch folgen. Aber selbst die Graal-Erfinder selbst tun sich da oft schwer, eben weil das Graal-Universum so viele Zielrichtungen verfolgt. Wie man das in 45 Minuten überhaupt vorstellen könnte – ich weiß es auch nicht.

Auch Stephan Frind versuchte sich unter dem Titel “Graal oder nicht Graal –ist das wirklich eine Frage?” an diesem Themenkomplex. Deutlich high-leveliger als Michael Wiedeking, und deshalb erfolgreicher beim Vermitteln des allgemeinen Überblicks. Leider aber dadurch eben auch fehlende Tiefe. Die Compilerbau-Details und die C1-C2-Abgrenzung in der JVM waren nicht immer ganz korrekt, aber das tat nicht weiter weh. Details, die nur einem passionierten i-Dipfeles-Scheißer auffallen. Die Abgrenzung Graal vs. GraalVM war auch hier eher unscharf, Schwächen der derzeitigen native image-Lösung kamen etwas zu kurz. Am Ende der Hinweis “Testen! GraalVM etwas schwieriger” könnte in der Kategorie “Untertreibung des Jahres” durchaus weit vorne landen. Man schaue sich die diversen Issues zum Thema native image auf GitHub an, um ein Gefühl für den präzisen Stand der Dinge bezüglich Stabilität und Umfang zu bekommen.

Aufgrund sparsamer Konkurrenz im Zeitslot verschlug es mich auch in “Aus eins mach zehn: Neuentwicklung mit Microservices” von Lars Alvincz und Bastian Feigl (andrena objects ag). Im Abstract war schon angekündigt, dass auch “Null vermeiden” ein Thema sein sollte, und das störte doch gewaltig – wie kann man etwa 10 Minuten der wertvollen Zeit auf so eine Nichtigkeit verschwenden, wo es doch um das Thema “wir lösen ein monolithisches System durch Microservices ab” ging? Mit all seinen interessanten Fragestellungen von “sind Microservices der richtige Ansatz” über “was hat die Neuentwicklung veranlasst” bis zu “das war der Ressourcenbedarf an CPU und Speicher vorher und nachher”. Neben den Klassikern “wie schneide ich die Services richtig” und “wie schaffe ich es, transaktionales DB-Verhalten mit dem Microservice-Umfeld zu verheiraten” natürlich. Es wurde nicht mal ansatzweise versucht, hier mehr als ein paar Worte zu den interessanten Teilgebieten zu verlieren. Besonders ärgerlich, weil die dargestellten Anforderungen von Kundenseite keineswegs eine Microservice-Architektur nahelegten oder gar erzwangen. Warum bei einer eher trivialen Standardanwendung, zudem mit zwingender Datenmigration und klarer Vorgabe an Funktionalität ein “Big Up Front-Design nicht sinnvoll/möglich” gewesen sein soll – behauptet wurde es, aber begründet nicht. Zusätzlich fiel mir wieder auf, dass die Lösung mit zwei Vortragenden nur ganz selten wirklich gut funktioniert. Auch die Themen “Versionsverwaltung des Codes” und “Trunk-based development vs. Feature Branches” wurde kurz gestreift, ohne dass klar wurde, warum das in diesem Kontext unbedingt Erwähnung finden musste. Insgesamt kein überzeugender Vortrag. In keinem der behandelten Unterthemen.

Da mein tägliches Brot immer noch Java 8 ist (böse Zungen würden behaupten, dass man in Java 8 natürlich immer noch genauso wie in Java 1.4 programmieren kann und schon die Einführung von Generics ein Fehler war) und ich die Neuerungen aus Java 9 bis Java 13 zwar kenne, aber nicht seriös im produktiven Einsatz habe, habe ich mir “Power Catch-up: Alles Praktische und Wichtige aus Java 9 bis 12” von Benjamin Schmid angeschaut. Sehr komprimiert, ohne Füllstoff, kompakt und doch mit Mehrwert gegenüber dem bloßen Lesen von Artikeln zum Thema. Ich war zufrieden. Auch wenn einzelne Themen wie die neuen GC-Varianten oder Project Loom natürlich in 45 Minuten nicht annähernd in gebührender Ausführlichkeit behandelt werden können. Der Vortragende hat sich aber mit potenziell verwirrenden Details angenehm zurückgehalten. Sehr gut.

Und das Beste kommt zum Schluss. “RxJava2: von 0 auf 100(?) in 45 Minuten” von und mit Johannes Schneider. Sensationell. Einer der besten Vorträgem egal zu welchem Thema, den ich je auf einer Konferenz gehört habe. Genau getimed auf die 45 Minuten, professionell und trotzdem humorvoll vorgetragen, voller Anregungen und Informationen. Didaktisch vom Feinsten. Ich muss gestehen, dass das den 2017er Vortrag zur reaktiven Programmierung, den ich anno dazumal gar nicht so schlecht fand, im Nachhinein ziemlich blass wirken lässt, Mein Kompliment an Johannes Schneider.

Was bleibt hängen? Zwei Dinge sollte man sich (ich mir) dringend im Detail anschauen: natürlich Graal(VM), und RxJava/ReactoveX, gerne auch im Swing-Kontext (auch wenn meine heimische Library natürlich die allermeisten Dinge schon auf anderem Wege erreicht – aber mit RxJava gäbe es hier die Möglichkeit, die Code-Lesbarkeit zu verbessern). Dafür kann man glatt seine Vorbehalte gegen 3rd-party-Abhängigkeiten über Bord werfen.

Beknackte Oberflächen – heute: Windows 10 Timeserver konfigurieren

Mein Klagelied über mein “Übergangsauto” lässt noch auf sich warten, deshalb heute zur Erholung ein weiteres Kleinod aus der Reihe “Beknackte Oberflächen”.

Die Vorgeschichte; ein Phänomen zeigte sich auf einem einzigen Windows 10-Rechner in meinem Heimnetz: die Uhrzeit war regelmäßig falsch. Also nicht nur ein paar Minuten, sondern Stunden oder wie heute ein ganzer Tag. Mal vor, mal nach. Besonders bei E-Mails, die man schreibt, kann das zu Irritationen führen.

Man konnte das Problem für den Moment stets beheben, wenn man in den Uhrzeit-Einstellungen die automatische Synchronisation mit einem Internet-Timeserver kurz abschaltet und wieder anschaltet. Das bewährte IT-Rezept – “have you tried to turn it off and on again” – in voller Blüte.

In den Einstellungen gab es hingegen keinen offensichtlichen Weg, einen spezifischen Timeserver einzustellen, denn offenbar war der Default ja ein sehr unzuverlässiges Exemplar seiner Gattung. Eine kurze Internet-Recherche später stellt sich raus: der Weg zur Einstellung des Timeservers ist nicht nur nicht offensichtlich, er ist extrem unoffensichtlich. Man muss den Link “Uhren für unterschiedliche Zeitzonen hinzufügen” klicken, und schon öffnet sich ein guter alter Windows-XP-Style-Dialog, der auch einen Reiter “Internetzeit” hat. Dort gibt es dann einen Button, der einen weiteren Dialog öffnet (klar, dass man für diese kritische Aktion Admin-Rechte braucht). Flugs die FritzBox eingetragen, scheint zu funktionieren. Mal sehen wie lange. Besser als mit dem alten Server (natürlich einer unter microsoft.com – sehr beruhigend, das Betriebssystem eines Herstellers einzusetzen, der nicht mal einen zuverlässigen Timeserver betreiben kann) wird es auf jeden Fall sein.

Gastbeitrag: Ein Nachruf für meinen Audi A6 Avant

Vorwort von hubersn: Jeder große Blog veröffentlicht regelmäßig Gastbeiträge. Vermutlich ist die Kausalität dieser Korrelation so, dass der Blog erst mal groß wird und dadurch zu Gastbeiträgen kommt. Ich versuche es jetzt mal andersrum. Denn das Schicksal wollte es so, dass mein lieber Freund und Kollege Roland ein ähnliches Schicksal erfuhr wie ich – ein hoffentlich nur temporärer, kurzer Wechsel von einem “Wunschfahrzeug” zu einem “war-gerade-übrig-Fahrzeug”. Da hier im IT-Blog regelmäßig über schlechte Usability und bescheuerte Oberflächen geschimpft wird, erschien mir sein Beitrag sehr passend – mein Beitrag zu diesem Thema folgt demnächst. Aber genug der Vorrede, lassen wir Roland zu Wort kommen…

Es war ein trauriger Tag. Ich verabschiedete mich nach drei Jahren von meinem geliebten, geleasten Firmenwagen. Der Nachfolger sollte ein A5 Cabrio werden… aber mein Chef hatte eine andere Idee.

Und so erlebte ich völlig neue Erfahrungen. Ich hätte nicht gedacht, dass Audi noch so viel Vorsprung durch Technik hat. Aber eins nach dem anderen. Ein Jetzt-Ex-Kollege hat einen wenige Monate neuen Volvo V90 hinterlassen und mein Chef sagte, jetzt fährst Du den mal. Ich sagte maximal ein Jahr – ich hatte da so eine Vorahnung. China und Schweden. Geht das gut?

Guten Mutes fuhr ich also mit dem V90 los. Ich fahre jede Woche gut 400 Kilometer Autobahn und da ruft mich gerne eine Kollegin an. Auf einer der ersten Fahrten war das mal wieder der Fall. Auf dem BlackBerry unserer Firma hat sie mich nicht erreicht. Warum? Ich habe es bisher nicht geschafft, dem Volvo beizubringen, dass ich zwei aktive Mobilverbindungen benötige. Vielleicht lese ich doch irgendwann die im Auto integrierte Anleitung, die man nur im Stand lesen kann. Es gibt ja keine Beifahrer, die während der Fahrt lesen können – so etwas ist in Europa nicht denkbar. Zum Glück kennt meine Kollegin meine private Nummer. Ich gehe ans Telefon, wir sprechen kurz. Dann fragt sie mich leicht verwirrt, was ich denn gerade mache. Sie dachte, ich wäre im Auto unterwegs. Ich sage, dass ich im Auto unterwegs bin. Sie meint, es gäbe so laute Nebengeräusche und sie versteht mich so schlecht. Ich sage nur: “Ich fahre jetzt Volvo”.

Die schönste Neuerung im Volvo ist das große, glänzende Touch-Display über der Mittelkonsole. Da freut man sich. Super hell und nur im Nachtmodus dimmbar. Ja, tatsächlich funktioniert der Drehknopf links neben dem Lenkrad nur im Nachtmodus – wirkt sich aber selbstverständlich auf den Tagmodus aus. Man muss also einen Tunnel finden, um tagsüber die Einstellung zu ändern. Schon mal das Wort Benutzerfreundlichkeit in Schweden gehört? Nix “lindra” im Volvo.

Das ist aber nicht die größte Schwäche des tollen Displays. Das ist der Touch selbst. Man wird wahnsinnig in Kombination mit Android Auto. Ob es an Google oder Volvo liegt ist mir egal. Im Stand kann man Audible, Amazon Music und Google Maps wenn man sich am Rand des Displays festhält ja noch halbwegs bedienen. Aber während der Fahrt wird das zum Geduldsspiel. Man tippt lässig, um eine Aktion auszuführen. Das Display scrollt hoch und runter ganz munter aber Aktionen ausführen – seltenst. Da muss man vorausschauend fahren und an roten Ampeln die Playlist auswählen – mit ruhiger Hand. Wer keine ruhige Hand hat – Finger weg vom Volvo!

Es gibt ein zweites, mattes Display hinter dem Lenkrad. Vom Audi gewohnt, suche ich heute nach 6 Monaten immer noch nach dem Schalter, welcher die Karte da vorne verdrängt und mir sinnvolle Dinge anzeigt. Fehlanzeige. Oder zu gut versteckt im Handbuch? Tipps nehme ich gerne entgegen.

Von A nach B fährt inzwischen jedes Auto. Wenigstens meistens. Mein Audi konnte bei 220 km/h noch die Kurve alleine fahren. Beim Volvo ist bei knapp 140 km/h Schluss. Ist ja klar. Die Schweden haben sich für die Höchstgeschwindigkeit 120km/h entschieden. Da darf man alles darüber nicht testen und schaltet einfach ab. Und dann ist der Volvo ein Nervenbündel beim Spurhalten. Zuckelt links und rechts anstatt ruhig und elegant wie der Audi zu fahren. Was für ein Rückschritt zu einem mindestens drei Jahre älteren Audi. Vorsprung durch Technik kann ich da bestätigen.

Man erträgt es halt und wendet sich den guten Dingen im Volvo zu. Die Sitze sind wirklich gut. Leder. Und der Volvo speichert alle möglichen und unmöglichen Einstellungen in einem Fahrerprofil im Schlüssel. Leider auch wenig durchdacht. Ich würde lachen, wenn es nicht so traurig wäre. Umluft kann man auch ganz einfach mit wenigen Berührungen der sehr großen Schaltflächen aktivieren. Umständlicher als jeder Knopf wie man es von früher kennt. Vor Touch im Auto. Gute Zeiten waren das, sage ich euch. Mit viel Tippen und Wischen findet man viele Einstellungen. Während der Fahrt nur ratsam, wenn man den Zappelphilipp von Pilot-Assistent aktiviert. Und immer schön hin- und herschauen, ob im matten Display das Lenkrad grün ist, sonst zappelt nichts mehr von alleine am Lenkrad. Dann ist der Assistent plötzlich aus.

Lustig – eher traurig – ist auch der Assistent, der verhindern soll, dass man beim Rückwärts- oder langsam Vorwärtsfahren nichts beschädigt. Das Ding geht aber manchmal plötzlich aus und man merkt es nicht. Damit ist es irgendwie sinnlos geworden, oder?

Zur Verarbeitung: Ich habe Grünschnitt transportiert und die klapprige Kofferraumabdeckung entfernt. Danach schnell wieder einbauen. Denkste. Die rastet irgendwie nicht ein. Ich wollte schon den Hammer holen. Beim Audi flutscht das nur so. Klack und fertig. Okay. Beim Audi wiegt das Ding das Zehnfache. Wirkt solide und funktioniert. Und dann frisst die Volvo Abdeckung auch noch eine Ecke meiner besten Wellensteyn-Jacke! Die Ablage oben sollte man niemals für Jacken benutzen! Das steht bestimmt im Handbuch. Habe ich ja nicht gelesen. Bin da halt noch Papier gewöhnt. Beim Öffnen frisst der Einzug die Jacke an und gibt sie nicht mehr her! Schließlich musste ich die Jacke herausreißen und damit eine Ecke am Kragen abreißen.

Neulich dann der Höhepunkt. Mein Chef fährt mit und ist ganz begeistert, wie toll das Auto ist. Nun muss man wissen, dass mein Chef das älteste Technik-Kind der Welt ist, Porsche fährt und jeden Technikschnickschnack liebt. Er kennt sich aus und steigt ein und meint, dass man da aber toll sitzt und das wäre ja ein super Display. Wow! Gestochen scharf. Ich meine nur: Aber nicht bedienbar – touch.

Das probiert der Chef begeistert aus und will mit Google Maps das Ziel eingeben. Er versucht den Knopf für das Mikrofon auszulösen. Wir fahren bereits. Beim 5. Versuch klappt es – wir stehen an der Ampel. Google findet das Ziel und Chefe will es auswählen zum Starten der Navigation. Im Heslacher Tunnel beweise ich ihm, dass man das Display tatsächlich abblenden kann – nur im Tunnel. Schon nach dem Tunnel schaffen wir es die Navigation zu starten. Jetzt bräuchten wir wieder einen Tunnel, damit wir das Display wieder heller machen können. Google lenkt uns nach rechts vorbei am nächsten Tunnel. Blöd gelaufen.

Ich denke, beim nächsten Gespräch mit meinem Chef gesteht er mir im Frühjahr ein Cabrio zu. Ich bin ja als Bereichsleiter durchaus ein Mitarbeiter, den man halten will, oder?

Roland

MISTer-Fortschritte und die zweite Luft für MIST

Schon lange nichts mehr zu MISTer und MIST geschrieben (zuletzt im Juni 2017). Dabei gibt es großartige Fortschritte bei beiden Projekten.

Der MISTer hat einen neu implementierten CPC-Core bekommen, der um Welten besser funktioniert als die alte, ursprüngliche Variante. MISTer-Mastermind Sorgelig hat viel Zeit darauf verwendet, besonders die CRTC-Implementierung (ein Motorola 6845, der in verschiedenen Varianten im CPC verbaut wurde, die sich alle in Kleinigkeiten unterscheiden, was von verschiedenen Demos auch weidlich ausgenutzt wird) und auch teilweise die Z80-Implementierung sowie die Floppy-Simulation (NEC 765), um endlich auch diverse Kopierschutzmaßnahmen sowie Sonderformate zu unterstützen.

MIST und MISTer profitieren gerade gemeinsam von einer Weiterentwicklung des Sega Genesis-Cores (für Europäer: Sega Megadrive), bei der unter anderem Sorgelig, GreyRogue und MIST-Mastermind Till Harbaum (“MasterOfGizmo” im Atari-Forum) zusammenarbeiten, nebst einem Spezialisten (“Jotego” im Atari-Forum) für die Soundchips des Genesis und mehreren Kennern der Originalspiele, die in langwierigen Testläufen die diversen Änderungen, die teilweise im Stundentakt gemacht werden, auf Regressions prüfen.

Im Zuge dieser Anpassungen und Weiterentwicklungen wurde auch für den MISTer eine neue Art des Scalings für den HDMI-Output implementiert auf Basis des “Nearest Neighbor”-Algorithmus, der diverse Artefakte des bisher benutzten Scalers verhindert (und dafür auf gewissen Hardware-Kombinationen aber auch neue Artefakte erzeugt). Das ist noch “work in progress” und derzeit eine Compile-Time-Einstellung, es sieht aber so aus wie wenn über zur Laufzeit ladbare Koeffizienten beide Skalieralgorithmen gleichzeitig im selben Core leben können. Und ein komplett neuer Scaler ist derzeit in Entwicklung – Stand heute ist die Scaler-Geschichte etwas unschön, weil sie spezielle IP erfordert, und diese nur für eher teure Quartus-Versionen (quasi die “Entwicklungsumgebung” für die Intel/Altera-FPGAs) verfügbar ist und die freien Versionen solche Cores nicht bauen können. Mit einem unabhängigen Open-Source-Scaler wäre dieses Hindernis auch aus dem Weg geräumt.

Lange Zeit fehlte ein Atari ST-Core beim MISTer – eigentlich die einzige große Lücke, alle anderen Cores waren längst auf dem MISTer portiert und teils sogar stark verbessert gegenüber ihrem MIST-Original. Und hier gibt es den vermutlich größten Fortschritt: ein komplett neuer Core namens FX CAST (von Jorge Cwik aka ijor im Atari-Forum), basierend auf einer zyklenexakten Nachbildung des Motorola 68000 und einer sehr präzisen Nachbildung von Grafik- und Soundchip. Die Sourcen dazu sind noch nicht offen, das soll aber demnächst so weit sein.

Also, ran an den MISTer, oder den alten MIST nochmal auspacken. Es gibt für den MIST inzwischen viele Cores, die das “Component out”-Kabel unterstützen, was es deutlich erleichtert, den MIST an einigermaßen aktuelle Fernseher oder Projektoren anzuschließen – bei vielen Bildgeräten hat “Component” neben HDMI als Input-Schnittstelle überlebt, während Scart-RGB mit der Lupe gesucht werden muss, genauso wie VGA-Eingänge mit ausreichender Flexibilität für die “krummen” Videosignale der diversen Cores.

Für den MISTer gibt es auch eine neue Version des USB-Hub-Boards mit der Wiederauferstehung des 9pol-Digital-Joystick-Anschlusses. Und es wird gerade mit Serial-to-MIDI experimentiert, um auch noch diese letzte MIST-MISTer-Lücke zu schließen.

Rundrum großer Fortschritt und viel Bewegung und Weiterentwicklung. Es ist eine Freude, das zu verfolgen, und ein schönes Beispiel für “Open Source funktioniert”. Besonders freue ich mich, dass Till wieder aktiv ins Geschehen eingreift, er klang zuletzt etwas negativ bezüglich der MIST-Zukunft (Produktion eingestellt, im Prinzip Abverkauf der letzten Exemplare, und Ärger mit Billig-Nachbauten wie Mistica die ihre Kunden im Regen stehen lassen). Er scheint wieder neue Energie gefunden zu haben.

Eine Swing-Komponente – HexDumpView

Nach dem großen Erfolg der JVM-Memory-Indicator-Komponente und dem sensationellen Layout-Manager nun eine weitere unverzichtbare Komponente in jeder grafischen Oberfläche, die aus unverständlichen Gründen nicht schon standardmäßig in Swing ausgeliefert wird: die Anzeige eines klassischen Hexdumps. In der ebenso klassischen 8 + 16 + 16-Darstellung: 8-stellige Adresse, danach 16 Bytes in Hex-Darstellung, danach 16 ASCII-Zeichen als direkte Übersetzung der Bytes. Mit möglichst simpler API: hier ist das Byte-Array, mach’ mir die HexDump-View als Swing-Komponente dazu, die ich zur Nutzung nur noch in eine JScrollPane reinsetzen muss. Natürlich superschnell und superspeichersparend.

Testkandidat war eine 800KiB-Datei (ein adf-Floppyimage, aber das tut nichts zur Sache, hat aber mit dem Ursprung des Wunsches nach dieser Komponente zu tun – ich hoffe, da kann ich demnächst Vollzug melden, dann voraussichtlich nebenan im RISC OS-Blog), resultierend in einem rund 50000 (genau 51200) Zeilen umfassenden Hexdump.

Wie ist es implementiert? Da gab es natürlich eine Menge Varianten, die mir spontan in den Sinn kamen und die wie immer mit unterschiedlichen Kompromissen behaftet sind, die ich kurz anreißen will.

Variante 1: Erzeugen einer String-Repräsentation der Binärdaten und Nutzung einer “normalen” JTextArea oder JEditorPane. Geht schnell, ist unproblematisch in der Realisierung, alle typischen Dinge einer Textkomponente sind direkt verfügbar (auch wenn in diesem Falle das Copy&Paste vermutlich nicht ganz das ist, was der Anwender erwartet). Aber speicher- und laufzeittechnisch sicher suboptimal – die Konvertierung in eine lange Zeichenkette, obwohl man diese ja bei größeren Datensätzen niemals gleichzeitig auf dem Schirm sieht, ist doch verhältnismäßig teuer, und die Wahrscheinlichkeit, dass sowohl das Quell-Byte-Array als auch die Zeichenkette im Speicher verbleiben müssen, ist ja doch recht groß (vermutlich will der Verwender seine Quelldaten ja nicht wegwerfen, sondern noch weiter verarbeiten), und pro Byte kostet die String-Repräsentation ja zusätzlich etwa 14 Bytes (73 Zeichen pro 16 Bytes.

Variante 1a: Erzeugen einer String-Repräsentation der Binärdaten und wrappen in ein java.swing.text.Document als Unterfütterung einer JTextArea/JEditorPane. Spontan würde ich denken, dass so ein reines ReadOnly-Document etwas besser bei Speicher- und Laufzeitverhalten ist als ein Wald- und Wiesen-Document aus dem Swing-Bauchladen.

Variante 2: Erzeugen einer String-Repräsentation der Binärdaten und direkter Redraw in paintComponent. Habe ich getestet, für mäßig große Binärdaten funktioniert das erschreckenderweise recht gut (in meinem Falle waren das rund 50000 Zeilen des Hexdumps), was glaube ich eher zeigt, wie schnell die Maschinen heutzutage sind, und nicht, wie man ein solches Problem angehen sollte. Speichertechnisch ist das etwas günstiger als eine JTextArea oder JEditorPane weil man die ganze unnütze Funktionalität weglässt, aber das Hauptproblem der Erzeugung der großen Zeichenkette bleibt.

Variante 2a: wie 2, aber mit optimiertem Redraw (paintComponent) – nur die Zeilen zeichnen, die laut Clipping Rectangle tatsächlich gezeichnet werden. Das ist geschwindigkeitstechnisch schon erheblich besser als Variante 2, weil eben nur sagen wir 100 Zeilen gezeichnet werden statt 50000, und deshalb aus der vollständigen String-Repräsentation auch nur 100 Substring-Operationen anstatt 50000 durchgeführt werden müssen. Ich habe allerdings nicht analysiert, ob denn den Substring-Operationen im Vergleich zum dann weggeclippten drawString wirklich die teurere Operation ist – einigen wir uns auf “hilft beides nicht”.

Variante 3: kein Erzeugen der vollständigen String-Repräsentation, sondern Erzeugung der zu malenden Textzeile “on demand” aus den Quelldaten, dem Byte-Array. Dabei Inspektion des Clipping Rectangles, um nur die notwendigen Operationen durchzuführen.

Nachfolgend beispielhaft die Implementierungsvariante 3 (die anderen sind zu trivial :-)), als ganz nacktes Java – keine externen Abhängigkeiten, und vermutlich würde der Code so schon unter Java 1.1 mit Swing 1.1 funktionieren. Es gibt hier natürlich Lücken (es sind nur rund 200 Zeilen Code, für eine vollständige JComponent, inklusive JavaDoc!), die selbstverständlich nur als Aufforderung zur Übung an den Leser zu verstehen sind. Encoding-Unterstützung für den Textbereich, Implementierung eines Carets und einer Selektionsmöglichkeit, Unterstützung für nicht-Monospaced-Schriften, on-demand-loading der Binärdaten aus einer Datei größer als MaxHeap während der Benutzer scrollt, suchen nach Text und Bytes und Adressen, Darstellung in Byte- und Word-Form, hinzufügen der Zeilennummer…

Auch eine Variante 4 – ein Document das allein durch die Quelldaten unterfüttert ist und on-the-fly den Text für die JTextComponent generiert, wäre eine interessante Forschungsaufgabe.

/*
 * (c) hubersn Software
 * www.hubersn.com
 *
 * Use wherever you like, change whatever you want. It's free!
 */
package com.hubersn.playground.swing;

import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Rectangle;

import javax.swing.JComponent;

/**
 * Component showing a hexdump.
 */
public class HexDumpView extends JComponent {

  /** Number of characters in one line of a hexdump: 8 characters address, 16 2-character-bytes, 16 characters, spaces. */
  private static final int LINE_LENGTH = 8 + 1 + 3 * 16 + 16;

  private byte[] data;

  private int lineCount = -1;

  private FontMetrics fm = null;

  private int lineHeight = 0;

  private int lineAscent = 0;

  private int characterWidth;

  private int calculatedWidth;

  private int calculatedHeight;

  /**
   * Creates a hexdump view based on the given data with a monospaced 12pt font.
   *
   * @param data hexdump data.
   */
  public HexDumpView(final byte[] data) {
    setFont(new Font("Monospaced", Font.PLAIN, 12));
    setData(data);
  }

  /**
   * Sets the data to be visualized by this hexdump view.
   *
   * @param data hexdump data.
   */
  public void setData(final byte[] data) {
    this.data = data;
    refresh();
  }

  /**
   * Sets the font to be used for the hexdump - note that only a monospaced font will yield sensible results.
   */
  @Override
  public void setFont(final Font font) {
    super.setFont(font);
    this.fm = null;
    refresh();
  }

  private void refresh() {
    if (this.data == null) {
      return;
    }
    calculateSizes();
    revalidate();
  }

  private int calculateLineCount() {
    if (this.data == null) {
      return 0;
    }
    int lc = this.data.length / 16;
    if (this.data.length % 16 > 0) {
      lc++;
    }
    return lc;
  }

  private void calculateSizes() {
    if (this.fm == null) {
      this.fm = getFontMetrics(getFont());
      this.lineHeight = this.fm.getHeight();
      this.lineAscent = this.fm.getAscent();
      this.characterWidth = this.fm.stringWidth("M");
    }
    this.lineCount = calculateLineCount();
    this.calculatedWidth = getInsets().left + getInsets().right;
    this.calculatedWidth += LINE_LENGTH * this.characterWidth;
    this.calculatedHeight = getInsets().top + getInsets().bottom;
    this.calculatedHeight += this.lineCount * this.lineHeight + this.lineAscent;
  }

  @Override
  public Dimension getPreferredSize() {
    return new Dimension(this.calculatedWidth, this.calculatedHeight);
  }

  @Override
  protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    if (this.data == null) {
      return;
    }
    Rectangle clipBounds = g.getClipBounds();
    Insets insets = getInsets();
    int y = insets.top + this.lineAscent;
    for (int i = 0; i < this.lineCount; i++) {
      // optimized redraw - check if clipping bounds contains line
      if (shouldDraw(clipBounds, y)) {
        String str = getHexDumpLine(this.data, 0, i * 16);
        g.drawString(str, insets.left, y);
      }
      y += this.lineHeight;
    }
  }

  private boolean shouldDraw(Rectangle r, int y) {
    return !(y + this.lineHeight < r.y || y - this.lineHeight > r.y + r.height);
  }

  //
  // -- utility methods for hex(dump) representation
  //

  /**
   * Converts a value between 0 and 15 into the corresponding hex character - throws an
   * IllegalArgumentException on illegal input values.
   *
   * @param value value to convert.
   * @param useUpperCase use upper case letters A-F?
   * @return hex character representing value.
   */
  private static char valueToHexChar(final long value) {
    if (value < 0 || value > 15) {
      throw new IllegalArgumentException("Value " + value + " out of range - must be 0-15");
    }
    if (value < 10) return (char) ('0' + value);

    return (char) ('A' + (value - 10));
  }

  /**
   * Converts the given value to a number of hex characters specified in nibble count value.
   *
   * @param value
   * @param nibbleCount amount of nibbles (characters) to return.
   * @param useUpperCase use upper case hex characters.
   * @return hex representation of value.
   */
  private static String valueToHexChars(final long value, final int nibbleCount) {
    char[] hexChars = new char[nibbleCount];
    for (int i = 0; i < nibbleCount; i++) {
      hexChars[i] = valueToHexChar(0b1111 & (value >> (4 * (nibbleCount - 1 - i))));
    }
    return new String(hexChars);
  }

  /**
   * Returns a line (not terminated) for a hexdump output - either 16 bytes starting from offset or less if data has less than 16 bytes available.
   *
   * @param data data for dump.
   * @param startAddress start address of data.
   * @param offset offset into data.
   * @return hexdump line.
   */
  private static String getHexDumpLine(final byte[] data, final long startAddress, final int offset) {
    StringBuilder sb = new StringBuilder(LINE_LENGTH);
    sb.append(valueToHexChars(startAddress + offset, 8));
    sb.append(' ');
    for (int lineOffset = 0; lineOffset < 16; lineOffset++) {
      if (offset + lineOffset < data.length) {
        sb.append(valueToHexChars(getUnsigned(data[offset + lineOffset]), 2));
        sb.append(' ');
      } else {
        sb.append("   ");
      }
    }
    for (int lineOffset = 0; lineOffset < 16; lineOffset++) {
      if (offset + lineOffset < data.length) {
        sb.append(getPrintableCharacter(getUnsigned(data[offset + lineOffset])));
      }
    }
    return sb.toString();
  }

  private static int getUnsigned(final byte b) {
    if (b < 0) {
      return ((int) b + 256);
    }
    return b;
  }

  private static char getPrintableCharacter(final int byteValue) {
    if (byteValue < 32 || byteValue > 255 || (byteValue > 128 && byteValue < 160)) {
      return '.';
    }
    return (char) byteValue;
  }

}

Bits frickeln mit Java

Gerade bin ich dabei – zum besseren Kennenlernen von Filecore-basierten Dateisystemen (man muss den Feind kennen) – in Java ein kleines Tool zu entwickeln, das Disc-Images im Filecore-Format versteht und Dateien und Verzeichnisse daraus extrahieren kann. Nur lesend, um weitere Komplexitäten erst mal zu vermeiden.

Etwas Hintergrund: Filecore ist das native Dateisystem unter RISC OS. Oft wird es auch als “ADFS” bezeichnet, weil das “Advanced Disc Filing System” quasi die erste Implementierung von Filecore war (damals, Floppy-Zeit, zur Archimedes-Ära 800 KiB auf einer 3,5″-DD-Diskette), bevor es abstrahiert und generalisiert als Basis für Disketten- und Festplattenformate in RISC OS Einzug hielt. Es gab über die Jahre reichlich Varianten, um die diversen Einschränkungen und Limits in den besser nutzbaren Bereich zu schieben – das E-Format ist das Übliche für DD-Disketten (800 KiB) und man findet es heute in den Weiten des Internets als .adf-Disc-Images der klassischen Archimedes-Software. Später kam das F-Format für die HD-Floppies (1600 KiB). Schließlich wurde mit RISC OS 4 das E+/F+-Format eingeführt (“Big Map”), um die lächerlich niedrige Grenze solcher Dinge wie “maximale Anzahl von Einträgen in einem Verzeichnis” auf ein halbwegs erträgliches Niveau zu heben.

Wie auch immer – beim Hantieren mit Emulatoren und natürlich dem MIST(er) hat man häufig mit diesen .adf- (Floppy-Images) und .hdf- (Harddisc-Images) Dateien zu tun. Oftmals will man “nur kurz” einen Blick reinwerfen und dazu nicht gleich den Emulator hochfahren. Zumal die Emulatoren auch nicht alle so richtig nutzerfreundlich sind was das “Mounten” des Images angeht.

Also: ein Tool muss her. Systemunabhängig und natürlich mit anständigem UI. Also Java. Das Problem: Filecore wurde von echten Bitfuchsern entwickelt. Da wimmelt es nur so von Bitleisten, unsigned values mit allem zwischen 8 und 64 bit und sonstigem, was unter Java nicht so richtig “nativ” unterstützt wird. Übrigens gibt es Spezialisten, die es auch noch gut finden, dass Java keine “unsigned types” hat – für mich ist dieser Artikel ein Beispiel dafür, wie eine limitierte Erfahrung auf nur wenige Programmiersprachen den Blick aufs wesentliche vernebeln kann, denn die allermeisten der genannten Probleme sind nur inhärent im Java- und C-Typsystem, aber in vernünftigen Sprachen wie Ada elegant gelöst. Dumm, dass die Java-Erfinder vermutlich einen genauso eingeschränkten Blickwinkel hatten – C, C++, Smalltalk, Ende der Geschichte. Aber ich schweife ab.

Jedenfalls habe ich nach Bibliotheken gesucht, die unter Java die Bitfrickelei und unsigned types im Handling etwas angenehmer machen. Leider bin ich bis auf Lukas Eders jOOU-Bibliothek auf nix gescheites gestoßen. Klar, im Apache-Commons-Universum gibt es ein paar Dinge, in Guava ebenso, und sogar Oracle hat das Licht gesehen und ein paar nützliche Dinge in Java 8 nachgerüstet. Also: selbst ist der Mann. “Not invented here” ist schließlich eine der grundsätzlichen Triebfedern der IT.

Es sei denn, jemand hat einen Hinweis auf eine vernünftige, umfassende Bibliothek um meine Bedürfnisse zu erfüllen. Unter ebenso vernünftiger Open-Source-Lizenz natürlich. UAwg! Nebenbei: die eigentliche Implementierung ist nun nicht das große Problem, das Finden geeigneter Doku und fehlerarme Interpretation derselben nebst Erkennen gewünschter und unerwünschter Abweichungen davon in der Praxis hingegen schon. Hat man das Problem erst mal in einer Sprache geknackt, sollte eine Portierung in andere Sprachen kein großes Problem sein. Es wäre eine interessante Fallstudie, den Ada-Code neben dem Java-Code zu sehen.

Und falls jemand ein anständiges (nicht jedoch komplexes!) Dateisystem kennt mit einer simplen Implementierung in einer freien Lizenz – mail me. Filecore hat einen Nachfolger schon seit etwa 30 Jahren verdient.