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

PDFium Component sváže svou tabulku exportů Delphi v jednom průchodu, kde CheckGetProcAddress přeruší načtení při chybějícím povinném exportu, zatímco TryGetProcAddress bezpečně degraduje volitelný
Povinné exporty se vážou vše-nebo-nič a přeruší načtení na prvním nil, zatímco volitelné exporty nechají nil ukazatel za kontrolou schopnosti Assigned
function CheckGetProcAddress(const Name: string): Pointer;
begin
  Result := GetProcAddress(PDFiumLibrary, PChar(Name));
  if Result = nil then
  begin
    // Chybějící povinný export znamená, že nasazený pdfium.dll je starší
    // než toto sestavení bindingu. Zahoďte každý ukazatel vyřešený dosud,
    // aby žádný volající nemohl dosáhnout do modulu, který se chystáme uvolnit.
    UnloadLibrary;
    raise EPdfError.Create('Required PDFium export not found: ' + Name);
  end;
end;

function TryGetProcAddress(const Name: string): Pointer;
begin
  // Volitelný export. nil je zde legitimní odpověď; každý volající
  // musí před rozpojením proměnné otestovat Assigned().
  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');
// Popisy příloh byly přidány po revizi přibalené DLL.
// Nechte je volitelné, aby se starší nasazení dál načítala.
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

Chybějící export popisu přílohy PDFium v Delphi vrátí prázdné čtení přes TPdf.GetAttachmentDescription a vyvolá výjimku při zápisu, střeženo přes AttachmentDescriptionFeaturesAvailable
Čtecí strana se degraduje na prázdnou odpověď, zapisovací strana vyhodí výjimku s pojmenovaným důvodem a pojmenovaná sonda dovolí UI funkci předem vypnout
function TPdf.GetAttachmentDescription(Index: Integer): WString;
begin
  CheckActive;
  Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
  Result := '';

  // Strana čtení degraduje: stará DLL neumí nahlásit /Desc a '' je
  // nerozlišitelný od přílohy, která žádný popis nenese.
  if not Assigned(FPDFAttachment_GetDescription) then
    Exit;
  // ... dvouprůchodové vyčíslení bufferu proti FPDFAttachment_GetDescription ...
end;

procedure TPdf.SetAttachmentDescription(Index: Integer; const Value: WString);
begin
  CheckActive;
  Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
  // Strana zápisu odmítá: tiché zahazování hodnoty by vytvořilo soubor,
  // o kterém volající věří, že nese popis, a přitom ho nenese.
  Check(Assigned(FPDFAttachment_SetDescription),
    'Attachment descriptions are not supported by the loaded PDFium DLL');
  // ... FPDFDoc_GetAttachment, pak 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
  // Zeptejte se jednou při sestavení formuláře, místo abyste limit objevili při uložení.
  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í