Technical Article

Deploying the PDFium DLL in Delphi: Fixing Load Failures

Wenn das Laden von pdfium.dll auf einem Zielsystem fehlschlägt, übersetzt die PDFium-Komponente für Delphi und Lazarus den kryptischen Betriebssystemfehler in eine konkrete Ursache. Die Routine CheckLoadLibrary deutet den Windows-Fehler ERROR_BAD_EXE_FORMAT (193) als 32/64-Bit-Architekturkonflikt, und CheckGetProcAddress meldet einen fehlenden Funktions-Export als Hinweis darauf, dass die pdfium.dll älter ist als die Pascal-Deklarationen. Beide Meldungen benennen die Lösung, statt Sie im Dunkeln stehen zu lassen

Dies ist wichtig, da eine native DLL auf drei verschiedene Arten fehlschlagen kann und die rohen Fehlermeldungen des Betriebssystems diese oft vermischen. Die Architektur kann falsch sein, die bereitgestellte Binärdatei kann zu alt für den Pascal-Wrapper sein, oder zwei Threads können versuchen, sie im selben Moment zu laden. Jedes Problem erfordert eine andere Lösung, und der Sinn der in v2.11.0 hinzugefügten Lebenszyklus-Diagnose besteht darin, Ihnen genau zu sagen, womit Sie es zu tun haben

Warum schlägt das Laden von pdfium.dll mit dem Fehler "Zulässiges Format fehlt" fehl?

Der Windows-Fehler 193, ERROR_BAD_EXE_FORMAT, bedeutet, dass die Datei gefunden und geöffnet wurde, ihr PE-Dateityp (Portable Executable) jedoch nicht zum ausführenden Prozess passt. Eine 64-Bit-Anwendung kann keine 32-Bit-Version der pdfium.dll laden, und ein 32-Bit-Prozess kann keine 64-Bit-Version laden. Da die Datei vorhanden ist, führt die Vermutung, dass eine DLL fehlt, in die völlig falsche Richtung

Die PDFium-Komponente fängt diesen Fall in CheckLoadLibrary ab: Wenn LoadLibrary ein Null-Handle zurückgibt und GetLastError 193 lautet, fügt sie einen Hinweis an, dass die DLL zwar gefunden wurde, ihre Architektur jedoch nicht zum Prozess passt, und verweist auf das Verzeichnis DLLs/Win32 bzw. DLLs/Win64. Das Unterverzeichnis wird von BuildDllSubDir ausgewählt, das Win64 zurückgibt, wenn IsWin64 auf True steht, und andernfalls Win32. Stellen Sie die DLL im Pfad bereit, den die Komponente tatsächlich durchsucht, und das Problem ist behoben

uses
  SysUtils, PDFium;

procedure OpenDocument(const AFileName: string);
var
  Pdf: TPdf;
begin
  // Deploy pdfium.dll next to the executable, matched to the build target:
  //   <AppDir>\DLLs\Win32\pdfium.dll   for a 32-bit host process
  //   <AppDir>\DLLs\Win64\pdfium.dll   for a 64-bit host process
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := AFileName;
    try
      Pdf.Active := True;   // first use lazily binds pdfium.dll
    except
      on E: EPdfError do
        // The message already names the real cause: a 32/64-bit mismatch
        // (ERROR_BAD_EXE_FORMAT) or a pdfium.dll older than this binding
        raise Exception.CreateFmt('PDF engine did not start: %s', [E.Message]);
    end;
    if Pdf.Active then
      RenderFirstPage(Pdf);
  finally
    Pdf.Free;
  end;
end;

Was sagt Ihnen ein fehlender PDFium-Export?

Die zweite Fehlerklasse ist die Versionsabweichung. PDFium führt im Laufe der Zeit neue Funktions-Exporte ein, und der Pascal-Wrapper löst jede benötigte Funktion über CheckGetProcAddress während des Ladens auf. Wenn ein erforderlicher Export fehlt, ist der Wrapper neuer als die pdfium.dll auf der Festplatte. Die Diagnose lautet in diesem Fall, dass die bereitgestellte DLL veraltet (und nicht beschädigt) ist. Die Funktion CheckGetProcAddress meldet genau das: Sie löst eine EPdfError-Exception aus und meldet, dass die bereitgestellte pdfium.dll älter ist als die genutzte Version des PDFiumPas-Wrappers, und nennt das Verzeichnis DLLs/<subdir>, in das die passende Binärdatei gehört

Zwei Details sorgen dafür, dass dieser Ablauf robust ist. Erstens ruft CheckGetProcAddress die Methode UnloadLibrary auf, bevor die Exception ausgelöst wird. So verbleiben nach einem unvollständigen Ladeversuch niemals unvollständig verknüpfte Funktionszeiger im Speicher, über die der nächste Ladeversuch stolpern könnte. Zweitens stammt der gemeldete DLL-Name aus BuildDllName, das normalerweise pdfium.dll zurückgibt, aber pdfium.v8.dll liefert, wenn das globale Flag EnableV8Engine gesetzt ist. Wenn Ihre Anwendung die V8-JavaScript-Engine für XFA- oder skriptgesteuerte Formulare nutzt, weist der Fehlertext auf die V8-Version hin, damit Sie auf Anhieb die richtige Datei austauschen

Ist das Laden der PDFium-DLL aus einem Hintergrund-Thread heraus sicher?

Das Laden der Bibliothek ist nun aus jedem Thread heraus sicher. Der Aufruf der PDFium-API aus mehreren Threads heraus ist es jedoch weiterhin nicht. Dies sind zwei verschiedene Dinge. Die Trennung zu beachten, entscheidet über die Stabilität Ihres Worker-Thread-Pools. Das Rendern im Hintergrund führt jede Seiten-Rendering-Aufgabe meist über einen TPdfFuture<T>-Worker aus. Der erste Thread, der mit TPdf interagiert, löst das verzögerte Laden aus. Ohne Schutz könnten zwei Worker-Threads gleichzeitig feststellen, dass die Bibliothek noch nicht geladen ist, beide den Ladevorgang starten, das Modul-Handle überschreiben und das zuerst geladene Modul im Speicher verwaisen lassen

Die v2.11.0 schließt diese Lücke mit PDFiumLoadLock, einem prozessweiten TRTLCriticalSection-Objekt, das den Zugriff auf LoadLibrary und UnloadLibrary umschließt. Diese Critical Section macht die Abfrage-und-Lade-Sequenz atomar, sodass keine zwei Threads gleichzeitig Loaded=False sehen und die DLL doppelt laden können. Ebenso kann kein Thread die DLL freigeben, während ein anderer sie gerade lädt. Die Sperre wird im initialization-Abschnitt der Unit erstellt und im finalization-Abschnitt abgebaut, geschützt durch das Flag PDFiumLoadLockReady, damit dies auch beim Beenden der Anwendung sicher läuft. Wenn Sie das gesamte Worker-und-Antwort-Muster mit Unterstützung für Abbrüche benötigen, beschreibt der Begleitartikel über Hintergrund-Rendering mit abbrechbaren Futures diesen Ablauf ausführlich

uses
  PDFium, FPdfAsync;

// Worker method runs on a background thread. The first future to bind
// pdfium.dll is serialised by PDFiumLoadLock, so a second concurrent
// worker cannot double-bind or leak the module handle.
function TReportForm.RenderThumbnail(
  const AToken: IPdfCancellationToken): TBitmap;
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Active := True;         // safe concurrent bind
    AToken.ThrowIfCancelled;
    Result := Pdf.Thumbnail;    // this TPdf is owned by one thread only
  finally
    Pdf.Free;
  end;
end;

procedure TReportForm.StartRender;
begin
  TPdfFuture<TBitmap>.Run(RenderThumbnail, ThumbnailReady);
end;

procedure TReportForm.ThumbnailReady(
  const AResult: TPdfFutureResult<TBitmap>);
begin
  if AResult.IsSuccess then
    Preview.Picture.Assign(AResult.Value);
end;

Was die Ladesperre nicht schützt

Diese Grenze sollte man sich bewusst machen, da man die Fehlerbehebung sonst leicht überschätzt: PDFiumLoadLock serialisiert nur den globalen Bindungszustand — das Modul-Handle und die Funktionszeiger-Zuweisungen der FPDF_*-Methoden. Die zugrunde liegende PDFium-C-API ist jedoch weiterhin nicht threadsicher, genau wie es die Upstream-Header deklarieren. Das Aufrufen der FPDF_*-Funktionen oder das Teilen eines FPDF_DOCUMENT-Handles über mehrere Threads hinweg muss also weiterhin von Ihnen selbst serialisiert werden. Das sichere Entwurfsmuster sieht aus wie oben gezeigt: Jeder Worker besitzt sein eigenes TPdf-Objekt und übergibt niemals ein geöffnetes Dokument-Handle an einen anderen Thread. Eine detaillierte Behandlung dieser Grenze und der ABI-Regeln finden Sie unter Härten des PDFium-VCL-Wrappers gegen ABI- und Speicherfehler

Warum der finalization-Abschnitt und FPDF_DestroyLibrary wichtig sind

Eine unauffällige Lücke befand sich früher im Beendigungspfad: Die PDFium-Unit besaß keinen finalization-Abschnitt, was bedeutete, dass FPDF_DestroyLibrary beim Beenden des Prozesses niemals aufgerufen wurde. Das Betriebssystem gab zwar den Speicher der DLL frei, übersprang jedoch die interne Bereinigung von PDFium, wie das Beenden der Worker-Threads, das Freigeben des V8-Isolates (wenn EnableV8Engine gesetzt ist) sowie den Abbau der Schriftarten und Caches. Bei reinen Rendering-Aufgaben ist dies ein kleiner Speicherverlust beim Beenden der Anwendung. Bei aktivierter V8-Engine ging jedoch bei jedem Durchlauf ein JavaScript-Isolationsbereich verloren, was sich bei wiederholter Automatisierung schnell bemerkbar machte

Der finalization-Abschnitt ruft nun UnloadLibrary auf, was FPDF_DestroyLibrary ausführt und das Modul freigibt. Dies ist auf Unit-Ebene angesiedelt: TPdf.Destroy setzt nur Active := False, um das aktuelle Dokument zu schließen, entlädt jedoch bewusst nicht die globale Bibliothek. So behält eine Anwendung mit mehreren Instanzen eine einzige PDFium-Bindung über die gesamte Laufzeit des Prozesses bei. Das Platzieren des Abbaus im finalization-Abschnitt sorgt dafür, dass die gemeinsam genutzte Bibliothek genau einmal freigegeben wird, wenn die Unit entladen wird. UnloadLibrary prüft dabei zuerst das Flag Loaded, sodass der Aufruf wirkungslos bleibt, falls PDFium niemals genutzt wurde

Eine kurze Checkliste für die Bereitstellung

Drei Gewohnheiten verhindern fast jeden Ladefehler im Betrieb: Richten Sie die DLL-Architektur am ausführenden Prozess aus und platzieren Sie die Datei im Verzeichnis DLLs/Win32 bzw. DLLs/Win64. Halten Sie die Version der pdfium.dll synchron mit der Version des Wrappers, damit kein benötigter Funktions-Export fehlt. Und teilen Sie niemals ein TPdf-Objekt oder ein Dokument-Handle über Threads hinweg, auch wenn der Bindungsvorgang selbst nun atomar abläuft. Wenn Sie denselben Quellcode für Delphi und Lazarus kompilieren, beschreibt der Artikel über Fallstricke bei der Cross-Kompilierung in Delphi und FPC die Unterschiede beim Deployment. Die Diagnose, die Ladesperre und die Bereinigung im finalization-Abschnitt werden alle mit der PDFium-Komponente für Delphi und C++Builder ausgeliefert