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
function CheckGetProcAddress(const Name: string): Pointer;
begin
Result := GetProcAddress(PDFiumLibrary, PChar(Name));
if Result = nil then
begin
// A missing required export means the deployed pdfium.dll is older
// than this build of the binding. Drop every pointer resolved so far
// so no caller can reach into the module we are about to free.
UnloadLibrary;
raise EPdfError.Create('Required PDFium export not found: ' + Name);
end;
end;
function TryGetProcAddress(const Name: string): Pointer;
begin
// Optional export. nil is a legitimate answer here; every caller is
// required to test Assigned() before dereferencing the variable.
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');
// Attachment descriptions were added after the bundled DLL revision.
// Keep them optional so older deployments continue to load.
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
function TPdf.GetAttachmentDescription(Index: Integer): WString;
begin
CheckActive;
Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
Result := '';
// Read side degrades: an old DLL cannot report /Desc, and '' is
// indistinguishable from an attachment that carries no description.
if not Assigned(FPDFAttachment_GetDescription) then
Exit;
// ... two-pass buffer sizing against FPDFAttachment_GetDescription ...
end;
procedure TPdf.SetAttachmentDescription(Index: Integer; const Value: WString);
begin
CheckActive;
Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
// Write side refuses: silently dropping the value would produce a file
// the caller believes carries a description and does not.
Check(Assigned(FPDFAttachment_SetDescription),
'Attachment descriptions are not supported by the loaded PDFium DLL');
// ... FPDFDoc_GetAttachment, then 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
// Ask once, at form setup, instead of discovering the limit on save.
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