Articol tehnic

Exporturi PDFium opționale: porți de capabilitate în Delphi

pdfium.dll-ul dumneavoastră se încarcă bine, iar o procedură tot lipsește. PDFium Component gestionează asta împărțind binding-urile sale în două clase: exporturile necesare rezolvate prin CheckGetProcAddress, care abandonează încărcarea direct, și exporturile opționale rezolvate prin TryGetProcAddress, care lasă în urmă un pointer nil și o verificare de capabilitate în schimb

Aceasta nu este aceeași problemă ca un DLL care nu poate fi găsit. Dacă aplicația dumneavoastră moare cu o eroare de format EXE greșit, un fișier lipsă sau o nepotrivire de arhitectură, acea poveste este spusă în articolul asociat despre implementarea pdfium.dll și diagnosticarea eșecurilor de încărcare. Aici loader-ul a reușit. Handle-ul modulului este valid, sute de exporturi s-au rezolvat, iar rularea tot se termină înainte ca prima dumneavoastră pagină să se randeze pentru că un punct de intrare care a sosit într-un build PDFium mai nou nu se află în binarul de pe disc

De ce un singur export lipsă strică întreaga bibliotecă?

Pentru că un binding necesar este un contract dur, și este aplicat în timpul unei singure secvențe de bind totul-sau-nimic. PDFium Component își rezolvă întregul tabel de exporturi în interiorul lui LoadLibrary, un apel CheckGetProcAddress după altul. Primul rezultat nil ridică EPdfError și apelează UnloadLibrary înainte de asta, ceea ce este deliberat: un bind parțial ar lăsa altfel pointeri deja rezolvați îndreptați spre un modul care este pe cale să fie eliberat, învingând tacit fiecare gardă Assigned din aval

Consecința este modul de eșec care aduce oamenii aici. Actualizați componenta, livrați același pdfium.dll pe care l-ați livrat de doi ani, iar aplicația nu pornește. Eroarea numește un export pentru o funcționalitate pe care nu ați apelat-o niciodată. Nimic ce faceți la locul apelului nu ajută, deoarece locul apelului nu rulează niciodată; eșecul a avut loc în timpul bind-ului, înainte ca vreun document să fie deschis

PDFium Component își leagă tabela de exporturi Delphi într-o singură trecere, în care CheckGetProcAddress abandonează încărcarea la un export cerut lipsă, în timp ce TryGetProcAddress degradează în siguranță unul opțional
Exporturile obligatorii se leagă tot-sau-nimic și abandonează încărcarea la primul nil, în timp ce exporturile opționale lasă un pointer nil în spatele unei verificări de capabilitate Assigned
function CheckGetProcAddress(const Name: string): Pointer;
begin
  Result := GetProcAddress(PDFiumLibrary, PChar(Name));
  if Result = nil then
  begin
    // O exportare obligatorie lipsă înseamnă că pdfium.dll implementat este mai vechi
    // decât această compilare a legăturii. Aruncați fiecare pointer rezolvat până acum
    // pentru ca niciun apelant să nu poată ajunge în modulul pe care urmează să îl eliberăm.
    UnloadLibrary;
    raise EPdfError.Create('Required PDFium export not found: ' + Name);
  end;
end;

function TryGetProcAddress(const Name: string): Pointer;
begin
  // Exportare opțională. nil este un răspuns legitim aici; fiecare apelant este
  // obligat să testeze Assigned() înainte de a dereferenția variabila.
  Result := GetProcAddress(PDFiumLibrary, PChar(Name));
end;

Necesar sau opțional: unde stă de fapt linia

Regula pe care PDFium Component o aplică este directă. Un export este necesar când absența lui face ca componenta să nu poată face treaba pentru care există, și opțional când absența lui elimină doar o funcționalitate frunză. FPDF_InitLibrary, FPDF_LoadDocument, FPDF_RenderPageBitmap, FPDF_ClosePage sunt necesare, iar eșecul zgomotos pe acestea este corect: un vizualizator care nu poate randa nu este un vizualizator degradat, este unul stricat

Tot ce este atins astăzi prin loader-ul tolerant este o frunză. FPDFBookmark_GetColor a sosit după M109 și furnizează doar array-ul opțional de culoare /C al unei intrări de contur, așa că un DLL care îl predatează pur și simplu raportează nicio culoare de semn de carte. Ajutoarele V8 FPDF_GetRecommendedV8Flags și FPDF_GetArrayBufferAllocatorSharedInstance, și ajutoarele de string XFA FPDF_BStr_Init, FPDF_BStr_Set și FPDF_BStr_Clear, sunt absente din orice build non-V8 prin construcție, așa că tratarea lor ca necesare ar face pdfium.dll-ul simplu de neîncărcat. Iar perechea care a motivat acest articol: FPDFAttachment_SetDescription și FPDFAttachment_GetDescription, adăugate în amonte pe 2026-07-13, mai târziu decât data de build a tuturor celor patru binare PDFium pe care proiectul le livrează sub DLLs/Win32 și DLLs/Win64. Acel ultim caz este forma generală a problemei, nu un caz izolat: un strat de binding urmărește anteturile din amonte, care se mișcă continuu, în timp ce DLL-ul din instalatorul dumneavoastră se mișcă în salturi discrete ori de câte ori cineva îl reconstruiește. Există întotdeauna o fereastră în care partea Pascal cunoaște exporturi pe care binarul implementat nu le are, iar decizia dinainte asupra căreia parte a liniei necesar/opțional cade fiecare export nou este singurul lucru care menține acea fereastră supraviețuibilă

FPDFDoc_GetAttachmentCount    := CheckGetProcAddress('FPDFDoc_GetAttachmentCount');
FPDFDoc_AddAttachment         := CheckGetProcAddress('FPDFDoc_AddAttachment');
FPDFAttachment_GetName        := CheckGetProcAddress('FPDFAttachment_GetName');
FPDFAttachment_GetStringValue := CheckGetProcAddress('FPDFAttachment_GetStringValue');
// Descrierile de atașamente au fost adăugate după revizia DLL-ului inclus.
// Păstrați-le opționale, astfel încât implementările mai vechi să continue să se încarce.
FPDFAttachment_SetDescription := TryGetProcAddress('FPDFAttachment_SetDescription');
FPDFAttachment_GetDescription := TryGetProcAddress('FPDFAttachment_GetDescription');
FPDFAttachment_SetFile        := CheckGetProcAddress('FPDFAttachment_SetFile');
FPDFAttachment_GetFile        := CheckGetProcAddress('FPDFAttachment_GetFile');

Ce ar trebui să facă o poartă de capabilitate la locul apelului?

Ar trebui să fie asimetrică, iar acea asimetrie este întregul design. O citire care nu poate rula are un răspuns onest gol. O scriere care nu poate rula nu are deloc un răspuns onest, așa că trebuie să ridice o excepție. PDFium Component împarte proprietatea descrierii atașamentului exact pe acea linie, iar împărțirea este ceea ce oprește un export lipsă să se transforme în pierdere tacită de date. TPdf.GetAttachmentDescription testează Assigned(FPDFAttachment_GetDescription) și iese cu un WString gol. Aceasta nu este o minciună: pe un DLL fără exportul respectiv, componenta chiar nu poate spune dacă atașamentul poartă o intrare /Desc, iar o descriere goală se citește la fel ca un atașament care nu a avut niciodată una. Restul API-ului de atașamente, tratat în articolul despre lucrul cu atașamente PDF în Delphi, continuă să funcționeze neatins

TPdf.SetAttachmentDescription ia ruta opusă. Apelează Check pe același test Assigned și ridică EPdfError cu textul "Attachment descriptions are not supported by the loaded PDFium DLL". Revenirea tacită aici ar fi cea mai proastă opțiune disponibilă: apelantul ar seta o descriere, nu ar primi nicio eroare, ar salva fișierul și ar livra un PDF unde descrierea este pur și simplu absentă. Nimeni nu observă până când un consumator din aval întreabă unde s-a dus

Un export de descriere de atașament PDFium lipsă în Delphi returnează o citire vidă prin TPdf.GetAttachmentDescription și ridică la scriere, filtrat de AttachmentDescriptionFeaturesAvailable
Partea de citire se degradează la un răspuns gol, partea de scriere ridică excepție cu un motiv denumit, iar o sondă denumită lasă UI-ul să dezactiveze funcționalitatea dinainte
function TPdf.GetAttachmentDescription(Index: Integer): WString;
begin
  CheckActive;
  Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
  Result := '';

  // Partea de citire degradează: un DLL vechi nu poate raporta /Desc, iar '' este
  // imposibil de distins de un atașament care nu poartă nicio descriere.
  if not Assigned(FPDFAttachment_GetDescription) then
    Exit;
  // ... dimensionare în două treceri a bufferului împotriva FPDFAttachment_GetDescription ...
end;

procedure TPdf.SetAttachmentDescription(Index: Integer; const Value: WString);
begin
  CheckActive;
  Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
  // Partea de scriere refuză: scăderea silențioasă a valorii ar produce un fișier
  // pe care apelantul crede că îl poartă cu o descriere și nu o poartă.
  Check(Assigned(FPDFAttachment_SetDescription),
    'Attachment descriptions are not supported by the loaded PDFium DLL');
  // ... FPDFDoc_GetAttachment, apoi FPDFAttachment_SetDescription ...
end;

Sondarea capabilității înainte de a oferi funcționalitatea

Prinderea unei excepții este un mod slab de a descoperi ce poate face implementarea dumneavoastră, așa că PDFium Component expune același test ca o funcție numită. AttachmentDescriptionFeaturesAvailable apelează LoadLibrary și returnează dacă ambele jumătăți ale perechii s-au rezolvat. Stă alături de V8FeaturesAvailable, XfaBStrHelpersAvailable și XfaFeaturesAvailable, care urmează același tipar pentru propriile lor grupuri opționale. Numirea sondei contează mai mult decât pare: un boolean numit AttachmentDescriptionFeaturesAvailable spune următorului mentenant că această funcționalitate este condiționată de binarul implementat, ceea ce un simplu test Assigned îngropat într-un setter de proprietate nu face niciodată. Mai dă și stratului de UI ceva de care să se lege, astfel încât căsuța de editare a descrierii este dezactivată din start, în loc să accepte intrare și să o respingă la salvare

procedure TAttachmentFrame.SyncCapabilities;
begin
  // Întrebați o singură dată, la configurarea formularului, în loc să descoperiți limita la salvare.
  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;

De ce trebuie dovedită acoperirea binding-ului printr-un instrument?

Pentru că numerele au trecut de punctul în care unui om i se poate avea încredere cu ele. PDFium Component a auditat 21 de anteturi PDFium publice față de o linie de bază din amonte din 2026-07-29 și a găsit 470 de funcții ABI C exportate. Binding-ul acoperea deja 468 dintre ele. Nimeni nu a localizat acel gol de doi citind anteturile; un script a făcut-o, într-o secundă, și o va face din nou la următoarea actualizare din amonte. tools/audit_pdfium_public_api.py este deliberat mic: se potrivește cu regex pe FPDF_EXPORT ... FPDF_CALLCONV name( peste fiecare antet din directorul public, se potrivește cu regex pe fiecare CheckGetProcAddress('Name') și TryGetProcAddress('Name') din PDFium.pas, și afișează cele două diferențe de mulțimi: missing pentru exporturile fără niciun binding, stale pentru binding-urile al căror export nu mai există în amonte. Iese cu cod diferit de zero când oricare mulțime este nevidă, așa că se strecoară într-un pas de build fără mai multă ceremonie. Rezultatul curent este 470 din 470 legate, missing 0, stale 0

Direcția stale își câștigă locul la fel de mult ca cea missing. Un export pe care amontele îl elimină lasă în urmă o linie CheckGetProcAddress care va eșua dur fiecare încărcare viitoare, iar acel tip de putrezire este invizibil până în ziua în care cineva actualizează DLL-ul. Revizia manuală găsește funcția la care vă gândeați; nu o găsește pe cea la care nu vă gândeați. Rețineți de asemenea că auditul contabilizează deliberat ambii loaderi ca acoperire, ceea ce este alegerea corectă pentru deriva API, și motivul pentru care împărțirea necesar/opțional trebuie să fie o decizie documentată, nu un produs secundar al oricui a adăugat linia

Unde încetează binding-ul opțional să fie onest

Două limite merită enunțate clar, deoarece tiparul este ușor de aplicat excesiv. Prima este că un pointer de funcție nil este sigur doar dacă literalmente fiecare cale care îl atinge testează Assigned mai întâi. Într-o unitate care declară sute de variabile de funcție cdecl, un singur apel nepăzit este o încălcare de acces la o adresă care nu înseamnă nimic într-un stack trace. Aceeași disciplină care guvernează convențiile de apel și duratele de viață peste granița C se aplică aici, și este subiectul articolului despre întărirea binding-ului PDFium împotriva defectelor ABI și de siguranță a memoriei

A doua limită este domeniul de aplicare. Binding-ul opțional nu este o licență generală de a face totul tolerant. Dacă FPDF_RenderPageBitmap ar fi opțional, componenta s-ar încărca fericită și apoi ar eșua pe fiecare pagină, convertind o eroare clară de pornire într-o împrăștiere de erori la rulare fără nicio cauză evidentă. Necesar este valoarea implicită corectă. Opțional este excepția la care apelați când o funcționalitate este cu adevărat o frunză, când absența are un comportament degradat apărabil pe partea de citire, și când partea de scriere poate refuza cu un mesaj care numește motivul

Designul loader-ului, sondele de capabilitate și instrumentul de audit descrise aici vin ca parte a PDFium Component pentru Delphi și C++Builder; pagina de produs listează binarele PDFium incluse și întreaga suprafață API pe care o expun