Technischer Artikel

Optionale PDFium-Exports: Capability-Gates in Delphi

Die eigene pdfium.dll lädt einwandfrei, und trotzdem fehlt eine Prozedur. Die PDFium-Komponente behandelt das, indem sie ihre Bindings in zwei Klassen aufteilt: erforderliche Exports, aufgelöst über CheckGetProcAddress, die das Laden komplett abbrechen, und optionale Exports, aufgelöst über TryGetProcAddress, die stattdessen einen nil-Zeiger und eine Capability-Prüfung hinterlassen

Das ist nicht dasselbe Problem wie eine nicht auffindbare DLL. Stirbt die Anwendung mit einem Fehler zum falschen EXE-Format, einer fehlenden Datei oder einer Architektur-Inkompatibilität, wird das in dem begleitenden Artikel zum Deployment von pdfium.dll und zur Diagnose von Ladefehlern behandelt. Hier ist der Loader erfolgreich. Das Modul-Handle ist gültig, hunderte Exports wurden aufgelöst, und der Lauf endet trotzdem, bevor die erste Seite gerendert wird, weil ein Einstiegspunkt, der in einem neueren PDFium-Build hinzukam, in der Binärdatei auf der Platte fehlt

Warum bricht ein fehlender Export die ganze Bibliothek?

Weil ein erforderliches Binding ein harter Vertrag ist, und er wird während einer einzigen Alles-oder-nichts-Bindesequenz durchgesetzt. Die PDFium-Komponente löst ihre gesamte Export-Tabelle innerhalb von LoadLibrary auf, ein CheckGetProcAddress-Aufruf nach dem anderen. Das erste nil-Ergebnis löst EPdfError aus und ruft davor UnloadLibrary auf, was beabsichtigt ist: Eine unvollständige Bindung würde sonst bereits aufgelöste Zeiger hinterlassen, die auf ein Modul zielen, das gerade freigegeben wird, und dabei still jede nachgelagerte Assigned-Absicherung aushebeln

Die Konsequenz ist der Fehlermodus, der einen hierher führt. Man aktualisiert die Komponente, liefert dieselbe pdfium.dll aus, die man seit zwei Jahren ausliefert, und die Anwendung startet nicht. Der Fehler nennt einen Export für eine Funktion, die man nie aufgerufen hat. Nichts, was man an der Aufrufstelle tut, hilft, weil die Aufrufstelle nie läuft; der Fehlschlag geschah während der Bindung, bevor überhaupt ein Dokument geöffnet wurde

PDFium Component bindet seine Delphi-Exporttabelle in einem Durchlauf: CheckGetProcAddress bricht das Laden bei einem fehlenden Pflichtexport ab, während TryGetProcAddress einen optionalen sicher abstufen lässt
Erforderliche Exporte binden alles-oder-nichts und brechen das Laden beim ersten nil ab, während optionale Exporte einen nil-Zeiger hinter einer Assigned-Fähigkeitsprüfung zurücklassen
function CheckGetProcAddress(const Name: string): Pointer;
begin
  Result := GetProcAddress(PDFiumLibrary, PChar(Name));
  if Result = nil then
  begin
    // Ein fehlender erforderlicher Export bedeutet, dass die bereitgestellte pdfium.dll älter ist
    // als dieser Build des Bindings. Verwerfe jeden bisher aufgelösten Zeiger,
    // damit kein Aufrufer in das Modul greifen kann, das wir gleich freigeben.
    UnloadLibrary;
    raise EPdfError.Create('Required PDFium export not found: ' + Name);
  end;
end;

function TryGetProcAddress(const Name: string): Pointer;
begin
  // Optionaler Export. nil ist hier eine legitime Antwort; jeder Aufrufer muss
  // Assigned() testen, bevor er die Variable dereferenziert.
  Result := GetProcAddress(PDFiumLibrary, PChar(Name));
end;

Erforderlich oder optional: wo die Grenze tatsächlich verläuft

Die von der PDFium-Komponente angewendete Regel ist unverblümt. Ein Export ist erforderlich, wenn sein Fehlen die Komponente unfähig macht, ihre eigentliche Aufgabe zu erledigen, und optional, wenn sein Fehlen nur eine einzelne Randfunktion entfernt. FPDF_InitLibrary, FPDF_LoadDocument, FPDF_RenderPageBitmap, FPDF_ClosePage sind erforderlich, und bei diesen laut zu scheitern, ist korrekt: Ein Viewer, der nicht rendern kann, ist kein degradierter Viewer, er ist defekt

Alles, was heute über den toleranten Loader erreicht wird, ist eine Randfunktion. FPDFBookmark_GetColor kam nach M109 hinzu und liefert nur das optionale /C-Farb-Array eines Outline-Eintrags, sodass eine DLL, die älter ist, einfach keine Lesezeichenfarbe meldet. Die V8-Hilfsfunktionen FPDF_GetRecommendedV8Flags und FPDF_GetArrayBufferAllocatorSharedInstance sowie die XFA-String-Hilfsfunktionen FPDF_BStr_Init, FPDF_BStr_Set und FPDF_BStr_Clear fehlen konstruktionsbedingt in jedem Nicht-V8-Build, sodass sie als erforderlich zu behandeln die schlichte pdfium.dll unladbar machen würde. Und das Paar, das diesen Artikel motivierte: FPDFAttachment_SetDescription und FPDFAttachment_GetDescription, upstream hinzugefügt am 2026-07-13, später als das Build-Datum aller vier PDFium-Binärdateien, die das Projekt unter DLLs/Win32 und DLLs/Win64 ausliefert. Dieser letzte Fall ist die allgemeine Form des Problems, kein Einzelfall: Eine Binding-Schicht verfolgt Upstream-Header, die sich kontinuierlich bewegen, während die DLL im eigenen Installer sich in diskreten Sprüngen bewegt, wann immer jemand sie neu baut. Es gibt immer ein Fenster, in dem die Pascal-Seite Exports kennt, die die ausgelieferte Binärdatei nicht hat, und im Voraus zu entscheiden, auf welcher Seite der Erforderlich/Optional-Grenze jeder neue Export liegt, ist das Einzige, das dieses Fenster überlebbar macht

FPDFDoc_GetAttachmentCount    := CheckGetProcAddress('FPDFDoc_GetAttachmentCount');
FPDFDoc_AddAttachment         := CheckGetProcAddress('FPDFDoc_AddAttachment');
FPDFAttachment_GetName        := CheckGetProcAddress('FPDFAttachment_GetName');
FPDFAttachment_GetStringValue := CheckGetProcAddress('FPDFAttachment_GetStringValue');
// Anhangbeschreibungen wurden nach der gebündelten DLL-Revision hinzugefügt.
// Sie optional zu halten, damit ältere Auslieferungen weiterhin laden.
FPDFAttachment_SetDescription := TryGetProcAddress('FPDFAttachment_SetDescription');
FPDFAttachment_GetDescription := TryGetProcAddress('FPDFAttachment_GetDescription');
FPDFAttachment_SetFile        := CheckGetProcAddress('FPDFAttachment_SetFile');
FPDFAttachment_GetFile        := CheckGetProcAddress('FPDFAttachment_GetFile');

Was soll ein Capability-Gate an der Aufrufstelle tun?

Es sollte asymmetrisch sein, und diese Asymmetrie ist das gesamte Design. Ein Lesevorgang, der nicht laufen kann, hat eine ehrliche leere Antwort. Ein Schreibvorgang, der nicht laufen kann, hat überhaupt keine ehrliche Antwort, also muss er eine Exception auslösen. Die PDFium-Komponente teilt die Eigenschaft für die Anhangsbeschreibung exakt entlang dieser Linie, und diese Aufteilung ist es, die einen fehlenden Export davon abhält, sich in stillen Datenverlust zu verwandeln. TPdf.GetAttachmentDescription testet Assigned(FPDFAttachment_GetDescription) und beendet mit einem leeren WString. Das ist keine Lüge: Auf einer DLL ohne den Export kann die Komponente wirklich nicht sagen, ob der Anhang einen /Desc-Eintrag trägt, und eine leere Beschreibung liest sich genauso wie ein Anhang, der nie eine hatte. Der Rest der Anhangs-API, behandelt in dem Artikel zur Arbeit mit PDF-Anhängen in Delphi, funktioniert unverändert weiter

TPdf.SetAttachmentDescription nimmt die entgegengesetzte Route. Es ruft Check auf demselben Assigned-Test auf und löst EPdfError mit dem Text "Attachment descriptions are not supported by the loaded PDFium DLL" aus. Hier still zurückzukehren wäre die schlechteste verfügbare Option: Der Aufrufer würde eine Beschreibung setzen, keinen Fehler erhalten, die Datei speichern und eine PDF-Datei ausliefern, in der die Beschreibung schlicht fehlt. Niemand bemerkt es, bis ein nachgelagerter Konsument fragt, wo sie geblieben ist

Ein fehlender PDFium-Attachment-Description-Export in Delphi gibt über TPdf.GetAttachmentDescription eine leere Lesung zurück und wirft beim Schreiben, bewacht durch AttachmentDescriptionFeaturesAvailable
Die Leseseite degradiert zu einer leeren Antwort, die Schreibseite wirft mit benanntem Grund, und eine benannte Sonde lässt die UI das Feature vorab deaktivieren
function TPdf.GetAttachmentDescription(Index: Integer): WString;
begin
  CheckActive;
  Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
  Result := '';

  // Leseseite verschlechtert sich: eine alte DLL kann /Desc nicht melden, und '' ist
  // nicht von einem Anhang zu unterscheiden, der keine Beschreibung trägt.
  if not Assigned(FPDFAttachment_GetDescription) then
    Exit;
  // ... zweipassige Puffergrößenbestimmung gegen FPDFAttachment_GetDescription ...
end;

procedure TPdf.SetAttachmentDescription(Index: Integer; const Value: WString);
begin
  CheckActive;
  Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
  // Schreibseite weigert sich: den Wert still fallen zu lassen würde eine Datei erzeugen,
  // von der der Aufrufer glaubt, dass sie eine Beschreibung trägt, obwohl sie keine hat.
  Check(Assigned(FPDFAttachment_SetDescription),
    'Attachment descriptions are not supported by the loaded PDFium DLL');
  // ... FPDFDoc_GetAttachment, dann FPDFAttachment_SetDescription ...
end;

Die Capability prüfen, bevor man die Funktion anbietet

Eine Exception abzufangen ist ein schlechter Weg herauszufinden, was das eigene Deployment kann, deshalb legt die PDFium-Komponente denselben Test als benannte Funktion offen. AttachmentDescriptionFeaturesAvailable ruft LoadLibrary auf und liefert zurück, ob beide Hälften des Paars aufgelöst wurden. Es steht neben V8FeaturesAvailable, XfaBStrHelpersAvailable und XfaFeaturesAvailable, die für ihre eigenen optionalen Gruppen demselben Muster folgen. Die Benennung der Prüfung ist wichtiger, als es aussieht: Ein Boolean namens AttachmentDescriptionFeaturesAvailable sagt dem nächsten Maintainer, dass diese Funktion vom ausgelieferten Binary abhängt, was ein blanker Assigned-Test, vergraben in einem Property-Setter, nie tut. Es gibt der UI-Schicht auch etwas zum Binden, sodass das Bearbeitungsfeld für die Beschreibung von vornherein deaktiviert ist, statt Eingaben anzunehmen und sie beim Speichern abzulehnen

procedure TAttachmentFrame.SyncCapabilities;
begin
  // Einmal fragen, bei der Formular-Einrichtung, statt das Limit beim Speichern zu entdecken.
  DescriptionEdit.Enabled := AttachmentDescriptionFeaturesAvailable;
  if not DescriptionEdit.Enabled then
    DescriptionEdit.TextHint := 'Requires a newer pdfium.dll';
end;

procedure TAttachmentFrame.SaveDescription(Pdf: TPdf; Index: Integer);
begin
  if not AttachmentDescriptionFeaturesAvailable then
    Exit;
  Pdf.AttachmentDescription[Index] := DescriptionEdit.Text;
end;

Warum muss Binding-Abdeckung durch ein Werkzeug bewiesen werden?

Weil die Zahlen den Punkt überschritten haben, an dem man einem Menschen damit vertrauen kann. Die PDFium-Komponente hat 21 öffentliche PDFium-Header gegen eine Upstream-Basislinie vom 2026-07-29 geprüft und 470 exportierte C-ABI-Funktionen gefunden. Das Binding deckte bereits 468 davon ab. Niemand fand diese Lücke von zwei durch das Lesen von Headern; ein Skript tat es, in einer Sekunde, und es wird es beim nächsten Upstream-Sprung wieder tun. tools/audit_pdfium_public_api.py ist bewusst klein: Es matcht per Regex FPDF_EXPORT ... FPDF_CALLCONV name( über jeden Header im öffentlichen Verzeichnis, matcht per Regex jedes CheckGetProcAddress('Name') und TryGetProcAddress('Name') in PDFium.pas und gibt die beiden Mengendifferenzen aus: missing für Exports ohne Binding, stale für Bindings, deren Export upstream nicht mehr existiert. Es beendet sich mit einem Fehlercode, wenn eine der beiden Mengen nicht leer ist, und fügt sich damit ohne weiteres Zeremoniell in einen Build-Schritt ein. Das aktuelle Ergebnis ist 470 von 470 gebunden, missing 0, stale 0

Die Stale-Richtung verdient sich ihren Platz genauso wie die Missing-Richtung. Ein Export, den Upstream entfernt, hinterlässt eine CheckGetProcAddress-Zeile, die jedes künftige Laden hart scheitern lässt, und diese Art von Verfall ist unsichtbar, bis jemand eines Tages die DLL aktualisiert. Manuelle Durchsicht findet die Funktion, an die man gerade gedacht hat; sie findet nicht die, an die man nicht gedacht hat. Zu beachten ist auch, dass die Prüfung bewusst beide Loader als Abdeckung zählt, was der richtige Ansatz für API-Drift ist, und der Grund, warum die Erforderlich/Optional-Aufteilung eine dokumentierte Entscheidung sein muss statt ein Nebenprodukt davon, wer die Zeile hinzugefügt hat

Wo optionales Binden aufhört, ehrlich zu sein

Zwei Grenzen sind es wert, klar benannt zu werden, weil das Muster leicht überstrapaziert wird. Die erste ist, dass ein nil-Funktionszeiger nur sicher ist, wenn buchstäblich jeder Pfad, der ihn berührt, zuerst Assigned testet. In einer Unit, die hunderte cdecl-Funktionsvariablen deklariert, ist ein einziger ungesicherter Aufruf eine Zugriffsverletzung an einer Adresse, die in einem Stack-Trace nichts bedeutet. Dieselbe Disziplin, die Aufrufkonventionen und Lebensdauern über die C-Grenze hinweg regelt, gilt auch hier, und das ist Thema von dem Artikel zur Härtung des PDFium-Bindings gegen ABI- und Speichersicherheitsfehler

Die zweite Grenze ist der Geltungsbereich. Optionales Binden ist keine allgemeine Lizenz, alles tolerant zu machen. Wäre FPDF_RenderPageBitmap optional, würde die Komponente bereitwillig laden und dann bei jeder Seite scheitern, wodurch sich ein klarer Startfehler in eine Streuung von Laufzeitfehlern ohne offensichtliche Ursache verwandelt. Erforderlich ist die richtige Vorgabe. Optional ist die Ausnahme, zu der man greift, wenn eine Funktion wirklich eine Randfunktion ist, wenn das Fehlen auf der Leseseite ein vertretbares degradiertes Verhalten hat, und wenn die Schreibseite mit einer Nachricht ablehnen kann, die den Grund benennt

Das Loader-Design, die Capability-Prüfungen und das hier beschriebene Audit-Werkzeug sind Teil der PDFium-Komponente für Delphi und C++Builder; die Produktseite listet die mitgelieferten PDFium-Binärdateien und die vollständige API-Oberfläche, die sie offenlegen