HotPDF kompiliert und läuft unter Free Pascal 3.2.2 mit Lazarus, und die ehrliche Zusammenfassung dieses Ports sind zwei Sätze. Dokumenterzeugung, Laden, Speichern, Kompression, Dekompression, Verschlüsselung und Entschlüsselung funktionieren alle auf reinen Pascal-Backends, sodass eine Lazarus-Anwendung echtes PDF ohne jegliche C-Abhängigkeit erzeugen und konsumieren kann. Die optionalen nativen Bildcodecs tun es nicht, denn die vorgebauten Win64-Objekte verwenden eine COFF-Variante, die kein Free-Pascal-Linker konsumieren kann, sodass auf dieser Toolchain die Einstiegspunkte zu Stubs aufgelöst werden, die passiv scheitern
Von „kompiliert“ bis „funktioniert“ brauchte es einen bestimmten Satz Fixes, und jeder einzelne ist eine Falle, die jede andere Delphi-Codebasis erwischen wird, die zu Free Pascal zieht. Es lohnt sich, sie in der Reihenfolge festzuhalten, in der sie wehtaten
Warum beweist das Kompilieren einer Unit nichts?
Weil eine Pascal-Unit auf ein Symbol verweisen kann, das niemals etwas Nützliches tun wird, und trotzdem den Compiler zufriedenstellt. Als alle 113 Bibliotheks-Units unter Free Pascal sauber bauten, funktionierten die Archivcontainer-Handler tatsächlich, verifiziert durch einen Smoke-Test, der eine CBZ öffnete und sie nach PDF konvertierte. XFA-Formularflattening funktionierte überhaupt nicht, denn Flattening muss den komprimierten /XFA-Paketstrom inflateen, und der Deflate-Einstiegspunkt war noch ein Stub. Nichts im Build-Output unterschied diese beiden Fälle
Die Regel, die daraus entstand, ist kurz. Bevor man in eine Release-Note schreibt, dass ein Feature auf einer neuen Toolchain funktioniert, schreibe man eine Runtime-Sonde, die das Feature auf dieser Toolchain Ende-zu-Ende ausübt. Compile-Abdeckung ist eine Voraussetzung, nie ein Beweis. Das größere Bild dessen, was der Port abdeckt, liefern die Free-Pascal- und Lazarus-Win64-Supportnotizen
Ein Raise in einem cdecl-Stub erreicht den Aufrufer nicht
Das verdient eine eigene Sektion, weil das Symptom so irreführend ist. Die Stub-Units legen C-Einstiegspunkte offen, wie es eine statische Bibliothek täte, also sieht ein Stub so aus
// Sieht vernünftig aus. Ist es nicht.
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
public name 'inflate';
begin
raise ENotSupportedException.Create('codec unavailable');
end;
Auf Free Pascal für Win64 propagiert diese Exception nicht zum Aufrufer. Es gibt keinen try..except-Handler, der sie sieht, denn das Entfalten über eine so deklarierte cdecl-Grenze trägt den Pascal-Exceptionframe nicht mit sich; der Prozess terminiert mit Exit-Code 217. Aus Sicht der Anwendung gibt es keinen Fehler, keine Meldung und keine Logzeile, nur ein Programm, das verschwindet. Das ist strikt schlechter als eine falsche Antwort, denn eine falsche Antwort kann behandelt werden
Der verführerische Fix ist, den Stub stattdessen einen Fehlercode zurückgeben zu lassen, und bei inflate ist das richtig, weil zlib einen klar definierten Fehler-Rückgabewert hat. Im Allgemeinen ist es falsch: Ein Stub für jpeg_read_header, der null zurückgibt, sagt dem Aufrufer, mit einer Struktur weiterzumachen, die niemand initialisiert hat. Der dauerhafte Fix ist, am Pascal-Einstiegspunkt vorzuschalten statt im C-förmigen Stub, mit der Fehlerkonvention, die diese API bereits hat
function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
// Ablehnen, bevor der Stub überhaupt erreicht wird, mit der eigenen
// Fehlerkonvention dieser API statt einer Exception über cdecl
Bitmap := nil;
Result := False;
Exit;
{$ENDIF}
Result := DecodeJPEGNative(Data, Bitmap);
end;
paszlib ist nicht zlib, und der Unterschied sind zwei Dokumentklassen
Die unter Free Pascal verfügbare Pascal-Deflate-Implementierung beherrscht zwei Framings: den zlib-Wrapper und rohes Deflate. Sie beherrscht kein gzip-Framing, das zlib über windowBits-Werte von 16 bis 31 wählt, und sie beherrscht nicht den automatischen Erkennungsmodus, den Werte 32 bis 47 wählen. HotPDF braucht beides. Der sichere SVG-Import-Pfad fordert 31 an, und der Loader hat eine Fallback-Leiter, die 47 anfordert, wenn das Framing eines Streams mehrdeutig ist. Lässt man eines davon weg, hört eine ganze Dokumentfamilie auf zu öffnen, mit einem Decode-Fehler, der auf den Stream zeigt statt auf das fehlende Framing
Es gibt eine zweite, schärfere Inkompatibilität. Der z_stream-Datensatz, den paszlib deklariert, hat nicht dasselbe Speicherlayout wie das C-Pendant: Sein msg-Feld ist ein Shortstring statt eines Zeigers, und total_in und total_out sind 64 Bit, wo die C-ABI Maschinenworte hat. Ein Aufrufer-Datensatz kann also nicht direkt durchgereicht werden. Die funktionierende Anordnung ist, den paszlib-Zustand hinter dem state-Zeiger zu halten, den der öffentliche Datensatz bereits reserviert, und die öffentlichen Felder um jeden Aufruf herum hinein und heraus zu kopieren. Die gzip-CRC und der acht Byte lange Längstrailer werden in derselben Shim-Schicht verrechnet, was der natürliche Ort dafür ist, da sie ohnehin die Framing-Entscheidung besitzt
Ein dynamisches Array an einen untypisierten var-Parameter übergeben
Das ist der Fehler, der gerade jetzt mit größter Wahrscheinlichkeit in Ihrem Code sitzt. Wenn Sie ein dynamisches Array an einen untypisierten var-Parameter übergeben, erhält der Empfänger die Adresse der Array-Variablen, also die Adresse eines Zeigers, nicht die Adresse der Nutzdaten. Ein Lesevorgang hinein überschreibt also die Variable selbst und was auch immer daneben liegt
var
FBuffer: TBytes;
begin
SetLength(FBuffer, 65536);
// Falsch: übergibt die Adresse der FBuffer-Variablen
FStream.Read(FBuffer, Length(FBuffer));
// Richtig: übergibt die Adresse des ersten Nutzdaten-Bytes
FStream.Read(FBuffer[0], Length(FBuffer));
end;
Auf Delphi erscheint die falsche Form häufig zu funktionieren, weil sie einen benachbarten Stackslot korrumpiert, den danach nichts liest. Auf Free Pascal verursacht dieselbe Zeile einen Segmentierungsfehler bei der ersten Verwendung. Was sie so schwer mit dem Auge zu erkennen macht: Statische Arrays haben dieses Problem nicht, denn eine statische Array-Variable ist ihre eigenen Nutzdaten, also sind beide Schreibweisen in derselben Datei korrekt, je nach Deklaration ein paar hundert Zeilen weiter
ZIP-Container ohne System.Zip
Free Pascal hat kein Äquivalent der RTL-Zip-Unit, und die verfügbare Alternative hat sowohl eine andere API-Oberfläche als auch keine Unterstützung für die Legacy-Verschlüsselung, die ältere Containerformate noch verwenden, also erwies sich ein kleiner hauseigener Reader als kürzer als die Anpassung an sie. Zwei Formatdetails kosteten Zeit und sind leicht falsch zu machen
Das erste ist das Prüfbyte des Verschlüsselungsheaders. Sein zwölftes Byte ist normalerweise das hohe Byte der CRC, aber wenn das allgemeine Flag-Bit 3 gesetzt ist, was bedeutet, dass die Größen in einem nachgelagerten Data Descriptor leben und die CRC noch nicht bekannt ist, stammt das Prüfbyte stattdessen aus dem hohen Byte der Änderungszeit. Implementiert man nur die CRC-Form, weist jedes im Streaming-Modus geschriebene Archiv ein korrektes Passwort zurück. Das zweite ist das ZIP64-Extra-Feld: Seine drei 64-Bit-Felder erscheinen in fester Reihenfolge, werden aber nur geschrieben, wenn das entsprechende 32-Bit-Feld gesättigt ist, also funktioniert das Lesen an festen Offsets bei den Archiven, die Sie getestet haben, und scheitert am nächsten. Parsen Sie sie positional danach, welche 32-Bit-Felder gesättigt sind
Eine Annehmlichkeit, die man kennen sollte: Der Free-Pascal-Dekompressionsstream nimmt ein zweites Konstruktorargument, das den zlib-Header überspringt, was genau das ist, was ZIP-Einträge brauchen, denn sie speichern rohes Deflate. Dieser Pfad berührt den zlib-Shim der Bibliothek überhaupt nicht, also ist er vom fehlenden C-Backend unberührt
Farbglyph-Transparenz unter der LCL
Das Auslesen des Alphakanals eines gerasterten Farbglyphs ist das eine Grafikdetail ohne direkte Übersetzung. Die LCL-PNG-Klasse hat keinen Scanline-Accessor, der Alpha freigibt, und die Zuweisung eines PNG an ein Bitmap verwirft ihn, sodass ein Farb-Emoji vollständig opak ankommt und sich mit einer schwarzen Box dahinter komponiert. Der funktionierende Weg ist das Interface-Bild: aus dem PNG erzeugen, dann Pixel über den Farb-Accessor lesen, im Kopf behaltend, dass seine Komponenten 16 Bit haben und um acht nach unten geschoben werden müssen, um Bytes zu werden. Diese Oberfläche verwendet außerdem die natürliche Zeilenreihenfolge von oben nach unten, also muss die Height - 1 - Y-Inversion, die VCL-Scanline-Code braucht, entfernt statt portiert werden
Zwei Build-System-Notizen, bevor Sie einen Bug melden
Ein vollständiger Rebuild scheitert gelegentlich mit einem undefinierten Symbol, dessen Name auf ein $crc-Suffix und einen Hexwert endet. Dieses Suffix wird aus den Parametertypen berechnet, und es passt nicht, wenn ein Build eine Unit im selben Durchlauf gegen zwei unterschiedliche Interface-Versionen kompiliert. Ein erneuter Lauf des Builds räumt es weg; die Signatur ist nicht falsch
Zweitens hat Free Pascal 3.2.2 keine anonymen Methoden, also greift der Free-Pascal-Build überall dort, wo die Bibliothek Closures verwendete, um eine parallele Pipeline zu verdrahten, stattdessen auf einen deterministischen seriellen Fallback zurück. Die Ausgabe ist identisch, der Durchsatz nicht; wenn Sie auf paralleles Seitenrendering angewiesen sind, ist das ein Grund, vorerst bei Delphi zu bleiben, und das Pipeline-Design beschreibt der Artikel zur parallelen Render-Pipeline. Die Bildcodec-Situation ist der andere Ort, an dem die Toolchain-Wahl die Fähigkeiten verändert und nicht nur die Geschwindigkeit, also sollte ein Lazarus-Deployment seine Bildformate entsprechend planen; die aktuelle Matrix je Toolchain steht auf der Produktseite der HotPDF Delphi PDF-Komponente