Tehnički članak

Opcioni PDFium izvozi: kapije mogućnosti u Delphi

Vaš pdfium.dll se učitava bez problema, a jedna procedura i dalje nedostaje. PDFium Component ovo rešava razdvajajući svoje binding-e u dve klase: obavezni izvozi razrešeni kroz CheckGetProcAddress, koji u potpunosti prekidaju učitavanje, i opcioni izvozi razrešeni kroz TryGetProcAddress, koji ostavljaju nil pokazivač i proveru mogućnosti umesto toga

Ovo nije isti problem kao DLL koji se ne može pronaći. Ako vaša aplikacija umire sa greškom lošeg EXE formata, nedostajućim fajlom, ili neslaganjem arhitekture, ta priča je ispričana u pratećem članku o deploy-ovanju pdfium.dll i dijagnostici otkaza učitavanja. Ovde je loader uspeo. Handle modula je validan, stotine izvoza su razrešene, a pokretanje se i dalje završava pre nego što se vaša prva stranica renderuje jer jedna ulazna tačka koja je stigla u novijem PDFium build-u nije u binarnom fajlu na disku

Zašto jedan nedostajući izvoz razbija čitavu biblioteku?

Zato što je obavezan binding tvrd ugovor, i primenjuje se tokom jedne sekvence vezivanja svega-ili-ništa. PDFium Component razrešava celu svoju tabelu izvoza unutar LoadLibrary, jedan poziv CheckGetProcAddress za drugim. Prvi nil rezultat izaziva EPdfError i pre toga poziva UnloadLibrary, što je namerno: delimično vezivanje bi inače ostavilo već razrešene pokazivače usmerene u modul koji je na ivici oslobađanja, tiho porazivši svaku Assigned stražu nizvodno

Posledica je režim otkaza koji vas ovde dovodi. Nadograđujete komponentu, isporučujete isti pdfium.dll koji isporučujete dve godine, i aplikacija se neće pokrenuti. Greška imenuje izvoz za funkciju koju nikad niste pozvali. Ništa što uradite na mestu poziva ne pomaže, jer se mesto poziva nikad ne izvršava; otkaz se desio tokom vezivanja, pre nego što je ijedan dokument otvoren

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;

Obavezno ili opciono: gde linija zapravo sedi

Pravilo koje PDFium Component primenjuje je grubo. Izvoz je obavezan kad njegovo odsustvo čini komponentu nesposobnom da obavi posao za koji postoji, a opcion kad njegovo odsustvo uklanja samo jednu list funkciju. FPDF_InitLibrary, FPDF_LoadDocument, FPDF_RenderPageBitmap, FPDF_ClosePage su obavezni, i glasan neuspeh na njima je ispravan: pregledač koji ne može da renderuje nije degradiran pregledač, već pokvaren

Sve dostupno kroz tolerantan loader danas je list. FPDFBookmark_GetColor stigao je posle M109 i samo dostavlja opcioni niz boje /C unosa obrisa, tako da DLL koji mu prethodi jednostavno prijavljuje bez boje bookmark-a. V8 pomoćnici FPDF_GetRecommendedV8Flags i FPDF_GetArrayBufferAllocatorSharedInstance, i XFA pomoćnici stringova FPDF_BStr_Init, FPDF_BStr_Set i FPDF_BStr_Clear, odsutni su iz bilo kog ne-V8 build-a konstrukcijom, tako da bi njihovo tretiranje kao obaveznih učinilo obični pdfium.dll neučitivim. I par koji je motivisao ovaj članak: FPDFAttachment_SetDescription i FPDFAttachment_GetDescription, dodati uzvodno 2026-07-13, kasnije od datuma build-a sva četiri PDFium binarna fajla koje projekat isporučuje pod DLLs/Win32 i DLLs/Win64. Taj poslednji slučaj je opšti oblik problema, ne jednokratna stvar: sloj binding-a prati uzvodne header-e, koji se kontinuirano pomeraju, dok se DLL u vašem instalateru pomera u diskretnim skokovima kad god ga neko ponovo build-uje. Uvek postoji prozor u kom Pascal strana zna o izvozima koje deploy-ovan binarni fajl nema, a odlučivanje unapred na kojoj strani obavezno/opciono granice svaki novi izvoz pada je jedino što drži taj prozor preživljivim

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

Šta kapija mogućnosti treba da uradi na mestu poziva?

Treba da bude asimetrična, i ta asimetrija je čitav dizajn. Čitanje koje ne može da se izvrši ima iskren prazan odgovor. Pisanje koje ne može da se izvrši nema nikakav iskren odgovor, tako da mora izazvati izuzetak. PDFium Component deli svojstvo opisa priloga tačno duž te linije, a podela je ono što sprečava da nedostajući izvoz postane tih gubitak podataka. TPdf.GetAttachmentDescription testira Assigned(FPDFAttachment_GetDescription) i izlazi sa praznim WString. To nije laž: na DLL-u bez izvoza, komponenta zaista ne može reći da li prilog nosi unos /Desc, a prazan opis se čita isto kao prilog koji ga nikad nije imao. Ostatak API-ja priloga, pokriven u članku o radu sa PDF prilozima u Delphi, nastavlja da radi nedirnut

TPdf.SetAttachmentDescription ide suprotnim putem. Poziva Check na istom Assigned testu i izaziva EPdfError sa tekstom "Attachment descriptions are not supported by the loaded PDFium DLL". Tiho vraćanje ovde bi bila najgora dostupna opcija: pozivalac bi postavio opis, ne bi dobio grešku, sačuvao fajl, i isporučio PDF gde je opis jednostavno odsutan. Niko to ne primeti dok potrošač nizvodno ne pita gde je nestao

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;

Probanje mogućnosti pre nego što ponudite funkciju

Hvatanje izuzetka je loš način da otkrijete šta vaš deployment može da uradi, tako da PDFium Component izlaže isti test kao imenovanu funkciju. AttachmentDescriptionFeaturesAvailable poziva LoadLibrary i vraća da li su se obe polovine para razrešile. Sedi pored V8FeaturesAvailable, XfaBStrHelpersAvailable i XfaFeaturesAvailable, koji slede identičan obrazac za sopstvene opcione grupe. Imenovanje probe je bitnije nego što izgleda: boolean nazvan AttachmentDescriptionFeaturesAvailable govori sledećem održavaocu da je ova funkcija uslovljena deploy-ovanim binarnim fajlom, što goli Assigned test zakopan u setter-u svojstva nikad ne radi. Takođe daje UI sloju nešto na šta se vezati, tako da je polje za izmenu opisa unapred onemogućeno umesto da prihvata unos i odbacuje ga pri snimanju

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;

Zašto pokrivenost binding-a mora biti dokazana alatom?

Zato što su brojevi prošli tačku gde se čoveku može verovati sa njima. PDFium Component je revidirao 21 javni PDFium header naspram uzvodne osnove od 2026-07-29 i pronašao 470 izvezenih C ABI funkcija. Binding je već pokrivao 468 od njih. Niko nije locirao taj jaz od dva čitanjem header-a; skripta jeste, za sekundu, i uradiće to ponovo na sledećem uzvodnom skoku. tools/audit_pdfium_public_api.py je namerno mala: regex-poklapa FPDF_EXPORT ... FPDF_CALLCONV name( preko svakog header-a u javnom direktorijumu, regex-poklapa svaki CheckGetProcAddress('Name') i TryGetProcAddress('Name') u PDFium.pas, i ispisuje dve razlike skupova: missing za izvoze bez binding-a, stale za binding-e čiji izvoz više ne postoji uzvodno. Izlazi sa ne-nula kodom kad je bilo koji skup ne-prazan, tako da upada u korak build-a bez daljeg ceremonijala. Trenutni rezultat je 470 od 470 vezano, missing 0, stale 0

Smer stale zaslužuje svoje mesto koliko i missing. Izvoz koji uzvodno uklone ostavlja iza sebe red CheckGetProcAddress koji će tvrdo otkazati svako buduće učitavanje, i ta vrsta truljenja je nevidljiva do dana kad neko ažurira DLL. Ručna revizija pronalazi funkciju o kojoj ste razmišljali; ne pronalazi onu o kojoj niste. Primetite i da revizija namerno broji oba loader-a kao pokrivenost, što je ispravna odluka za drift API-ja i razlog zašto podela obavezno/opciono mora biti dokumentovana odluka, a ne nusprodukt onoga ko je dodao red

Gde opcioni binding prestaje da bude iskren

Vredi jasno navesti dve granice, jer je šablon lako preterano primeniti. Prva je da je nil pokazivač funkcije bezbedan samo ako baš svaka putanja koja ga dodiruje prvo testira Assigned. U jedinici koja deklariše stotine promenljivih funkcija cdecl, jedan nezaštićen poziv je access violation na adresi koja ništa ne znači u stack trace-u. Ista disciplina koja upravlja konvencijama poziva i trajanjima preko C granice primenjuje se ovde, i tema je članka o otvrdnjavanju PDFium binding-a protiv ABI i bezbednosnih grešaka memorije

Druga granica je opseg. Opcioni binding nije opšta licenca da sve učinite tolerantnim. Da je FPDF_RenderPageBitmap opcion, komponenta bi se učitala bez problema i onda otkazala na svakoj stranici, pretvarajući jednu jasnu grešku pokretanja u rasejane runtime greške bez očiglednog uzroka. Obavezno je ispravno podrazumevano. Opciono je izuzetak kome posegnete kad je funkcija zaista list, kad odsustvo ima odbranjivo degradirano ponašanje na strani čitanja, i kad strana pisanja može odbiti sa porukom koja imenuje razlog

Dizajn loader-a, probe mogućnosti i alat revizije opisani ovde isporučuju se kao deo PDFium Component za Delphi i C++Builder; stranica proizvoda navodi upakovane PDFium binarne fajlove i kompletnu površinu API-ja koju izlažu