Műszaki cikk

Opcionális PDFium exportok: képességkapuk Delphiben

A pdfium.dll rendben betöltődik, és egyetlen eljárás mégis hiányzik. A PDFium Component ezt úgy kezeli, hogy a bindingjait két osztályra osztja: a kötelező exportokra, amelyeket a CheckGetProcAddress old fel, és amelyek teljesen megszakítják a betöltést, valamint az opcionális exportokra, amelyeket a TryGetProcAddress old fel, és amelyek egy nil mutatót és egy képességellenőrzést hagynak hátra helyette

Ez nem ugyanaz a probléma, mint egy meg nem található DLL. Ha az alkalmazásunk egy hibás EXE-formátum hibával, hiányzó fájllal vagy architektúra-eltéréssel hal meg, azt a történetet a pdfium.dll telepítéséről és a betöltési hibák diagnosztizálásáról szóló kísérőcikk meséli el. Itt a betöltő sikeres volt. A modulkezelő érvényes, több száz export feloldódott, és a futás mégis véget ér, mielőtt az első oldalunk renderelődne, mert egy belépési pont, amely egy újabb PDFium buildben érkezett, nincs benne a lemezen lévő binárisban

Miért töri el egyetlen hiányzó export az egész könyvtárat?

Mert egy kötelező binding kemény szerződés, és ez egyetlen, mindent-vagy-semmit kötési szekvencia során van érvényesítve. A PDFium Component a teljes exporttábláját a LoadLibrary belsejében oldja fel, egyik CheckGetProcAddress hívást a másik után. Az első nil eredmény EPdfError-t dob, és előtte meghívja az UnloadLibrary-t, ami szándékos: egy részleges kötés különben már feloldott mutatókat hagyna egy olyan modulra mutatva, amely rögtön felszabadításra kerül, csendben legyőzve minden downstream Assigned őrt

A következmény az a hibamód, amely ide hozza az embereket. Frissítjük a komponenst, kiszállítjuk ugyanazt a pdfium.dll-t, amit két éve szállítunk, és az alkalmazás nem indul el. A hiba megnevez egy exportot egy olyan funkcióhoz, amelyet soha nem hívtunk. Semmi, amit a hívási helyen teszünk, nem segít, mert a hívási hely soha nem fut le; a hiba a kötés közben történt, mielőtt bármilyen dokumentum megnyílt volna

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;

Kötelező vagy opcionális: hol húzódik valójában a határvonal

A szabály, amelyet a PDFium Component alkalmaz, egyértelmű. Egy export akkor kötelező, ha hiánya képtelenné teszi a komponenst arra a feladatra, amelyre létezik, és opcionális, ha hiánya csak egyetlen levélfunkciót vesz el. Az FPDF_InitLibrary, az FPDF_LoadDocument, az FPDF_RenderPageBitmap, az FPDF_ClosePage kötelezők, és a hangos bukás ezeknél helyes: egy megjelenítő, amely nem tud renderelni, nem egy lefokozott megjelenítő, hanem egy törött

Minden, amit ma a toleráns betöltőn keresztül érünk el, egy levél. Az FPDFBookmark_GetColor az M109 után érkezett, és csak egy körvonal-bejegyzés opcionális /C színtömbjét szolgáltatja, így egy azt megelőző DLL egyszerűen nem jelent könyvjelzőszínt. A V8 segédfüggvények, az FPDF_GetRecommendedV8Flags és az FPDF_GetArrayBufferAllocatorSharedInstance, valamint az XFA sztring-segédfüggvények, az FPDF_BStr_Init, az FPDF_BStr_Set és az FPDF_BStr_Clear, konstrukciónál fogva hiányoznak minden nem-V8 buildből, így ezek kötelezőnek kezelése betölthetetlenné tenné a sima pdfium.dll-t. És a pár, amely ezt a cikket ihlette: az FPDFAttachment_SetDescription és az FPDFAttachment_GetDescription, amelyeket 2026-07-13-án adtak hozzá felsőágazatilag, később, mint mind a négy PDFium bináris build-dátuma, amelyet a projekt a DLLs/Win32 és DLLs/Win64 alatt szállít. Ez az utolsó eset a probléma általános alakja, nem egyszeri: egy bindingréteg a felsőágazati fejléceket követi, amelyek folyamatosan mozognak, míg a telepítőnkben lévő DLL diszkrét ugrásokban mozog, valahányszor valaki újraépíti. Mindig van egy ablak, amelyben a Pascal oldal olyan exportokról tud, amelyekkel a telepített bináris nem rendelkezik, és az, hogy előre eldöntjük, minden új export a kötelező/opcionális vonal melyik oldalára esik, az egyetlen dolog, amely túlélhetővé teszi ezt az ablakot

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

Mit kell tennie egy képességkapunak a hívási helyen?

Aszimmetrikusnak kell lennie, és ez az aszimmetria a teljes tervezés. Egy olvasásnak, amely nem tud lefutni, van becsületes üres válasza. Egy írásnak, amely nem tud lefutni, egyáltalán nincs becsületes válasza, ezért dobnia kell. A PDFium Component pontosan e mentén a vonal mentén osztja szét a melléklet-leírás tulajdonságot, és ez a szétválasztás az, ami megakadályozza, hogy egy hiányzó export csendes adatvesztéssé váljon. A TPdf.GetAttachmentDescription ellenőrzi az Assigned(FPDFAttachment_GetDescription)-t, és egy üres WString-gel lép ki. Ez nem hazugság: egy export nélküli DLL-en a komponens valóban nem tudja megmondani, hogy a melléklet hordoz-e /Desc bejegyzést, és egy üres leírás ugyanúgy olvasható, mint egy olyan melléklet, amelynek soha nem volt egy. A melléklet-API többi része, amelyet a PDF mellékletekkel való munkáról szóló cikk Delphiben tárgyal, érintetlenül tovább működik

A TPdf.SetAttachmentDescription az ellenkező utat választja. Meghívja a Check-et ugyanazon Assigned teszten, és EPdfError-t dob "Attachment descriptions are not supported by the loaded PDFium DLL" szöveggel. A csendes visszatérés itt lenne a legrosszabb elérhető opció: a hívó beállítana egy leírást, nem kapna hibát, elmentené a fájlt, és kiszállítana egy PDF-et, ahol a leírás egyszerűen hiányzik. Senki nem veszi észre, amíg egy downstream fogyasztó meg nem kérdezi, hova lett

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;

A képesség vizsgálata, mielőtt felkínálnánk a funkciót

Egy kivétel elkapása gyenge módja annak, hogy kiderítsük, mire képes a telepítésünk, így a PDFium Component ugyanazt a tesztet egy elnevezett függvényként is közzéteszi. Az AttachmentDescriptionFeaturesAvailable meghívja a LoadLibrary-t, és visszaadja, hogy a pár mindkét fele feloldódott-e. A V8FeaturesAvailable, az XfaBStrHelpersAvailable és az XfaFeaturesAvailable mellett foglal helyet, amelyek ugyanazt a mintát követik a saját opcionális csoportjaikhoz. A vizsgálat elnevezése jobban számít, mint amennyire látszik: egy AttachmentDescriptionFeaturesAvailable nevű boolean elmondja a következő karbantartónak, hogy ez a funkció feltételes a telepített bináristól, amit egy tulajdonság-setterben eltemetett puszta Assigned teszt soha nem tesz meg. A felhasználói felület rétegének is ad valamit, amihez kötődhet, így a leírás szerkesztőmezője előre le van tiltva, ahelyett hogy bemenetet fogadna, és mentéskor elutasítaná

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;

Miért kell egy eszköznek bizonyítania a bindinglefedettséget?

Mert a számok túl vannak azon a ponton, ahol egy emberben megbízhatnánk velük. A PDFium Component 21 nyilvános PDFium fejlécet auditált egy 2026-07-29-es felsőágazati alapvonal ellen, és 470 exportált C ABI függvényt talált. A binding már 468-at lefedett belőlük. Senki nem azonosította ezt a kettes hiányt fejlécek olvasásával; egy szkript tette, egy másodperc alatt, és újra meg fogja tenni a következő felsőágazati emelésnél. A tools/audit_pdfium_public_api.py szándékosan apró: reguláris kifejezéssel illeszti az FPDF_EXPORT ... FPDF_CALLCONV name( mintát minden fejlécen a nyilvános könyvtárban, reguláris kifejezéssel illeszti minden CheckGetProcAddress('Name') és TryGetProcAddress('Name') előfordulást a PDFium.pas-ban, és kiírja a két halmazkülönbséget: missing-et a binding nélküli exportokhoz, stale-t azokhoz a bindingekhez, amelyek exportja már nem létezik felsőágazatilag. Nem nulla kilépőkóddal áll le, amikor bármelyik halmaz nem üres, így ceremónia nélkül beilleszthető egy buildlépésbe. Az aktuális eredmény 470-ből 470 kötve, 0 hiányzó, 0 elavult

Az elavult irány ugyanúgy megéri a fáradságot, mint a hiányzó. Egy export, amelyet a felsőágazat eltávolít, egy CheckGetProcAddress sort hagy hátra, amely minden jövőbeli betöltést keményen elbuktat, és ez a fajta rothadás láthatatlan addig, amíg valaki frissíti a DLL-t. A kézi átvizsgálás megtalálja azt a függvényt, amelyre gondoltunk; nem találja meg azt, amelyre nem gondoltunk. Vegyük észre azt is, hogy az audit szándékosan mindkét betöltőt lefedettségként számolja, ami a helyes döntés az API-eltolódásra nézve, és ez az oka annak, hogy a kötelező/opcionális szétválasztásnak dokumentált döntésnek kell lennie, nem pedig annak a mellékterméke, hogy ki adta hozzá a sort

Ahol az opcionális binding megszűnik becsületes lenni

Két határt érdemes egyértelműen kimondani, mert a mintát könnyű túlalkalmazni. Az első az, hogy egy nil függvénymutató csak akkor biztonságos, ha szó szerint minden útvonal, amely érinti, előbb teszteli az Assigned-et. Egy egységben, amely több száz cdecl függvényváltozót deklarál, egyetlen őrizetlen hívás egy olyan címen okoz hozzáférési kivételt, amely semmit nem jelent egy veremkiíratásban. Ugyanaz a fegyelem, amely a hívási konvenciókat és az élettartamokat szabályozza a C-határon át, itt is érvényes, és ez a témája a PDFium binding megerősítéséről ABI- és memóriabiztonsági hibák ellen szóló cikknek

A második határ a hatókör. Az opcionális binding nem általános engedély arra, hogy mindent toleránssá tegyünk. Ha az FPDF_RenderPageBitmap opcionális lenne, a komponens boldogan betöltődne, majd minden oldalon meghiúsulna, egyetlen világos indítási hibát futásidejűek szórványává alakítva nyilvánvaló ok nélkül. A kötelező a helyes alapértelmezés. Az opcionális az a kivétel, amelyhez akkor nyúlunk, amikor egy funkció valóban egy levél, amikor a hiánynak van védhető, lefokozott viselkedése az olvasási oldalon, és amikor az írási oldal el tud utasítani egy olyan üzenettel, amely megnevezi az okot

Az itt leírt betöltő-tervezés, a képességvizsgálatok és az auditeszköz a PDFium Component részeként érkeznek Delphihez és C++Builderhez; a termékoldal felsorolja a mellékelt PDFium binárisokat és a teljes API-felületet, amelyet közzétesznek