PDFlibPas lässt sich unter Free Pascal für 32-Bit-Windows bauen, und der schwierige Teil war nie Pascal selbst. Es waren die Objektdateien: Die AES- und OpenJPEG-Objekte, gegen die der Delphi-Build linkt, sind OMF, der interne Linker von Free Pascal verlangt aber COFF, und die Konvertierung zwischen beiden erzeugt Sektionsnamen und Sektionsdefinitionssymbole, an denen der Linker lieber mit internen Fehlern abbricht, als eine brauchbare Diagnose auszugeben
Wer schon einmal C-Objekte in eine Pascal-Bibliothek gelinkt hat, kennt dieses Terrain. Win64 ist vergleichsweise zivilisiert: ein Objektformat, eine Aufrufkonvention, keine Namensdekoration. Win32 bewahrt jede historische Schicht, die die Plattform angesammelt hat, und eine Bibliothek, die fremden C-Code statisch linkt, trifft auf alle gleichzeitig
Das Compiler-Verzeichnis verrät das Ziel nicht
Beginnen Sie am Build-Einstiegspunkt, denn wer hier falsch liegt, verschenkt Stunden, bevor überhaupt eine Objektdatei im Spiel ist. Der Name eines Free-Pascal-Installationsverzeichnisses sagt, wo der Hauptcompiler wohnt, nicht was er erzeugt. Ein 32-Bit-Host-Compiler kann einen danebenliegenden Cross-Compiler aufrufen und mit den richtigen Target-Switches 64-Bit-Code ausgeben. Wer das Ziel aus einem Pfad ableitet, rät also ins Blaue — solange, bis jemand die Toolchain umsortiert
Zuverlässig ist der Weg, den Compiler selbst zu fragen. Ermitteln Sie den tatsächlichen Zielprozessor und das Ziel-Betriebssystem über die Information-Switches des Compilers, und akzeptieren Sie beide gängigen Installationslayouts, das flache Binärverzeichnis wie das versionsverschachtelte, denn verschiedene Installer und Toolchain-Manager erzeugen unterschiedliche Strukturen. Ein Build-Skript, das eines der beiden Layouts fest verdrahtet, läuft exakt auf einer einzigen Maschine
Warum bricht eine konvertierte Objektdatei den internen Linker?
Weil die Konvertierung die OMF-Sektionsnamenskonvention beibehält und Sektionsdefinitionssymbole synthetisiert, die nicht zum Erwartungsbild des COFF-Linkers passen. Die OMF-Objekte nach COFF zu konvertieren ist notwendig, aber nicht hinreichend: Die entstandenen Dateien tragen die klassischen Sektionsnamen _TEXT, _DATA und _BSS plus die daraus abgeleiteten Namen der Sektionsdefinitionssymbole, und wer das dem internen Linker von Free Pascal vorwirft, erhält interne Compilerfehler statt einer Meldung zur Sektionsbenennung
Ein interner Fehler ist der schlechteste Fehlermodus für ein Build-Problem, denn er verrät nichts darüber, was an der Eingabe falsch war. Die Lösung ist ein Normalisierungslauf über die COFF-Datei nach der Konvertierung: Sektionsnamen in die erwartete Form umschreiben und die zugehörigen Sektionsdefinitionssymbole passend dazu, während Symbolindex, Code-Bytes und Relokationen unangetastet bleiben. Diese letzte Einschränkung ist die gesamte Schwierigkeit. Ein Umschreiben, das Symbole neu nummeriert oder Offsets verschiebt, erzeugt ein Objekt, das linkt und danach abstürzt
Für einen der beiden Objektsätze gibt es einen vorbereitenden Schritt. Die OpenJPEG-Objekte aus dem klassischen 32-Bit-C++-Compiler hängen von privaten 64-Bit-Integer-Helferroutinen von Delphi ab, die Free Pascal nicht mitbringt; keine noch so gute Formatkonvertierung macht sie dadurch brauchbar. Diese Objekte werden zunächst mit dem Clang-basierten Compiler neu gebaut, der diese Abhängigkeiten nicht erzeugt, und danach konvertiert
// Die Objekte für das FPC-Ziel liegen in einem eigenen Verzeichnis. Sie
// ersetzen nicht den Delphi-Objektsatz, denn beide Toolchains bauen aus
// demselben Quellbaum und jeder braucht seine eigenen Link-Eingaben
//
// Lib\thirdparty\Win32 Delphi-OMF-Objekte, unverändert
// Lib\thirdparty\Win32f FPC-COFF-Objekte, konvertiert und normalisiert
//
// Build-Einstiegspunkte:
// build-Win32-Lib-FPC.cmd
// build-Win64-Lib-FPC.cmd
Compiler-private Helfer sind nicht portabel, ihre Konventionen ebenso wenig
Die Delphi-Runtime stellt Assembler-Trampolines für 64-Bit-Integer-Operationen auf 32-Bit-x86 bereit, und vorkompilierte C-Objekte, die für Delphi gebaut wurden, springen diese an. Free Pascal hat eine eigene Lösung, daher müssen diese Referenzen anders befriedigt werden, statt sie umzuleiten. Das Detail, das eine Umleitung unmöglich macht, ist die Aufrufkonvention: Beim Timing-Helfer, den der Imaging-Code nutzt, räumt der Callee das vier Byte große Argument ab, während der 64-Bit-Divisions-Helfer sechzehn Byte abrechnet und sein Ergebnis im klassischen Registerpaar zurückgibt. Zwei Helfer, zwei Konventionen — und ein für den einen geschriebenes Trampoline korrumpiert den Stack für den anderen lautlos
Die Namensdekoration liefert die zweite Hälfte des Problems. Unter Win32 stellt Free Pascal externen C-Imports automatisch einen Unterstrich voran, exportiert aber public name-Deklarationen wörtlich — Import- und Exportseite derselben Brücke folgen also unterschiedlichen Regeln. Die C-Runtime-Brücke, die OpenJPEG braucht, muss daher exakt die C-Symbolnamen exportieren, und die variadischen Einsprungpunkte brauchen einen indirekten 32-Bit-Sprung statt eines direkten. Keines davon ist exotisch, sobald man es ausspricht. Und alles davon scheitert als Link-Fehler, der ein Symbol nennt, das niemand geschrieben hat
Warum starb eine Win32-Executable, bevor main lief?
Eine 64-Bit-DLL auf dem Suchpfad, erreicht, weil die Free-Pascal-Unit zlib dynamisch bindet statt statisch zu linken. Das Symptom war ein sofortiger Exit mit dem Invalid-Image-Statuscode, bevor irgendein Pascal-Code im Programm lief — man untersucht also das gerade gebaute Programm, während der Fehler darin besteht, dass der Loader einen Import gegen die falsche Architektur auflöst
Die Lektion betrifft Annahmen, nicht zlib. Eine Unit, die nach einer Kompressionsbibliothek benannt ist, enthält nicht zwangsläufig eine; sie kann ein Binding sein, das zur Laufzeit eine Shared Library erwartet, und eine unbeabsichtigte dynamische Abhängigkeit ist ein Deployment-Risiko, selbst wenn sie zufällig gerade auflöst. Der Wechsel zur reinen Pascal-Stream-Implementierung gibt beiden Zielen einen statisch eingebundenen Kompressionspfad ganz ohne externe Abhängigkeit — was eine Bibliothek, die in die Anwendung eines anderen eingebettet wird, ohnehin von Anfang an haben sollte
Dasselbe Prinzip gilt für das externe JBIG2-Encoder-Backend. Im 32-Bit-Ziel wird der externe Encoder nicht gelinkt, daher fallen Anfragen auf den eingebauten Pascal-Encoder zurück, und der Test, der das verifiziert, muss den Registrierungszustand des aktuellen Ziels prüfen, statt einen erfolgreichen Encode als Beweis dafür zu nehmen, dass das externe Backend vorhanden ist. Ein funktionierender Fallback ist genau das, was eine fehlende Abhängigkeit versteckt — das Fehlermuster, das in der Diagnose stiller Stub-Fehler untersucht wird. Die 64-Bit-Arbeit am statischen Linken ist in Statischem Linken von jbig2enc unter FPC beschrieben
32-Bit-Arithmetik auf einem Memory-Stream
Code, der Puffergrößen mit vorzeichenloser Arithmetik in Zeigerbreite manipuliert, ist auf Win64 korrekt und auf Win32 ein großes Bild vom Overflow entfernt. Der In-Memory-Stream, der den JPEG-2000-Codec speist, wächst durch Verdoppeln und rückt durch Addition vor, und auf einem 32-Bit-Ziel können beide Operationen bei Eingaben überlaufen, die groß, aber völlig legitim sind
Jeder Schreib-, Skip- und Seek-Vorgang sowie die anfängliche Allokation prüfen daher, bevor sie rechnen, und die Kapazitätsobergrenze ist der größte vorzeichenbehaftete Wert in Zeigerbreite, gewählt passend zu dem, was die Blockverschieberoutine und die Callback-Rückgabewerte ausdrücken können. Die Verhaltensanforderung, wenn eine Anfrage abgelehnt wird, ist leicht falsch umzusetzen: Eine Ablehnung darf weder die Stream-Position noch deren Länge verändern. Eine teilweise Mutation, auf die ein Fehler folgt, lässt den Stream in einem Zustand zurück, mit dem der Aufrufer nicht mehr rechnen kann, und die nächste Operation macht es schlimmer
// Erst prüfen, dann rechnen. Auf Win32 laufen beide Stellen bei Eingaben
// über, die ein großes JPEG-2000-Bild ganz legitim erzeugt
if Needed > NativeUInt(High(NativeInt)) - FPosition then
Exit(False); // ablehnen, Position und Größe unangetastet lassen
NewCapacity := FCapacity;
while (NewCapacity < FPosition + Needed) do
begin
if NewCapacity > NativeUInt(High(NativeInt)) shr 1 then
Exit(False); // Verdoppeln würde überlaufen
NewCapacity := NewCapacity shl 1;
end;
Zwei Build-Ausgaben-Fallen, die das Portieren überdauern
Test- und Beispiel-Executables nach Zielarchitektur in eigene Ausgabeverzeichnisse zu trennen, ist offensichtlich richtig und bricht sofort alles, was seine Testdaten durch Hochzählen von Verzeichnisebenen gefunden hat. Die Lösung ist, das Asset-Verzeichnis aufwärts zu suchen, statt eine feste Tiefe anzunehmen — mit einer bewussten Einschränkung: Das Signing-Beispiel akzeptiert einen Zertifikat-Fallback nur aus seinem eigenen Projektverzeichnis, nie aus einem beliebigen Vorfahren, denn ein gleichnamiges Zertifikat weiter oben im Baum ist eine Sicherheitsüberraschung, keine Annehmlichkeit
Die zweite Falle überlebt jedes Portieren und gehört in jedes FPC-Projekt mitgenommen. Nach einem Compiler-Upgrade reicht es nicht, dass der Compiler veraltete PPU-Dateien zurückweist, denn der Linker bevorzugt weiterhin liegengebliebene Objektdateien im Unit-Suchpfad, selbst wenn die geladene PPU aus dem richtigen Verzeichnis kam, und ein expliziter Objektausgabepfad ändert an dieser Bevorzugung nichts. Die einzige verlässliche Antwort ist ein frisches temporäres Unit-Verzeichnis pro Build-Runde. Alles andere erzeugt eine Binary, die aus zwei Compilerversionen gelinkt wurde und auf Arten scheitert, die nach Quellcode-Bugs aussehen
Plattform-Conditionals sind der letzte Baustein, und die richtige Achse zu wählen, matter mehr, als es scheint. Die richtige Frage lautet meist, ob der Code Windows-spezifisch ist, nicht ob eine bestimmte Widget-Bibliothek vorhanden ist, wie die Metafile-Konvertierungsarbeit in EMF-Vektorimport und Plattform-Conditionals zeigte: Diese Guard von einer Control-Library-Bedingung auf eine Plattformbedingung umzustellen, machte aus einer angeblichen Neuschreibung eine Änderung an einer einzigen Direktive. Free-Pascal- und Lazarus-Unterstützung für beide Windows-Ziele liegt der PDFlibPas Delphi PDF library bei, gebaut aus denselben Quellen wie die Delphi- und C++Builder-Pakete