Teknisk artikel

Valgfri PDFium-exports: kapabilitetsgates i Delphi

Ens pdfium.dll indlæser fint, og én procedure mangler stadig. PDFium Component håndterer dette ved at dele sine bindinger i to klasser: påkrævede exports løst gennem CheckGetProcAddress, som afbryder indlæsningen helt, og valgfrie exports løst gennem TryGetProcAddress, som efterlader en nil-pointer og et kapabilitetstjek i stedet

Dette er ikke samme problem som en DLL, der ikke kan findes. Dør din applikation med en bad EXE format-fejl, en manglende fil eller et arkitektur-mismatch, fortælles den historie i søsterartiklen om deployment af pdfium.dll og diagnosticering af indlæsningsfejl. Her lykkedes loaderen. Modulhandtet er gyldigt, hundredvis af exports er løst, og kørslen slutter alligevel før første side gengives, fordi ét entry point, der ankom i en nyere PDFium-build, ikke er i binæren på disken

Hvorfor ødelægger én manglende export hele biblioteket?

Fordi en påkrævet binding er en hård kontrakt, og den håndhæves under én alt-eller-intet bindesekvens. PDFium Component løser hele sin export-tabel inde i LoadLibrary, ét CheckGetProcAddress-kald efter et andet. Det første nil-resultat rejser EPdfError og kalder UnloadLibrary før det, hvilket er bevidst: en delvis binding ville ellers efterlade allerede løste pointere sigtet ind i et modul, der er ved at blive frigivet, og stiltiende besejre hver Assigned-vagt nedstrøms

Konsekvensen er den fejltilstand, der bringer folk hertil. Man opgraderer komponenten, sender den samme pdfium.dll, man har sendt i to år, og applikationen vil ikke starte. Fejlen navngiver en export til en funktion, man aldrig har kaldt. Intet man gør ved kaldestedet hjælper, fordi kaldestedet aldrig kører; fejlen skete under binding, før noget dokument blev åbnet

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;

Påkrævet eller valgfri: hvor grænsen egentlig ligger

Reglen, PDFium Component anvender, er ligeud. En export er påkrævet, når dens fravær gør komponenten ude af stand til at udføre det job, den findes til, og valgfri, når fraværet kun fjerner én blad-funktion. FPDF_InitLibrary, FPDF_LoadDocument, FPDF_RenderPageBitmap, FPDF_ClosePage er påkrævede, og at fejle højlydt på dem er korrekt: en viewer, der ikke kan gengive, er ikke en nedgraderet viewer, den er ødelagt

Alt, der nås gennem den tolerante loader i dag, er et blad. FPDFBookmark_GetColor ankom efter M109 og leverer kun det valgfrie /C-farvearray for en oversigtspost, så en DLL, der går forud for den, rapporterer simpelthen ingen bogmærkefarve. V8-hjælperne FPDF_GetRecommendedV8Flags og FPDF_GetArrayBufferAllocatorSharedInstance, samt XFA-strenghjælperne FPDF_BStr_Init, FPDF_BStr_Set og FPDF_BStr_Clear, er fraværende fra enhver ikke-V8-build ved konstruktion, så at behandle dem som påkrævede ville gøre den almindelige pdfium.dll ikke-indlæsbar. Og parret, der udløste denne artikel: FPDFAttachment_SetDescription og FPDFAttachment_GetDescription, tilføjet upstream den 2026-07-13, senere end build-datoen for alle fire PDFium-binærer, projektet leverer under DLLs/Win32 og DLLs/Win64. Det sidste tilfælde er den generelle form på problemet, ikke et engangstilfælde: et bindingslag følger upstream-headers, som bevæger sig konstant, mens DLL'en i din installer bevæger sig i diskrete spring, hver gang nogen genbygger den. Der er altid et vindue, hvor Pascal-siden kender exports, den deployerede binær ikke har, og at beslutte på forhånd, hvilken side af den påkrævet/valgfri-grænse hver ny export falder på, er det eneste, der holder det vindue overlevelsesdygtigt

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');

Hvad bør en kapabilitetsgate gøre ved kaldestedet?

Den bør være asymmetrisk, og den asymmetri er hele designet. En læsning, der ikke kan køre, har et ærligt tomt svar. En skrivning, der ikke kan køre, har intet ærligt svar overhovedet, så den skal rejse. PDFium Component deler egenskaben for vedhæftningsbeskrivelse nøjagtig langs den linje, og delingen er det, der stopper en manglende export fra at blive til stiltiende datatab. TPdf.GetAttachmentDescription tester Assigned(FPDFAttachment_GetDescription) og afslutter med en tom WString. Det er ikke en løgn: på en DLL uden exporten kan komponenten reelt ikke afgøre, om vedhæftningen bærer en /Desc-post, og en tom beskrivelse læses samme vej som en vedhæftning, der aldrig havde én. Resten af vedhæftnings-API'en, dækket i artiklen om arbejde med PDF-vedhæftninger i Delphi, bliver ved med at virke urørt

TPdf.SetAttachmentDescription tager den modsatte rute. Den kalder Check på samme Assigned-test og rejser EPdfError med teksten "Attachment descriptions are not supported by the loaded PDFium DLL". At returnere stille her ville være den værst tilgængelige mulighed: kalderen ville sætte en beskrivelse, få ingen fejl, gemme filen og sende en PDF, hvor beskrivelsen simpelthen mangler. Ingen bemærker det, før en nedstrøms forbruger spørger, hvor den blev af

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;

At sondere kapaciteten, før man tilbyder funktionen

At fange en undtagelse er en dårlig måde at opdage, hvad ens deployment kan gøre, så PDFium Component eksponerer samme test som en navngivet funktion. AttachmentDescriptionFeaturesAvailable kalder LoadLibrary og returnerer, om begge halvdele af parret blev løst. Den sidder ved siden af V8FeaturesAvailable, XfaBStrHelpersAvailable og XfaFeaturesAvailable, som følger det identiske mønster for deres egne valgfrie grupper. At navngive sonden betyder mere, end det ser ud til: en boolean kaldet AttachmentDescriptionFeaturesAvailable fortæller den næste vedligeholder, at denne funktion er betinget af den deployerede binær, hvilket en bar Assigned-test begravet i en property-setter aldrig gør. Det giver også UI-laget noget at binde til, så beskrivelsesfeltet deaktiveres på forhånd frem for at acceptere input og afvise det ved gemning

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;

Hvorfor skal bindingsdækning bevises af et værktøj?

Fordi tallene er forbi det punkt, hvor et menneske kan betros dem. PDFium Component reviderede 21 offentlige PDFium-headers mod en 2026-07-29 upstream-baseline og fandt 470 eksporterede C ABI-funktioner. Bindingen dækkede allerede 468 af dem. Ingen fandt det gab på to ved at læse headers; et script gjorde det på et sekund, og det vil gøre det igen ved næste upstream-spring. tools/audit_pdfium_public_api.py er bevidst lille: den regex-matcher FPDF_EXPORT ... FPDF_CALLCONV name( på tværs af hver header i den offentlige mappe, regex-matcher hver CheckGetProcAddress('Name') og TryGetProcAddress('Name') i PDFium.pas, og udskriver de to mængdedifferenser: missing for exports uden binding, stale for bindinger, hvis export ikke længere findes upstream. Den afslutter med ikke-nul, når nogen af mængderne ikke er tom, så den falder ind i et build-trin uden yderligere ceremoni. Det aktuelle resultat er 470 af 470 bundet, missing 0, stale 0

Den forældede retning fortjener sin plads lige så meget som den manglende. En export, upstream fjerner, efterlader en CheckGetProcAddress-linje, der vil hård-fejle enhver fremtidig indlæsning, og den slags forfald er usynligt, indtil dagen nogen opdaterer DLL'en. Manuel gennemgang finder den funktion, man tænkte på; den finder ikke den, man ikke tænkte på. Bemærk også, at auditten bevidst tæller begge loadere som dækning, hvilket er det rigtige valg for API-drift og grunden til, at den påkrævede/valgfrie deling skal være en dokumenteret beslutning frem for et biprodukt af, hvem der tilføjede linjen

Hvor valgfri binding holder op med at være ærlig

To grænser er værd at sige ligeud, fordi mønsteret er let at overanvende. Den første er, at en nil-funktionspointer kun er sikker, hvis bogstaveligt hver sti, der rører den, tester Assigned først. I en enhed, der erklærer hundredvis af cdecl-funktionsvariabler, er ét ubevogtet kald en access violation ved en adresse, der intet betyder i en stack trace. Samme disciplin, der styrer kaldekonventioner og levetider på tværs af C-grænsen, gælder her, og det er emnet for artiklen om hærdning af PDFium-bindingen mod ABI- og hukommelsessikkerhedsfejl

Den anden grænse er omfang. Valgfri binding er ikke en generel licens til at gøre alt tolerant. Var FPDF_RenderPageBitmap valgfri, ville komponenten indlæse gladeligt og så fejle på hver eneste side, og konvertere én klar opstartsfejl til en spredning af runtime-fejl uden nogen oplagt årsag. Påkrævet er standarden, der er korrekt. Valgfri er undtagelsen, man griber til, når en funktion genuint er et blad, når fraværet har en forsvarlig nedgraderet adfærd på læsesiden, og når skrivesiden kan afvise med en besked, der navngiver årsagen

Loader-designet, kapabilitetssonderne og audit-værktøjet beskrevet her leveres som en del af PDFium Component til Delphi og C++Builder; produktsiden opfører de medfølgende PDFium-binærer og hele API-fladen, de eksponerer