Articolo tecnico

Export opzionali PDFium: capability gate in Delphi

Il tuo pdfium.dll si carica correttamente e una procedura è comunque mancante. PDFium Component gestisce questo dividendo i propri binding in due classi: gli export richiesti risolti tramite CheckGetProcAddress, che interrompono del tutto il caricamento, e gli export opzionali risolti tramite TryGetProcAddress, che lasciano invece un puntatore nil e un controllo di capacità

Questo non è lo stesso problema di una DLL che non può essere trovata. Se la tua applicazione muore con un errore di formato EXE non valido, un file mancante, o un mismatch di architettura, quella storia è raccontata in l'articolo gemello sulla distribuzione di pdfium.dll e la diagnosi dei fallimenti di caricamento. Qui il loader ha avuto successo. L'handle del modulo è valido, centinaia di export si sono risolti, e l'esecuzione termina comunque prima che venga renderizzata la tua prima pagina perché un punto di ingresso arrivato in una build più recente di PDFium non è nel binario su disco

Perché un export mancante rompe l'intera libreria?

Perché un binding richiesto è un contratto rigido, ed è applicato durante un'unica sequenza di binding tutto-o-niente. PDFium Component risolve l'intera tabella di export dentro LoadLibrary, una chiamata CheckGetProcAddress dopo l'altra. Il primo risultato nil solleva EPdfError e chiama UnloadLibrary prima di farlo, il che è deliberato: un binding parziale altrimenti lascerebbe puntatori già risolti puntati verso un modulo che sta per essere liberato, sconfiggendo silenziosamente ogni protezione Assigned a valle

La conseguenza è la modalità di fallimento che porta le persone qui. Aggiorni il componente, spedisci lo stesso pdfium.dll che hai spedito per due anni, e l'applicazione non si avvia. L'errore nomina un export per una funzionalità che non hai mai chiamato. Niente di ciò che fai nel punto di chiamata aiuta, perché il punto di chiamata non gira mai; il fallimento è avvenuto durante il binding, prima che qualsiasi documento fosse aperto

Il PDFium Component lega la sua tabella di export Delphi in una passata, dove CheckGetProcAddress abortisce il caricamento su un export richiesto mancante mentre TryGetProcAddress degrada in sicurezza un export opzionale
Gli export richiesti si legano secondo il tutto-o-nulla e interrompono il caricamento al primo nil, mentre gli export opzionali lasciano un puntatore nil dietro un controllo di capacità Assigned
function CheckGetProcAddress(const Name: string): Pointer;
begin
  Result := GetProcAddress(PDFiumLibrary, PChar(Name));
  if Result = nil then
  begin
    // Un export richiesto mancante significa che il pdfium.dll distribuito è più vecchio
    // di questa build del binding. Rilascia ogni puntatore risolto finora
    // così nessun chiamante può raggiungere nel modulo che stiamo per liberare.
    UnloadLibrary;
    raise EPdfError.Create('Required PDFium export not found: ' + Name);
  end;
end;

function TryGetProcAddress(const Name: string): Pointer;
begin
  // Export opzionale. nil è una risposta legittima qui; ogni chiamante è
  // tenuto a testare Assigned() prima di dereferenziare la variabile.
  Result := GetProcAddress(PDFiumLibrary, PChar(Name));
end;

Richiesto o opzionale: dove sta realmente la linea

La regola che PDFium Component applica è netta. Un export è richiesto quando la sua assenza rende il componente incapace di svolgere il lavoro per cui esiste, e opzionale quando la sua assenza rimuove solo una funzionalità foglia. FPDF_InitLibrary, FPDF_LoadDocument, FPDF_RenderPageBitmap, FPDF_ClosePage sono richiesti, e fallire rumorosamente su questi è corretto: un viewer che non può renderizzare non è un viewer degradato, è rotto

Tutto ciò a cui il loader tollerante arriva oggi è una foglia. FPDFBookmark_GetColor è arrivato dopo M109 e fornisce solo l'array colore opzionale /C di una voce di outline, così una DLL precedente semplicemente riporta nessun colore di segnalibro. Gli helper V8 FPDF_GetRecommendedV8Flags e FPDF_GetArrayBufferAllocatorSharedInstance, e gli helper stringa XFA FPDF_BStr_Init, FPDF_BStr_Set e FPDF_BStr_Clear, sono assenti da qualsiasi build non-V8 per costruzione, quindi trattarli come richiesti renderebbe il pdfium.dll semplice non caricabile. E la coppia che ha motivato questo articolo: FPDFAttachment_SetDescription e FPDFAttachment_GetDescription, aggiunti upstream il 2026-07-13, più tardi della data di build di tutti e quattro i binari PDFium che il progetto distribuisce sotto DLLs/Win32 e DLLs/Win64. Quest'ultimo caso è la forma generale del problema, non un evento isolato: un livello di binding segue gli header upstream, che si muovono continuamente, mentre la DLL nel tuo installer si muove a salti discreti ogni volta che qualcuno la ricompila. C'è sempre una finestra in cui il lato Pascal conosce export che il binario distribuito non ha, e decidere in anticipo su quale lato della linea richiesto/opzionale cada ogni nuovo export è l'unica cosa che rende quella finestra sopravvivibile

FPDFDoc_GetAttachmentCount    := CheckGetProcAddress('FPDFDoc_GetAttachmentCount');
FPDFDoc_AddAttachment         := CheckGetProcAddress('FPDFDoc_AddAttachment');
FPDFAttachment_GetName        := CheckGetProcAddress('FPDFAttachment_GetName');
FPDFAttachment_GetStringValue := CheckGetProcAddress('FPDFAttachment_GetStringValue');
// Le descrizioni degli allegati sono state aggiunte dopo la revisione della DLL in dotazione.
// Tienile opzionali così le distribuzioni più vecchie continuano a caricarsi.
FPDFAttachment_SetDescription := TryGetProcAddress('FPDFAttachment_SetDescription');
FPDFAttachment_GetDescription := TryGetProcAddress('FPDFAttachment_GetDescription');
FPDFAttachment_SetFile        := CheckGetProcAddress('FPDFAttachment_SetFile');
FPDFAttachment_GetFile        := CheckGetProcAddress('FPDFAttachment_GetFile');

Cosa dovrebbe fare un capability gate nel punto di chiamata?

Dovrebbe essere asimmetrico, e quell'asimmetria è l'intero design. Una lettura che non può girare ha una risposta vuota onesta. Una scrittura che non può girare non ha alcuna risposta onesta, quindi deve sollevare un'eccezione. PDFium Component divide la proprietà descrizione allegato esattamente lungo quella linea, e la divisione è ciò che impedisce a un export mancante di trasformarsi in perdita silenziosa di dati. TPdf.GetAttachmentDescription testa Assigned(FPDFAttachment_GetDescription) ed esce con un WString vuoto. Questa non è una bugia: su una DLL senza l'export, il componente genuinamente non può sapere se l'allegato porta una voce /Desc, e una descrizione vuota si legge allo stesso modo di un allegato che non ne ha mai avuta una. Il resto dell'API allegati, trattata in l'articolo su come lavorare con gli allegati PDF in Delphi, continua a funzionare intatto

TPdf.SetAttachmentDescription prende la strada opposta. Chiama Check sullo stesso test Assigned e solleva EPdfError con il testo "Attachment descriptions are not supported by the loaded PDFium DLL". Restituire silenziosamente qui sarebbe la peggiore opzione disponibile: il chiamante imposterebbe una descrizione, non otterrebbe alcun errore, salverebbe il file, e spedirebbe un PDF dove la descrizione è semplicemente assente. Nessuno se ne accorge finché un consumatore a valle non chiede dove sia finita

Un export mancante della descrizione allegato PDFium in Delphi restituisce una lettura vuota attraverso TPdf.GetAttachmentDescription e solleva alla scrittura, gated da AttachmentDescriptionFeaturesAvailable
Il lato lettura degrada a una risposta vuota, il lato scrittura solleva con un motivo nominato, e una sonda nominata lascia alla UI disabilitare la funzionalità in anticipo
function TPdf.GetAttachmentDescription(Index: Integer): WString;
begin
  CheckActive;
  Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
  Result := '';

  // Il lato lettura degrada: una DLL vecchia non può riportare /Desc, e '' è
  // indistinguibile da un allegato che non porta alcuna descrizione.
  if not Assigned(FPDFAttachment_GetDescription) then
    Exit;
  // ... dimensionamento del buffer a due passaggi contro FPDFAttachment_GetDescription ...
end;

procedure TPdf.SetAttachmentDescription(Index: Integer; const Value: WString);
begin
  CheckActive;
  Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
  // Il lato scrittura rifiuta: scartare silenziosamente il valore produrrebbe un file
  // che il chiamante ritiene porti una descrizione mentre non la porta.
  Check(Assigned(FPDFAttachment_SetDescription),
    'Attachment descriptions are not supported by the loaded PDFium DLL');
  // ... FPDFDoc_GetAttachment, poi FPDFAttachment_SetDescription ...
end;

Sondare la capacità prima di offrire la funzionalità

Catturare un'eccezione è un modo scadente di scoprire cosa può fare la tua distribuzione, quindi PDFium Component espone lo stesso test come una funzione nominata. AttachmentDescriptionFeaturesAvailable chiama LoadLibrary e restituisce se entrambe le metà della coppia si sono risolte. Sta accanto a V8FeaturesAvailable, XfaBStrHelpersAvailable e XfaFeaturesAvailable, che seguono il pattern identico per i propri gruppi opzionali. Nominare la sonda conta più di quanto sembri: un booleano chiamato AttachmentDescriptionFeaturesAvailable dice al prossimo manutentore che questa funzionalità è condizionata al binario distribuito, cosa che un semplice test Assigned sepolto in un setter di proprietà non fa mai. Dà anche al livello UI qualcosa a cui agganciarsi, così il campo di modifica della descrizione viene disabilitato fin da subito invece di accettare input e rifiutarlo al salvataggio

procedure TAttachmentFrame.SyncCapabilities;
begin
  // Chiedi una volta, all'impostazione del modulo, invece di scoprire il limite al salvataggio.
  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;

Perché la copertura del binding deve essere dimostrata da uno strumento?

Perché i numeri sono oltre il punto in cui un umano può esserne fidato. PDFium Component ha sottoposto ad audit 21 header pubblici PDFium contro una baseline upstream del 2026-07-29 e trovato 470 funzioni C ABI esportate. Il binding ne copriva già 468. Nessuno ha individuato quel divario di due leggendo header; l'ha fatto uno script, in un secondo, e lo rifarà al prossimo aggiornamento upstream. tools/audit_pdfium_public_api.py è deliberatamente piccolo: esegue un regex-match di FPDF_EXPORT ... FPDF_CALLCONV name( attraverso ogni header nella directory pubblica, regex-match di ogni CheckGetProcAddress('Name') e TryGetProcAddress('Name') in PDFium.pas, e stampa le due differenze di insieme: missing per gli export senza binding, stale per i binding il cui export non esiste più upstream. Esce con codice diverso da zero quando uno dei due insiemi non è vuoto, così si inserisce in uno step di build senza ulteriore cerimonia. Il risultato attuale è 470 su 470 legati, mancanti 0, obsoleti 0

La direzione stale merita la sua attenzione tanto quanto quella missing. Un export che upstream rimuove lascia dietro di sé una riga CheckGetProcAddress che farà fallire duramente ogni caricamento futuro, e quel tipo di degrado è invisibile fino al giorno in cui qualcuno aggiorna la DLL. La revisione manuale trova la funzione a cui stavi pensando; non trova quella a cui non stavi pensando. Nota anche che l'audit conta deliberatamente entrambi i loader come copertura, il che è la scelta giusta per la deriva delle API e il motivo per cui la divisione richiesto/opzionale deve essere una decisione documentata piuttosto che un sottoprodotto di chiunque abbia aggiunto la riga

Dove il binding opzionale smette di essere onesto

Vale la pena dichiarare chiaramente due confini, perché il pattern è facile da applicare eccessivamente. Il primo è che un puntatore a funzione nil è sicuro solo se letteralmente ogni percorso che lo tocca testa Assigned prima. In un'unit che dichiara centinaia di variabili funzione cdecl, una singola chiamata non protetta è una violazione di accesso a un indirizzo che non significa nulla in uno stack trace. La stessa disciplina che governa le convenzioni di chiamata e le durate attraverso il confine C si applica qui, ed è l'argomento di l'articolo sull'irrobustimento del binding PDFium contro guasti ABI e di sicurezza della memoria

Il secondo confine è l'ambito. Il binding opzionale non è una licenza generale per rendere tutto tollerante. Se FPDF_RenderPageBitmap fosse opzionale, il componente si caricherebbe felicemente e poi fallirebbe su ogni pagina, convertendo un chiaro errore di avvio in una dispersione di errori a runtime senza causa ovvia. Richiesto è il predefinito corretto. Opzionale è l'eccezione a cui ricorri quando una funzionalità è genuinamente una foglia, quando l'assenza ha un comportamento degradato difendibile sul lato lettura, e quando il lato scrittura può rifiutare con un messaggio che nomina il motivo

Il design del loader, le sonde di capacità e lo strumento di audit descritti qui sono distribuiti come parte di PDFium Component per Delphi e C++Builder; la pagina prodotto elenca i binari PDFium inclusi e l'intera superficie API che espongono