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

A PDFium Component egy futásban köti Delphi exporttábláját: a CheckGetProcAddress megszakítja a betöltést hiányzó kötelező exportnál, míg a TryGetProcAddress egy opcionálisat biztonságosan lefokoz
A kötelező exportok minden-vagy-semmi módon kötnek, és az első nilnél megszakítják a betöltést, míg az opcionális exportok nil pointert hagynak egy Assigned képességellenőrzés mögött
function CheckGetProcAddress(const Name: string): Pointer;
begin
  Result := GetProcAddress(PDFiumLibrary, PChar(Name));
  if Result = nil then
  begin
    // Egy hiányzó kötelező export azt jelenti, hogy az üzembe helyezett pdfium.dll régebbi,
    // mint a binding ez a builde. Dobja el az eddig feloldott összes mutatót,
    // hogy egyetlen hívó se érhessen bele a modulba, amelyet mindjárt felszabadítunk.
    UnloadLibrary;
    raise EPdfError.Create('Required PDFium export not found: ' + Name);
  end;
end;

function TryGetProcAddress(const Name: string): Pointer;
begin
  // Opcionális export. A nil itt legitim válasz; minden hívónak
  // tesztelnie kell az Assigned() értéket, mielőtt dereferálná a változót.
  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');
// A melléklet-leírások a csomagolt DLL-revízió után kerültek hozzáadásra.
// Tartsa őket opcionálisnak, hogy a régebbi telepítések tovább betöltődjenek.
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

Hiányzó PDFium mellékletleírás export Delphi-ben üres olvasást ad TPdf.GetAttachmentDescription révén, íráskor pedig kivételt emel, az AttachmentDescriptionFeaturesAvailable kapuzva
Az olvasó oldal üres válasszal hanyatlik, az író oldal megnevezett okkal dob, és egy megnevezett próba lehetővé teszi, hogy a UI előre letiltsa a funkciót
function TPdf.GetAttachmentDescription(Index: Integer): WString;
begin
  CheckActive;
  Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
  Result := '';

  // Az olvasási oldal lefokozódik: egy régi DLL nem tudja jelenteni a /Desc értéket, és a ''
  // megkülönböztethetetlen egy olyan melléklettől, amelynek nincs leírása.
  if not Assigned(FPDFAttachment_GetDescription) then
    Exit;
  // ... kétmenetes puffer-méretezés az FPDFAttachment_GetDescription ellenében ...
end;

procedure TPdf.SetAttachmentDescription(Index: Integer; const Value: WString);
begin
  CheckActive;
  Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
  // Az írási oldal elutasít: az érték csendes eldobása olyan fájlt hozna létre,
  // amelyet a hívó leírással rendelkezőnek hisz, pedig nem az.
  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
  // Egyszer kérdez, az űrlap beállításakor, ahelyett hogy mentéskor fedezné fel a korlátot.
  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