Technischer Artikel

HotPDF unter Free Pascal und Lazarus: Win64-Grenzen

Die kurze Antwort auf dieses Support-Ticket lautet ja, mit Grenzen. HotPDF 2.730.0 baut unter Free Pascal 3.2.2 und Lazarus 4.6 für Win64, und die Kernpfade für Erzeugen, Laden und Speichern funktionieren. Was nicht mitkommt, ist alles, was auf einem statisch gelinkten nativen Codec-Objekt oder auf Delphi-anonymen Methoden ruht

Die Frage kommt meist auf dieselbe Art: Ein Team stellt auf Lazarus um für ein plattformübergreifendes Werkzeug, oder es erbt eine Free-Pascal-Codebasis und will dieselbe PDF-Komponente, die es für Delphi bereits lizenziert hat. Eine ausgereifte Delphi-Bibliothek zu portieren ist selten eine Syntaxfrage. Der interessante Teil ist, was die Portierung darüber verrät, wo die Bibliothek still an eine Toolchain gekoppelt war, und hier sitzt die Kopplung an zwei sehr konkreten Stellen: am Objektdatei-ABI der mitgelieferten Codecs und an den Compiler-Features hinter einem Versionssymbol

Eine Fähigkeitsmatrix, die den HotPDF-Delphi-Build mit dem Free-Pascal-3.2.2- und Lazarus-4.6-Win64-Build vergleicht und zeigt, welche Dokumentpfade geteilt sind und welche Codec-, Kompressions-, Parallel-Render- und Anonymous-Method-APIs auf einen werfenden Stub stoßen
Die Kernpfade für Erzeugen, Laden und Speichern sind in beiden Builds identisch, und die Lücke sitzt vollständig bei den statisch gelinkten Codecs und den Anonymous-Method-APIs

Was Free Pascal 3.2.2 braucht, bevor HPDFDoc kompiliert

HotPDF kompiliert unter Free Pascal nur im Delphi-Modus und nur, wenn die Lazarus-LCL-Unit-Verzeichnisse auf dem Suchpfad stehen. Beides ist nicht verhandelbar. HotPDF.inc schaltet den Compiler mit {$MODE DELPHI} und {$H+} in seinem {$IFDEF FPC}-Block um und weist Älteres mit einem {$FATAL} zurück, sobald FPC_FULLVERSION unter 30202 liegt, sodass eine 3.0.x-Installation laut scheitert, statt eine kaputte Unit zu erzeugen. Das Lazarus-Runtime-Paket HotPDFLaz.lpk kodiert den Rest: LCL als Pflichtpaket und -Mdelphi als benutzerdefinierte Option

Die LCL-Anforderung überrascht Leute, die nur Konsolenausgabe wollen, aber sie ist strukturell. HPDFFPCCompat liefert die Delphi-VCL-Typen, für die Free Pascal kein Äquivalent hat, und mappt TMetafile und TMetafileCanvas auf LCL-Bitmap- und Canvas-Klassen, während es TRichEdit auf TMemo aliasiert; HPDFDoc aliasiert TPNGObject auf Graphics.TPortableNetworkGraphic. Behandeln Sie das als Compilezeit-Shims, nicht als Feature-Parität: Eine Bitmap-gestützte Metafile-Klasse hält die Unit im Kompilat, sie lässt die Metafile-Pfade nicht so laufen wie unter Delphi. Sogar der Nicht-GUI-Smoke-Test zieht Interfaces herein, und das Build-Skript übergibt -Fu für lcl\units\x86_64-win64 und das lazutils-Ausgabeverzeichnis

Warum D2009+ nicht zugleich das Versions-Gate sein kann

Es ist verlockend, den Free-Pascal-Build als modernen Compiler zu behandeln und einfach das neueste Delphi-Feature-Symbol zu definieren. HotPDF tut das nicht, und der Grund lohnt Klartext: D2009+ bedeutet nicht nur Unicode-Strings, es gate auch Units, deren öffentliche API mit anonymen Methoden ausgedrückt ist. Free Pascal 3.2.2 unterstützt weder Delphi-anonyme Methoden noch diese APIs, also würde das Ausleihen des Symbols Code hereinziehen, der nicht kompilieren kann. Die uses-Klausel von HPDFDoc trägt deshalb zwei getrennte bedingte Schwänze, und die Überlappung zwischen ihnen ist absichtlich, nicht zufällig

uses
  // ...
  HPDFJavaScript,
  HPDFFormCalcGraph
{$IFDEF FPC}
  , HPDFFPCCodecStubs,
  HPDFCMS,
  HPDFWinCertSigner
{$ENDIF}
{$IFDEF D2009+}
  , HPDFXFARuntime,
  HPDFCMS,
  HPDFWinCertSigner,
  HPDFSignVerify,
  HPDFSignatureBatch
{$ENDIF};

Warum bleiben die nativen Codecs beim Linker hängen?

Weil sie Win64-COFF-Objekte sind, die von einer bestimmten Toolchain erzeugt wurden, und kein Free-Pascal-Linker unter Win64 sie annimmt: weder der interne Linker noch der externe GNU-ld-Pfad. Das ist ein Objektdatei-ABI-Problem, kein Pascal-Problem, und keine Menge bedingter Quelltexte behebt es. Die Bibliothek nimmt den einzigen ehrlichen Weg, der offen ist. Jede {$L}-Direktive, die ein statisches Codec-Objekt hereinzieht, ist in {$IFNDEF FPC} gewickelt, sodass der Free-Pascal-Build sie schlicht weglässt, und HPDFFPCCodecStubs liefert dann jedes fehlende externe Symbol als Stub, der wirft, statt zurückzukehren

// HPDFFPCCodecStubs.pas
function HPDFFPCNativeCodecUnavailable: PtrUInt;
begin
  raise ENotSupportedException.Create(
    'This native codec is not available in the Free Pascal build');
end;

function HPDFFPCStub_deflate: PtrUInt; cdecl;
  public name 'deflate';
begin
  Result := HPDFFPCNativeCodecUnavailable;
end;

Diese Stub-Tabelle ist lang, und ihr Lesen verrät genau, welche Fähigkeiten heute Delphi-only sind: die zlib-ng- und zopfli-deflate-Einsprungpunkte, libjpeg-Kompression und -Dekompression, den OpenJPEG-JPEG-2000-Codec, libtiff samt seiner Initialisierer je Kompression, JBIG2-Kodierung und -Dekodierung, die Little-CMS-Farbtransformations-Einsprungpunkte und die AES-Primitiven. Die Entwurfsentscheidung hinter den Stubs wiegt schwerer als die Liste. Ein fehlendes Symbol zur Linkzeit gibt Ihnen eine Wand undefinierter Referenzen aus einer Unit, die Sie nie angerührt haben; ein Stub, der ENotSupportedException wirft, gibt Ihnen einen Build, der läuft, eine Meldung mit dem Grund und einen Stacktrace, der auf die Aufrufstelle zeigt. Er bedeutet auch, dass ein Free-Pascal-Build nie stillschweigend falsche Bytes erzeugt, wo ein Delphi-Build korrekte liefern würde. Beachten Sie auch den Effekt zweiter Ordnung: Nicht vertrauenswürdige Image-Codecs in einem isolierten Prozess zu betreiben ist eine Entscheidung, die nur im Delphi-Build entsteht, denn ein Free-Pascal-Build hat überhaupt keinen nativen In-Process-Dekoder, den man sandboxen könnte

Unter Delphi werden die HotPDF-statischen Codec-Objekte gelinkt und nativ ausgeführt, während der Free-Pascal-Win64-Build die Link-Direktiven überspringt und jedes fehlende externe Symbol an einen Stub reicht, der an der Aufrufstelle eine benannte Exception wirft
Das Überspringen der Link-Direktiven und das Stubben jedes externen Symbols verwandelt eine Wand undefinierter Referenzen in einen Build, der läuft und seine Grenzen beim Namen nennt

Kompression: die erste zu ändernde Zeile ist cmNone

Bevor Sie irgendetwas anderes portieren, setzen Sie Compression auf cmNone. THPDFCompressionMethod bietet genau zwei Werte, cmNone und cmFlateDecode, und der zweite führt geradewegs in die deflate-Einsprungpunkte, die im Free-Pascal-Build Stubs sind. Verifizieren Sie zuerst das Kernobjektmodell mit ausgeschalteter Kompression, dann entscheiden Sie, was Sie sonst noch brauchen. Das ist die Reihenfolge des mitgelieferten Smoke-Tests: ein einseitiges unkomprimiertes Dokument erzeugen, neu laden und bestätigen, dass die Seitenanzahl als eins zurückkam. Unkomprimierte Ausgabe ist größer, und sie ist weiterhin ein völlig gültiges PDF

program HotPDFLazarusSmoke;

{$mode delphi}
{$H+}

uses
  Interfaces, SysUtils, HPDFDoc;

var
  Pdf, Reloaded: THotPDF;
  OutputFile: string;
  PageCount: Integer;
begin
  OutputFile := IncludeTrailingPathDelimiter(GetTempDir) +
    'HotPDF-FPC-Smoke.pdf';
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := OutputFile;
    Pdf.Compression := cmNone;   // cmFlateDecode landet auf einem gestubbten Symbol
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(72, 72, 0, 'HotPDF Free Pascal smoke test');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;

  Reloaded := THotPDF.Create(nil);
  try
    PageCount := Reloaded.LoadFromFile(OutputFile);
    if PageCount <> 1 then
      raise Exception.CreateFmt('Expected one page, got %d', [PageCount]);
  finally
    Reloaded.Free;
  end;
end.

Was geschieht mit dem parallelen Seiten-Rendering?

Es kompiliert weiter, liefert weiter korrekte Bitmaps und hört auf, parallel zu sein. THotPDF.RenderLoadedPagesParallel und THotPDF.RenderLoadedPagesParallelOrdered bauen auf TThread.CreateAnonymousThread mit einem Inline-procedure-Closure auf, den Free Pascal 3.2.2 nicht ausdrücken kann; der Free-Pascal-Zweig fährt daher einen deterministischen seriellen Fallback: Er geht die Seitenindizes der Reihe nach durch, ruft für jeden RenderLoadedPageToBitmap auf und zählt die Erfolge. Die API-Form, der Rückgabewert und das Ausgabearray bleiben unverändert, wodurch eine einzige Codebasis in beide Richtungen bauen kann

Derselbe HotPDF-Parallel-Render-Aufruf läuft unter Delphi auf überlappenden Worker-Threads und geht unter Free Pascal die Seitenindizes seriell durch, wobei der Pipeline-Info-Record eine Worker-Anzahl von eins meldet, statt den Fallback zu verstecken
Der Free-Pascal-Zweig behält die API-Form und das Ausgabearray und meldet dabei eine Worker-Anzahl von eins, sodass Code, der den Info-Record bereits liest, die Wahrheit sieht
var
  Bitmaps: THPDFBitmapArray;
  Info: THPDFParallelRenderPipelineInfo;
  Rendered: Integer;
begin
  Rendered := Pdf.RenderLoadedPagesParallel([0, 1, 2, 3], 150, 4,
    Bitmaps, Info);
  // Delphi: Info.WorkerCount ist, was das Speicherbudget erlaubte
  // Free Pascal: Info.WorkerCount ist immer 1, Seiten in Indexreihenfolge
  if Info.WorkerCount = 1 then
    LogSerialFallback(Rendered, Info.RequestedWorkerCount);

Der Fallback ist nicht stumm, und das ist der Teil, den man beim eigenen Entwurf einkalkulieren sollte. Er füllt THPDFParallelRenderPipelineInfo ehrlich: PageCount aus der Anfrage, RequestedWorkerCount als Echo dessen, was Sie verlangten, WorkerCount auf 1 gesetzt sowie abgeschlossene und ausgelieferte Zählungen passend zu dem, was wirklich zurückkam. Code, der Info bereits inspiziert, um einen Fortschrittsbalken oder ein Speicherbudget zu bemessen, funktioniert weiter und liest die Wahrheit statt einer Annahme. Hängt Ihr Durchsatzplan an der parallelen Render-Pipeline und ihrem Backpressure-Modell, ist dieser Plan ein Delphi-Plan; auf Free Pascal kalkulieren Sie die single-threaded Kosten des Renderns einer Seite in eine Bitmap mal der Seitenanzahl

Welchen Build liefern Sie tatsächlich aus?

Wählen Sie nach Fähigkeit, nicht nach Vorliebe. Wenn Ihr Workflow Dokumentmontage, Text- und Vektorzeichnung, Formularbefüllung, Laden und Speichern ist, deckt ihn der Free-Pascal-Build unter Win64 ab, und Sie sollten mit ausgeschalteter Kompression validieren, bevor Sie irgendetwas einschalten. Geht es um JPEG-, JPEG-2000-, TIFF- oder JBIG2-Bilder, ICC-Farbtransformationen, komprimierte Ausgabe oder Durchsatz, der von vielen Kernen abhängt, bleiben Sie vorerst bei Delphi oder C++Builder. Die Grenze ziehen ein Objektdatei-ABI und ein fehlendes Sprachfeature, beide im Quelltext sichtbar statt in einer Support-Matrix vergraben, und beide scheitern mit einer benannten Fehlermeldung statt mit einem falschen Ergebnis

Das Free-Pascal-und-Lazarus-Paket kommt in derselben Distribution wie die Delphi- und C++Builder-Units, also deckt eine Lizenz beide ab, und Sie können den Lazarus-Pfad an Ihren eigenen Dokumenten testen, bevor Sie sich festlegen; die Produktseite der HotPDF Delphi PDF Component führt die aktuelle Compiler-Support-Matrix und die vollständige API-Referenz