Technický článek

Volitelné exporty PDFium: brány schopností v Delphi

Váš pdfium.dll se v pořádku načte a jedna procedura přesto chybí. PDFium Component to řeší rozdělením svých bindingů do dvou tříd: povinné exporty vyhodnocované přes CheckGetProcAddress, které rovnou přeruší načtení, a volitelné exporty vyhodnocované přes TryGetProcAddress, které místo toho zanechají nil ukazatel a kontrolu schopnosti

Toto není stejný problém jako DLL, kterou nelze najít. Pokud vaše aplikace zemře s chybou špatného formátu EXE, chybějícím souborem nebo neshodou architektury, tento příběh vypráví doprovodný článek o nasazení pdfium.dll a diagnostice selhání načtení. Zde loader uspěl. Handle modulu je platný, stovky exportů se vyhodnotily, a přesto běh skončí ještě předtím, než se vykreslí vaše první stránka, protože jeden vstupní bod, který dorazil v novější sestavě PDFium, není v binárce na disku

Proč jeden chybějící export rozbije celou knihovnu?

Protože povinný binding je tvrdá smlouva, vynucovaná během jedné sekvence vázání typu vše-nebo-nic. PDFium Component vyhodnocuje celou svou exportní tabulku uvnitř LoadLibrary, jedno volání CheckGetProcAddress za druhým. První nil výsledek vyvolá EPdfError a předtím zavolá UnloadLibrary, což je záměrné: částečné vázání by jinak ponechalo už vyhodnocené ukazatele mířící do modulu, který se právě chystá uvolnit, a tiše by tak porazilo každou hlídku Assigned po proudu

Důsledkem je způsob selhání, který sem lidi přivádí. Aktualizujete komponentu, dodáte stejný pdfium.dll, jaký dodáváte dva roky, a aplikace se nespustí. Chyba pojmenuje export pro funkci, kterou jste nikdy nevolali. Nic, co uděláte na místě volání, nepomůže, protože místo volání se nikdy nespustí; selhání se stalo během vázání, ještě předtím, než byl otevřen jediný 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é, nebo volitelné: kde skutečně vede hranice

Pravidlo, které PDFium Component aplikuje, je přímočaré. Export je povinný, když jeho nepřítomnost znemožní komponentě dělat práci, kvůli které existuje, a volitelný, když jeho nepřítomnost odstraní jen jednu okrajovou funkci. FPDF_InitLibrary, FPDF_LoadDocument, FPDF_RenderPageBitmap, FPDF_ClosePage jsou povinné, a hlasitě selhat u nich je správné: prohlížeč, který neumí vykreslovat, není degradovaný prohlížeč, je rozbitý

Vše, k čemu se dnes přistupuje přes tolerantní loader, je okrajová funkce. FPDFBookmark_GetColor dorazila po M109 a dodává pouze volitelné pole barvy /C položky osnovy, takže DLL, která ji předchází, prostě hlásí žádnou barvu záložky. Pomocníci V8 FPDF_GetRecommendedV8Flags a FPDF_GetArrayBufferAllocatorSharedInstance, a pomocníci řetězců XFA FPDF_BStr_Init, FPDF_BStr_Set a FPDF_BStr_Clear, v jakékoli sestavě bez V8 konstrukčně chybí, takže považovat je za povinné by udělalo obyčejný pdfium.dll nenačitatelným. A dvojice, která tento článek podnítila: FPDFAttachment_SetDescription a FPDFAttachment_GetDescription, přidané upstream 2026-07-13, později, než je datum sestavení všech čtyř binárek PDFium, které projekt dodává pod DLLs/Win32 a DLLs/Win64. Tento poslední případ je obecný tvar problému, ne ojedinělost: vrstva bindingu sleduje upstream hlavičky, které se pohybují průběžně, zatímco DLL ve vašem instalátoru se pohybuje v diskrétních skocích, kdykoli ji někdo znovu sestaví. Vždy existuje okno, v němž strana Pascalu ví o exportech, které nasazená binárka nemá, a rozhodnout se předem, na kterou stranu hranice povinné/volitelné každý nový export spadá, je jediné, co dělá toto okno přežitelný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');

Co by měla brána schopnosti dělat na místě volání?

Měla by být asymetrická, a tato asymetrie je celý návrh. Čtení, které nemůže proběhnout, má poctivou prázdnou odpověď. Zápis, který nemůže proběhnout, nemá žádnou poctivou odpověď vůbec, takže musí vyvolat výjimku. PDFium Component rozděluje vlastnost popisu přílohy přesně podle této hranice, a toto rozdělení je to, co brání chybějícímu exportu proměnit se v tichou ztrátu dat. TPdf.GetAttachmentDescription testuje Assigned(FPDFAttachment_GetDescription) a odchází s prázdným WString. Není to lež: na DLL bez tohoto exportu komponenta opravdu neumí rozeznat, zda příloha nese položku /Desc, a prázdný popis se čte stejně jako příloha, která žádný nikdy neměla. Zbytek API pro přílohy, popsaný v článku o práci s přílohami PDF v Delphi, funguje nedotčen dál

TPdf.SetAttachmentDescription jde opačnou cestou. Volá Check na stejný test Assigned a vyvolává EPdfError s textem „Attachment descriptions are not supported by the loaded PDFium DLL". Tiché vrácení zde by byla nejhorší dostupná možnost: volající by nastavil popis, nedostal žádnou chybu, uložil soubor a dodal PDF, kde popis prostě chybí. Nikdo si toho nevšimne, dokud se konzument po proudu nezeptá, kam se poděl

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;

Sondování schopnosti ještě předtím, než funkci nabídnete

Zachycení výjimky je slabý způsob, jak zjistit, co vaše nasazení umí, takže PDFium Component vystavuje stejný test jako pojmenovanou funkci. AttachmentDescriptionFeaturesAvailable zavolá LoadLibrary a vrátí, zda se vyhodnotily obě poloviny dvojice. Sedí vedle V8FeaturesAvailable, XfaBStrHelpersAvailable a XfaFeaturesAvailable, které následují identický vzor pro vlastní volitelné skupiny. Pojmenování sondy má větší význam, než se zdá: booleovská hodnota nazvaná AttachmentDescriptionFeaturesAvailable říká dalšímu správci, že tato funkce je podmíněná nasazenou binárkou, což holý test Assigned zakopaný v setteru vlastnosti nikdy neřekne. Dává to také vrstvě UI něco, na co se navázat, takže editační pole popisu je zakázané rovnou, místo aby přijímalo vstup a odmítalo jej při 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;

Proč musí být pokrytí bindingu dokázáno nástrojem?

Protože čísla jsou za hranicí toho, s čím lze důvěřovat člověku. PDFium Component prošla auditem 21 veřejných hlaviček PDFium proti upstream základní linii z 2026-07-29 a našla 470 exportovaných funkcí C ABI. Binding už jich pokrýval 468. Nikdo tuto mezeru dvou nenašel čtením hlaviček; našel ji skript, za sekundu, a udělá to znovu při dalším upstream posunu. tools/audit_pdfium_public_api.py je záměrně malý: regexem hledá FPDF_EXPORT ... FPDF_CALLCONV name( napříč každou hlavičkou ve veřejném adresáři, regexem hledá každé CheckGetProcAddress('Name') a TryGetProcAddress('Name') v PDFium.pas a vypisuje rozdíly obou množin: missing pro exporty bez bindingu, stale pro bindingy, jejichž export už upstream neexistuje. Skončí s nenulovým kódem, když je kterákoli z množin neprázdná, takže zapadne do kroku buildu bez dalších okolků. Aktuální výsledek je 470 z 470 navázaných, missing 0, stale 0

Směr stale si zaslouží pozornost stejně jako missing. Export, který upstream odstraní, zanechá za sebou řádek CheckGetProcAddress, který natvrdo selže při každém budoucím načtení, a tento druh hniloby je neviditelný až do dne, kdy někdo DLL aktualizuje. Ruční revize najde funkci, na kterou jste mysleli; nenajde tu, na kterou jste nemysleli. Všimněte si také, že audit záměrně počítá pokrytí oběma loadery, což je správné rozhodnutí pro drift API, a je to důvod, proč rozdělení povinné/volitelné musí být zdokumentované rozhodnutí, ne vedlejší produkt toho, kdo ten řádek zrovna přidal

Kde volitelný binding přestává být poctivý

Dvě hranice stojí za otevřené vyjádření, protože tento vzor se snadno přeaplikovává. První je, že nil ukazatel na funkci je bezpečný jen tehdy, pokud doslova každá cesta, která se ho dotkne, nejprve testuje Assigned. V jednotce, která deklaruje stovky proměnných funkcí cdecl, je jediné nehlídané volání přístupová výjimka na adrese, která ve stack trace nic neznamená. Stejná disciplína, která řídí volací konvence a životnosti přes hranici C, platí i zde a je předmětem článku o posílení bindingu PDFium proti chybám ABI a bezpečnosti paměti

Druhá hranice je rozsah. Volitelný binding není obecná licence dělat vše tolerantní. Kdyby byl FPDF_RenderPageBitmap volitelný, komponenta by se ochotně načetla a pak selhávala na každé stránce, čímž by proměnila jednu jasnou chybu při startu na rozptýlení běhových chyb bez zjevné příčiny. Povinné je správná výchozí volba. Volitelné je výjimka, po které sáhnete, když je funkce skutečně okrajová, když nepřítomnost má obhajitelné degradované chování na straně čtení, a když strana zápisu může odmítnout se zprávou, která pojmenuje důvod

Návrh loaderu, sondy schopností a nástroj auditu popsané zde jsou součástí komponenty PDFium pro Delphi a C++Builder; stránka produktu uvádí přiložené binárky PDFium a celý povrch API, který vystavují