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