HotXLS baut unter Free Pascal und Lazarus auf Windows, und das Portieren hing an vier Entscheidungen, die mit Object-Pascal-Syntax nichts zu tun haben: den Kern im DELPHIUNICODE-Modus halten, die OLE-Structured-Storage-Interfaces als CORBA-Interfaces mit handverwaltetem Referenzzählen deklarieren, die Win32-AES-Objektdateien durch eine Pascal-Implementierung ersetzen und eine Inflate-Schleife reparieren, die ein abgechnittenes ZIP als vollständig akzeptieren konnte
Wer einmal eine reife Delphi-Bibliothek portiert hat, kennt die Form dieser Arbeit. Der Compiler akzeptiert beim ersten Durchlauf fast alles. Danach kommt ein langer Schweif von Verhaltensunterschieden, die sauber kompilieren und falsche Ergebnisse liefern — und eine Spreadsheet-Engine ist ihnen ungewöhnlich stark ausgesetzt, weil sie Textkodierung, COM-Structured-Storage, Kompression und Kryptografie in einem einzigen Codepfad berührt
Warum besteht der Kern auf DELPHIUNICODE statt auf schlichtem DELPHI?
Weil die Formel-Engine darauf angewiesen ist, dass String und Char UTF-16-Semantik tragen, und die ANSI-Alternative verliert Zeichen, bevor überhaupt etwas die Datei erreicht. Es ist verlockend, den Kern im FPC-DELPHI-Modus zu bauen, denn das ist der Kompatibilitätsschalter, zu dem die meisten Ports greifen — und der Code kompiliert. Dann läuft eine Arbeitsmappe mit chinesischen Sheet-Namen oder kyrillischen Labels durch den Berechnungspfad, und die Zeichen sind weg, bevor der Writer sie zu sehen bekommt, ohne irgendeinen Fehler
Der Modus ist nicht über die ganze Bibliothek einheitlich, und das ist Absicht, nicht Schlamperei. Der PNG-Byte-Decoder und die LCL-Overrides brauchen wirklich ANSI-Signaturen, weil sie mit Bytes arbeiten und mit dem, was das Widgetset ihnen reicht. Diese Units schalten einen separaten LX_FPC_ANSI-Switch ein. Zwei Modi in einer Bibliothek klingen nach einem Smell, bis einem auffällt, dass die Alternative ein Byte-Decoder ist, der seine Eingabe als Text behandelt
Es gibt ein Begleitdetail, das Leute später erwischt. DELPHIUNICODE macht TFormatSettings.DecimalSeparator in der FPC-Runtime nicht zu einem WideChar. Eingaben mit einem Unicode-Dezimaltrenner müssen zuerst innerhalb des Unicode-Strings auf einen ASCII-Trenner normalisiert werden, und jede Eingabe, deren Trenner nicht zum erwarteten passt, muss abgelehnt werden, statt still am Zeichen abgeschnitten zu werden, das der Parser nicht erkannte
program ExportReport;
{$MODE DELPHI}
uses
Interfaces, // muss zuerst kommen: initialisiert das LCL-Widgetset
SysUtils, lxHandle; // und die UTF-8-Konvertierungsschicht
var
Book: TXLSWorkbook;
begin
Book := TXLSWorkbook.Create(nil);
try
Book.LoadFromFile('input.xls');
Book.Sheets[0].AsString[1, 1] := 'Quarterly summary';
Book.SaveToFile('output.xls');
finally
Book.Free;
end;
end.
Die Unit Interfaces ist nicht optional und muss zuerst kommen. Sie initialisiert das LCL-Widgetset und die UTF-8-Konvertierungsschicht, und HotXLS verlässt sich auf beides, sobald Fonts, Dateipfade oder Text die RTL-LCL-Grenze überqueren. Ein Konsolenprogramm, das sie überspringt, kompiliert und verhält sich auf jedem Nicht-ASCII-Pfad falsch. Das ist auch der Grund, warum eine erfolgreiche Kompilierung hier so wenig beweist: Das Portieren arbeitete erst nachweislich, als echte Dokumente mit echten Font-Namen und echten Pfaden eine volle Runde drehten
Eine Klassen-VMT ist keine COM-vtable
Free Pascal lässt einen nicht zu, eine Klassen-VMT als COM-Interface-vtable an Windows zu übergeben, selbst wenn die Deklaration identisch aussieht zu der, die Delphi akzeptiert. Die Layouts unterscheiden sich so, dass ein Aufruf im falschen Slot landet — sichtbar als Absturz irgendwo, nichts mit der Aufrufstelle zu tun hat. Structured Storage ist hier relevant, weil das klassische binäre Arbeitsmappenformat eine OLE-Compound-Datei ist, und eine zu lesen oder zu schreiben bedeutet, ILockBytes zu implementieren, in die die Windows-Storage-API zurückruft
Die funktionierende Lösung ist ein CORBA-Interface mit explizit deklarierten COM-Slots und von Hand verwaltetem AddRef und Release. Das heißt, auf automatisches Referenzzählen für diese Typen zu verzichten und die Lebensdauer selbst zu verantworten — ein fairer Tausch für eine Handvoll Interfaces, die in einer Unit leben. Die konkrete Falle in dieser Arbeit ist QueryInterface: Es muss einen Interface-Zeiger zurückgeben, nicht den Objektzeiger. Beides kompiliert. Eines davon reicht Windows eine Adresse, deren erstes Maschinenwort keine vtable ist
Die FPC-spezifischen Deklarationen liegen in lxOleInterfaces.inc, neben lxAESBackend.inc und lxZlibBackend.inc im FPC-Quellverzeichnis, sodass die compilerspezifischen Entscheidungen an einem Ort sitzen, statt durch die Engine verstreut zu sein. Das Format selbst und wie die Bibliothek darin navigiert, beschreibt OLE2-Compound-Dateien in Pascal lesen
Ein weiteres Typdetail gehört in dieselbe Familie. LargeInt muss im FPC-Zweig zu Int64 auflösen, und die Compiler-Klassifikation von Comp unterscheidet sich zwischen den beiden Toolchains so sehr, dass die Überladungsauflösung einen anderen Kandidaten wählen kann. Groß-Offset-Verhalten mit einem File-Stream testen, nicht mit einem HGLOBAL-Stream: Der Windows-Global-Memory-Stream wrappt bei Seeks über 4 GiB hinaus von selbst, ein bestehender Test dort beweist also nichts über die eigene Arithmetik
Was eine selbstkonsistente AES-Implementierung verbirgt
Die Win32-AES-Objektdateien, die der Delphi-Build linkt, sind OMF, und der Free-Pascal-Linker kann sie nicht verzehren, also nutzt der FPC-Zweig stattdessen eine Pascal-AES-Implementierung. Delphi linkt weiterhin die Objektdateien wie eh und je, was die ausgelieferte Binary für bestehende Kunden unverändert lässt
Die Verifikationsanforderung ist der Teil, den man in jedes Projekt mitnimmt. Daten zu verschlüsseln und mit derselben Implementierung wieder zu entschlüsseln beweist überhaupt nichts: Ein symmetrischer Algorithmus mit falschem Key Schedule, falscher Blockordnung oder falscher Verkettung ist perfekt selbstkonsistent und dreht seinen eigenen Output jedes Mal. Nur Known-Answer-Vektoren finden das, indem sie Key-Expansion, Blockordnung und CBC-Verkettung gegen veröffentlichte Werte prüfen. Eine selbstkonsistente falsche Implementierung ausliefern, und das Symptom erscheint, sobald ein Kunde die Datei zum ersten Mal in Excel öffnet
Die Kompression hatte einen Defekt anderen Charakters. Ein Pascal-Inflate-Backend kann noch Ausgabe offen haben, nachdem es seine gesamte komprimierte Eingabe verbraucht hat, also muss der Aufrufer weiter aufrufen, bis der Stream sein Ende meldet. Erschöpfte Eingabe als Streamende zu behandeln, stutzt den letzten Block ab. Schlimmer: Es macht aus einem beschädigten Archiv ein still akzeptiertes — genau der Fehlermodus, den die Härtung in der Validierung des ZIP-End-of-Central-Directory-Records verhindern soll. Die Regel: Kein Fortschritt plus nicht fertig ist ein Trunkierungsfehler, nie ein EOF
Zwei Build-System-Fallen, die echte Stunden kosten
Die LCL-Suchpfade müssen vor den FPC-Paket-Wildcard-Pfaden stehen, sonst überdeckt die Free-Vision-Unit Menus die gleichnamige LCL-Unit, und man bekommt eine PPU-Checksum-Abweichung, die über keine der beiden etwas aussagt. Eine Lazarus-Installation, die nach der Installation verschoben wurde, kann zudem veraltete Pfade in fpc.cfg hinterlassen, also geben die Build-Einstiegspunkte Unit- und Binärpfade explizit an, statt zu erben, was die Umgebung gerade anbietet
Die zweite Falle hat mit Pascal nichts zu tun. Eine .cmd-Batchdatei mit LF-Zeilenenden funktioniert, bis die Datei über die Größe des Lesebuffers des Interpreters hinauswächst — ab dann scheitert call :label mit der Behauptung, das Batch-Label existiere nicht, und der Fehler erscheint bei dem Programm, das zufällig hinter der Grenze sitzt. Jedes Tool, das ein Batch-Skript umschreibt, muss CRLF zurückschreiben. Und lazbuild --build-all leert das Paket-Unit-Ausgabeverzeichnis vor der Kompilierung, also wird eine in diesem Verzeichnis geparkte Optionsdatei gelöscht, bevor sie gelesen werden kann: draußen halten, und daran denken, dass der @-Pfad relativ zum Paketverzeichnis aufgelöst wird, weil lazbuild den Compiler von dort aufruft
// Lazarus-Grid-Export: TGridToXLS liegt im Lazarus-Paket bei, derselbe
// DB-Grid-Export-Code funktioniert also in einer LCL-Anwendung
var
Exporter: TGridToXLS;
begin
Exporter := TGridToXLS.Create(nil);
try
Exporter.DBGrid := GridOrders;
Exporter.WorksheetName := 'Orders';
Exporter.ExportHeader := True;
Exporter.SetColumnsWidth := True;
Exporter.ExportDBGrid;
Exporter.SaveAs('orders.xls');
finally
Exporter.Free;
end;
end;
Was eine Compiler-Warnung wert ist
Free Pascal meldet uninitialisierte lokale Variablen, die Delphi nicht meldet, und der FPC-Build machte aus diesem Unterschied zwei echte Defekte in der Berechnungs-Unit. Eine Funktion las eine Count-Variable, die vor der Nutzung nie zugewiesen wurde, und eine andere benutzte zwei Koordinaten in einem Zweig, bevor der Code, der sie berechnete, in einem anderen Zweig lief. Unter Delphi verhielten sich beide nach dem, was der Stack zufällig hielt — die Definition eines Bugs, der auf der einen Maschine reproduziert und auf der anderen nicht
Die praktische Konsequenz: Der zweite Compiler lohnt sich im Loop zu behalten, selbst für ein Produkt, das primär auf dem ersten ausgeliefert wird. Die FPC-Warnklassen periodisch zu scannen ist ein billiger statischer Analyse-Durchlauf über eine Delphi-Codebasis, und er findet eine Defektkategorie, die keine Testsuite verlässlich erreicht. Die breitere Versionsmatrix-Disziplin, in der das sitzt, beschreibt die Cross-Compiler-Build-Matrix
Free-Pascal- und Lazarus-Unterstützung für Windows liegt der HotXLS Delphi spreadsheet component als Lazarus-Paket neben den Delphi- und C++Builder-Paketen bei, gebaut aus demselben Quellbaum statt aus einem Fork. Das ist der Sinn der Übung: eine Engine, vier Toolchains, und die compilerspezifischen Entscheidungen isoliert in Include-Dateien, die sich in einem Rutsch lesen lassen