Odborný článok

Voliteľné PDFium exporty: brány schopností v Delphi

Vaša pdfium.dll sa v poriadku načíta a jedna procedúra napriek tomu chýba. PDFium Component to rieši rozdelením svojich bindingov do dvoch tried: povinné exporty riešené cez CheckGetProcAddress, ktoré rovno prerušia načítanie, a voliteľné exporty riešené cez TryGetProcAddress, ktoré namiesto toho zanechajú nil ukazovateľ a kontrolu schopnosti

Toto nie je ten istý problém ako DLL, ktorá sa nedá nájsť. Ak vaša aplikácia zomrie s chybou zlého EXE formátu, chýbajúcim súborom, alebo nesúladom architektúry, tento príbeh je vyrozprávaný v sprievodnom článku o nasadzovaní pdfium.dll a diagnostike zlyhaní načítania. Tu sa loader úspešne dokončil. Handle modulu je platný, stovky exportov sa vyriešili, a beh napriek tomu skončí skôr, než sa vykreslí vaša prvá strana, pretože jeden vstupný bod, ktorý pribudol v novšom PDFium builde, nie je v binárke na disku

Prečo jeden chýbajúci export rozbije celú knižnicu?

Pretože povinný binding je tvrdý kontrakt, a je vynucovaný počas jedinej sekvencie bindovania typu všetko alebo nič. PDFium Component rieši celú svoju exportnú tabuľku vnútri LoadLibrary, jedno volanie CheckGetProcAddress za druhým. Prvý nil výsledok vyvolá EPdfError a pred tým zavolá UnloadLibrary, čo je zámerné: čiastočný bind by inak nechal už vyriešené ukazovatele mieriace do modulu, ktorý sa práve chystá uvoľniť, ticho porážajúc každú Assigned poistku po prúde

Dôsledkom je režim zlyhania, ktorý sem ľudí privádza. Aktualizujete komponentu, dodáte tú istú pdfium.dll, ktorú dodávate dva roky, a aplikácia sa nespustí. Chyba pomenuje export pre funkciu, ktorú ste nikdy nezavolali. Nič, čo urobíte na mieste volania, nepomôže, pretože miesto volania nikdy nebeží; zlyhanie sa stalo počas bindovania, skôr, než bol otvorený akýkoľvek dokument

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;

Povinné alebo voliteľné: kde skutočne sedí hranica

Pravidlo, ktoré PDFium Component aplikuje, je priamočiare. Export je povinný, keď jeho absencia znemožní komponente vykonávať prácu, na ktorú existuje, a voliteľný, keď jeho absencia odstráni iba jednu okrajovú funkciu. FPDF_InitLibrary, FPDF_LoadDocument, FPDF_RenderPageBitmap, FPDF_ClosePage sú povinné, a nahlas zlyhať pri nich je správne: viewer, ktorý nedokáže renderovať, nie je degradovaný viewer, je pokazený

Všetko, čoho dnes dosahuje tolerantný loader, je okraj. FPDFBookmark_GetColor prišiel po M109 a dodáva iba voliteľné pole farby /C položky osnovy, takže DLL, ktorá ho predchádza, jednoducho nehlási žiadnu farbu záložky. Pomocníci V8 FPDF_GetRecommendedV8Flags a FPDF_GetArrayBufferAllocatorSharedInstance, a XFA reťazcové pomocníky FPDF_BStr_Init, FPDF_BStr_Set a FPDF_BStr_Clear, chýbajú v akomkoľvek ne-V8 builde konštrukciou, takže ich považovať za povinné by urobilo obyčajnú pdfium.dll nenačítateľnou. A dvojica, ktorá motivovala tento článok: FPDFAttachment_SetDescription a FPDFAttachment_GetDescription, pridané upstream 2026-07-13, neskôr než dátum buildu všetkých štyroch PDFium binárok, ktoré projekt dodáva pod DLLs/Win32 a DLLs/Win64. Tento posledný prípad je všeobecným tvarom problému, nie ojedinelou udalosťou: vrstva bindingu sleduje upstream hlavičky, ktoré sa nepretržite hýbu, zatiaľ čo DLL vo vašom inštalátore sa hýbe v diskrétnych skokoch vždy, keď ju niekto znovu zostaví. Vždy existuje okno, v ktorom Pascal strana vie o exportoch, ktoré nasadená binárka nemá, a rozhodnutie vopred, na ktorú stranu hranice povinné/voliteľné každý nový export padá, je jediné, čo robí toto okno prežiteľným

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

Čo by mala brána schopnosti robiť na mieste volania?

Mala by byť asymetrická, a táto asymetria je celý dizajn. Čítanie, ktoré nemôže bežať, má úprimnú prázdnu odpoveď. Zápis, ktorý nemôže bežať, nemá žiadnu úprimnú odpoveď vôbec, takže musí vyvolať výnimku. PDFium Component rozdeľuje vlastnosť popisu prílohy presne pozdĺž tejto línie, a toto rozdelenie je to, čo zastavuje chýbajúci export pred premenením sa na tichú stratu dát. TPdf.GetAttachmentDescription testuje Assigned(FPDFAttachment_GetDescription) a skončí s prázdnym WString. To nie je klamstvo: na DLL bez tohto exportu komponenta skutočne nedokáže povedať, či príloha nesie položku /Desc, a prázdny popis sa číta rovnako ako príloha, ktorá ho nikdy nemala. Zvyšok API príloh, pokrytý v článku o práci s PDF prílohami v Delphi, pokračuje bez zmeny

TPdf.SetAttachmentDescription ide opačnou cestou. Zavolá Check na tom istom teste Assigned a vyvolá EPdfError s textom „Attachment descriptions are not supported by the loaded PDFium DLL“. Tiché vrátenie by tu bolo najhoršou dostupnou možnosťou: volajúci by nastavil popis, nedostal by žiadnu chybu, uložil by súbor, a odoslal by PDF, kde popis jednoducho chýba. Nikto si to nevšimne, kým sa spotrebiteľ po prúde nespýta, kam sa podel

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;

Skúmanie schopnosti skôr, než funkciu ponúknete

Zachytenie výnimky je slabý spôsob, ako zistiť, čo vaše nasadenie dokáže, takže PDFium Component sprístupňuje ten istý test ako pomenovanú funkciu. AttachmentDescriptionFeaturesAvailable zavolá LoadLibrary a vráti, či sa vyriešili obe polovice dvojice. Sedí popri V8FeaturesAvailable, XfaBStrHelpersAvailable a XfaFeaturesAvailable, ktoré nasledujú identický vzorec pre svoje vlastné voliteľné skupiny. Pomenovanie skúšky záleží viac, než sa zdá: boolean nazvaný AttachmentDescriptionFeaturesAvailable hovorí ďalšiemu udržiavateľovi, že táto funkcia je podmienená nasadenou binárkou, čo holý test Assigned zakopaný v settery vlastnosti nikdy nerobí. Tiež dáva UI vrstve niečo, na čo sa naviazať, takže editovacie pole popisu je vypnuté vopred namiesto prijímania vstupu a jeho odmietnutia pri uložení

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;

Prečo musí byť pokrytie bindingu dokázané nástrojom?

Pretože čísla sú za bodom, kedy sa im dá dôverovať s človekom. PDFium Component auditoval 21 verejných PDFium hlavičiek voči upstream základu z 2026-07-29 a našiel 470 exportovaných funkcií C ABI. Binding už pokrýval 468 z nich. Nikto tú medzeru dvoch nenašiel čítaním hlavičiek; našiel ju skript, za sekundu, a urobí to znova pri ďalšom upstream posune. tools/audit_pdfium_public_api.py je zámerne malý: regex-om zhoduje FPDF_EXPORT ... FPDF_CALLCONV name( naprieč každou hlavičkou vo verejnom adresári, regex-om zhoduje každé CheckGetProcAddress('Name') a TryGetProcAddress('Name') v PDFium.pas, a vytlačí dva rozdiely množín: missing pre exporty bez bindingu, stale pre bindingy, ktorých export už upstream neexistuje. Skončí s nenulovým kódom, keď je ktorákoľvek množina neprázdna, takže zapadá do build kroku bez ďalšej ceremónie. Aktuálny výsledok je 470 z 470 naviazaných, chýbajúcich 0, zastaraných 0

Smer zastaraných si zarába svoje miesto rovnako ako smer chýbajúcich. Export, ktorý upstream odstráni, zanechá riadok CheckGetProcAddress, ktorý natvrdo zlyhá pri každom budúcom načítaní, a tento druh hnitia je neviditeľný, kým niekto niekedy neaktualizuje DLL. Manuálna revízia nájde funkciu, na ktorú ste mysleli; nenájde tú, na ktorú ste nemysleli. Všimnite si tiež, že audit zámerne počíta oba loadery ako pokrytie, čo je správne rozhodnutie pre drift API a dôvod, prečo rozdelenie povinné/voliteľné musí byť zdokumentované rozhodnutie, nie vedľajší produkt toho, kto pridal riadok

Kde voliteľné bindovanie prestáva byť úprimné

Dve hranice stoja za jasné vyslovenie, pretože vzorec sa dá ľahko nadmerne aplikovať. Prvá je, že nil ukazovateľ funkcie je bezpečný iba vtedy, keď doslova každá cesta, ktorá sa ho dotýka, najprv testuje Assigned. V jednotke, ktorá deklaruje stovky premenných funkcií cdecl, jedno nestrážené volanie je porušenie prístupu na adrese, ktorá v stack trace nič neznamená. Rovnaká disciplína, ktorá riadi volacie konvencie a životnosti naprieč C hranicou, sa uplatňuje aj tu, a je predmetom článku o spevňovaní PDFium bindingu voči chybám ABI a bezpečnosti pamäte

Druhá hranica je rozsah. Voliteľné bindovanie nie je všeobecná licencia urobiť všetko tolerantným. Keby bol FPDF_RenderPageBitmap voliteľný, komponenta by sa ochotne načítala a potom by zlyhala na každej strane, premeniac jednu jasnú chybu pri štarte na rozptyl runtime chýb bez zjavnej príčiny. Povinné je správne predvolené nastavenie. Voliteľné je výnimka, po ktorej siahnete, keď je funkcia skutočne okrajová, keď má absencia obhájiteľné degradované správanie na strane čítania, a keď strana zápisu môže odmietnuť so správou, ktorá pomenuje dôvod

Dizajn loadera, skúšky schopností a nástroj auditu opísané tu sú súčasťou PDFium Component pre Delphi a C++Builder; stránka produktu uvádza zabalené PDFium binárky a úplný povrch API, ktorý sprístupňujú