Je pdfium.dll laadt prima en er ontbreekt nog steeds één procedure. PDFium Component pakt dit aan door zijn bindings in twee klassen te splitsen: verplichte exports opgelost via CheckGetProcAddress, die de load ronduit afbreken, en optionele exports opgelost via TryGetProcAddress, die in plaats daarvan een nil-pointer en een capability-check achterlaten
Dit is niet hetzelfde probleem als een DLL die niet gevonden kan worden. Als je applicatie sterft met een bad-EXE-format-fout, een ontbrekend bestand, of een architectuurmismatch, dat verhaal wordt verteld in het bijbehorende artikel over het uitrollen van pdfium.dll en het diagnosticeren van laadfouten. Hier slaagde de loader. De modulehandle is geldig, honderden exports zijn opgelost, en de run eindigt toch voordat je eerste pagina rendert omdat één toegangspunt dat in een nieuwere PDFium-build arriveerde niet in het binaire bestand op schijf zit
Waarom breekt één ontbrekende export de hele bibliotheek?
Omdat een verplichte binding een hard contract is, afgedwongen tijdens een enkele alles-of-niets-bindsequentie. PDFium Component lost zijn volledige exporttabel op binnen LoadLibrary, de ene CheckGetProcAddress-aanroep na de andere. Het eerste nil-resultaat werpt EPdfError en roept daarvoor UnloadLibrary aan, wat opzettelijk is: een gedeeltelijke bind zou anders al opgeloste pointers gericht laten op een module die op het punt staat vrijgegeven te worden, waardoor elke Assigned-bewaker verderop stilzwijgend teniet gedaan wordt
Het gevolg is het faalpatroon dat mensen hierheen brengt. Je upgradet het component, verstuurt dezelfde pdfium.dll die je al twee jaar verstuurt, en de applicatie start niet. De fout noemt een export voor een functie die je nooit aangeroepen hebt. Niets wat je bij de aanroepplek doet, helpt, want de aanroepplek draait nooit; het falen gebeurde tijdens het binden, voordat er ook maar één document geopend was
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;
Verplicht of optioneel: waar de lijn daadwerkelijk ligt
De regel die PDFium Component toepast is bot. Een export is verplicht wanneer de afwezigheid ervan het component onbekwaam maakt om de taak te doen waarvoor het bestaat, en optioneel wanneer de afwezigheid ervan slechts één blad-functie wegneemt. FPDF_InitLibrary, FPDF_LoadDocument, FPDF_RenderPageBitmap, FPDF_ClosePage zijn verplicht, en luid falen op die is correct: een viewer die niet kan renderen is geen gedegradeerde viewer, het is een kapotte
Alles wat vandaag via de tolerante lader bereikt wordt, is een blad. FPDFBookmark_GetColor arriveerde na M109 en levert alleen de optionele /C-kleurarray van een outline-item, dus een DLL van vóór die tijd rapporteert simpelweg geen bladwijzerkleur. De V8-helpers FPDF_GetRecommendedV8Flags en FPDF_GetArrayBufferAllocatorSharedInstance, en de XFA-string-helpers FPDF_BStr_Init, FPDF_BStr_Set en FPDF_BStr_Clear, ontbreken per constructie in elke niet-V8-build, dus die als verplicht behandelen zou de gewone pdfium.dll onlaadbaar maken. En het paar dat dit artikel motiveerde: FPDFAttachment_SetDescription en FPDFAttachment_GetDescription, upstream toegevoegd op 2026-07-13, later dan de builddatum van alle vier de PDFium-binaries die het project verstuurt onder DLLs/Win32 en DLLs/Win64. Dat laatste geval is de algemene vorm van het probleem, geen eenmalig geval: een bindinglaag volgt upstream-headers, die continu bewegen, terwijl de DLL in je installer in discrete sprongen beweegt telkens wanneer iemand hem herbouwt. Er is altijd een venster waarin de Pascal-kant weet van exports die het geïnstalleerde binaire bestand niet heeft, en vooraf beslissen aan welke kant van de verplicht/optioneel-lijn elke nieuwe export valt, is het enige wat dat venster overleefbaar houdt
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');
Wat moet een capability gate doen bij de aanroepplek?
Het moet asymmetrisch zijn, en die asymmetrie is het hele ontwerp. Een lees die niet kan draaien heeft een eerlijk leeg antwoord. Een schrijfactie die niet kan draaien heeft helemaal geen eerlijk antwoord, dus die moet een exception werpen. PDFium Component splitst de attachment-description-eigenschap precies langs die lijn, en de splitsing is wat een ontbrekende export ervan weerhoudt om stille dataverlies te worden. TPdf.GetAttachmentDescription test Assigned(FPDFAttachment_GetDescription) en verlaat met een lege WString. Dat is geen leugen: op een DLL zonder de export kan het component echt niet vertellen of de bijlage een /Desc-item draagt, en een lege beschrijving leest hetzelfde als een bijlage die er nooit één had. De rest van de attachment-API, behandeld in het artikel over werken met PDF-bijlagen in Delphi, blijft ongewijzigd werken
TPdf.SetAttachmentDescription neemt de tegenovergestelde route. Het roept Check aan op dezelfde Assigned-test en werpt EPdfError met de tekst "Attachment descriptions are not supported by the loaded PDFium DLL". Hier stilzwijgend terugkeren zou de slechtst beschikbare optie zijn: de aanroeper zou een beschrijving instellen, geen fout krijgen, het bestand opslaan, en een PDF versturen waarin de beschrijving simpelweg afwezig is. Niemand merkt het totdat een downstream-consument vraagt waar hij gebleven is
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;
De capability testen vóór je de functie aanbiedt
Een exception opvangen is een slechte manier om te ontdekken wat je uitrol kan, dus PDFium Component stelt dezelfde test bloot als een genoemde functie. AttachmentDescriptionFeaturesAvailable roept LoadLibrary aan en retourneert of beide helften van het paar zijn opgelost. Het staat naast V8FeaturesAvailable, XfaBStrHelpersAvailable en XfaFeaturesAvailable, die hetzelfde patroon volgen voor hun eigen optionele groepen. Het benoemen van de probe doet er meer toe dan het lijkt: een boolean genaamd AttachmentDescriptionFeaturesAvailable vertelt de volgende onderhouder dat deze functie voorwaardelijk is aan het geïnstalleerde binaire bestand, wat een kale Assigned-test begraven in een property-setter nooit doet. Het geeft de UI-laag ook iets om aan te binden, zodat het beschrijvingsinvoerveld vooraf uitgeschakeld wordt in plaats van invoer te accepteren en die bij opslaan te weigeren
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;
Waarom moet bindingdekking door een tool bewezen worden?
Omdat de aantallen voorbij het punt zijn waarop een mens ermee vertrouwd kan worden. PDFium Component controleerde 21 publieke PDFium-headers tegen een upstream-basislijn van 2026-07-29 en vond 470 geëxporteerde C-ABI-functies. De binding dekte er daar al 468 van. Niemand vond dat gat van twee door headers te lezen; een script deed het, in een seconde, en zal het weer doen bij de volgende upstream-sprong. tools/audit_pdfium_public_api.py is bewust klein: het regex-matcht FPDF_EXPORT ... FPDF_CALLCONV name( over elke header in de publieke map, regex-matcht elke CheckGetProcAddress('Name') en TryGetProcAddress('Name') in PDFium.pas, en print de twee verzamelverschillen: missing voor exports zonder binding, stale voor bindings waarvan de export upstream niet meer bestaat. Het sluit af met een niet-nul code wanneer een van beide verzamelingen niet leeg is, dus het valt zonder verdere ceremonie in een buildstap. Het huidige resultaat is 470 van 470 gebonden, 0 missing, 0 stale
De stale-richting verdient zijn plek net zo goed als de missing-richting. Een export die upstream verwijderd wordt, laat een CheckGetProcAddress-regel achter die elke toekomstige load hard zal laten falen, en dat soort verval is onzichtbaar totdat de dag komt dat iemand de DLL bijwerkt. Handmatige review vindt de functie waar je aan dacht; het vindt niet de functie waar je niet aan dacht. Merk ook op dat de audit bewust beide laders als dekking telt, wat de juiste keuze is voor API-drift en de reden waarom de verplicht/optioneel-splitsing een gedocumenteerde beslissing moet zijn in plaats van een bijproduct van wie de regel ook toevoegde
Waar optionele binding ophoudt eerlijk te zijn
Twee grenzen zijn het waard om nadrukkelijk te noemen, want het patroon is makkelijk te overtoepassen. De eerste is dat een nil-functiepointer alleen veilig is als letterlijk elk pad dat hem aanraakt eerst Assigned test. In een unit die honderden cdecl-functievariabelen declareert, is één onbewaakte aanroep een access violation op een adres dat niets betekent in een stack trace. Dezelfde discipline die aanroepconventies en levensduren over de C-grens beheerst, geldt hier, en is het onderwerp van het artikel over het hardenen van de PDFium-binding tegen ABI- en geheugenveiligheidsfouten
De tweede grens is scope. Optionele binding is geen algemene licentie om alles tolerant te maken. Was FPDF_RenderPageBitmap optioneel, dan zou het component vrolijk laden en vervolgens op elke pagina falen, waarmee één duidelijke opstartfout omgezet wordt in een spreiding van runtimefouten zonder duidelijke oorzaak. Verplicht is de correcte standaard. Optioneel is de uitzondering waar je naar grijpt wanneer een functie echt een blad is, wanneer de afwezigheid een verdedigbaar gedegradeerd gedrag heeft aan de leeskant, en wanneer de schrijfkant kan weigeren met een melding die de reden benoemt
Het laderontwerp, de capability-probes en het audittool hier beschreven worden geleverd als onderdeel van het PDFium Component voor Delphi en C++Builder; de productpagina somt de meegeleverde PDFium-binaries op en het volledige API-oppervlak dat ze blootstellen