O seu pdfium.dll carrega sem problemas e um procedimento continua em falta. O PDFium Component trata isto dividindo os seus bindings em duas classes: exports obrigatórios resolvidos através de CheckGetProcAddress, que abortam o carregamento por completo, e exports opcionais resolvidos através de TryGetProcAddress, que deixam um ponteiro nil e uma verificação de capacidade em vez disso
Este não é o mesmo problema que uma DLL que não é encontrada. Se a sua aplicação morre com um erro de formato EXE inválido, um ficheiro em falta, ou uma incompatibilidade de arquitetura, essa história está contada em o artigo complementar sobre implantar pdfium.dll e diagnosticar falhas de carregamento. Aqui o carregador teve sucesso. O handle do módulo é válido, centenas de exports resolveram-se, e a execução continua a terminar antes de a sua primeira página renderizar porque um ponto de entrada que chegou numa build mais recente do PDFium não está no binário em disco
Por que quebra um único export em falta a biblioteca inteira?
Porque um binding obrigatório é um contrato rígido, e é imposto durante uma única sequência de binding tudo-ou-nada. O PDFium Component resolve a sua tabela de exports inteira dentro de LoadLibrary, uma chamada CheckGetProcAddress após outra. O primeiro resultado nil lança EPdfError e chama UnloadLibrary antes disso, o que é deliberado: um binding parcial deixaria de outro modo ponteiros já resolvidos apontados para um módulo prestes a ser libertado, derrotando silenciosamente cada proteção Assigned a jusante
A consequência é o modo de falha que traz as pessoas até aqui. Atualiza o componente, envia o mesmo pdfium.dll que tem enviado há dois anos, e a aplicação não arranca. O erro nomeia um export para uma funcionalidade que nunca chamou. Nada do que faça no local de chamada ajuda, porque o local de chamada nunca corre; a falha aconteceu durante o binding, antes de qualquer documento ser aberto
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;
Obrigatório ou opcional: onde fica realmente a linha
A regra que o PDFium Component aplica é direta. Um export é obrigatório quando a sua ausência torna o componente incapaz de fazer o trabalho para que existe, e opcional quando a sua ausência apenas remove uma funcionalidade de folha. FPDF_InitLibrary, FPDF_LoadDocument, FPDF_RenderPageBitmap, FPDF_ClosePage são obrigatórios, e falhar ruidosamente nesses é correto: um visualizador que não consegue renderizar não é um visualizador degradado, é um visualizador partido
Tudo o que é alcançado através do carregador tolerante hoje é uma folha. FPDFBookmark_GetColor chegou depois da M109 e só fornece o array de cor /C opcional de uma entrada de marcador, pelo que uma DLL anterior a ela simplesmente reporta nenhuma cor de marcador. Os auxiliares V8 FPDF_GetRecommendedV8Flags e FPDF_GetArrayBufferAllocatorSharedInstance, e os auxiliares de string XFA FPDF_BStr_Init, FPDF_BStr_Set e FPDF_BStr_Clear, estão ausentes de qualquer build não-V8 por construção, pelo que tratá-los como obrigatórios tornaria o pdfium.dll simples impossível de carregar. E o par que motivou este artigo: FPDFAttachment_SetDescription e FPDFAttachment_GetDescription, acrescentados a montante em 2026-07-13, mais tarde do que a data de build dos quatro binários PDFium que o projeto envia sob DLLs/Win32 e DLLs/Win64. Esse último caso é a forma geral do problema, não um caso isolado: uma camada de binding acompanha cabeçalhos a montante, que se movem continuamente, enquanto a DLL no seu instalador se move em saltos discretos sempre que alguém a reconstrói. Há sempre uma janela em que o lado Pascal conhece exports que o binário implantado não tem, e decidir antecipadamente de que lado da linha obrigatório/opcional cada novo export cai é a única coisa que mantém essa janela sobrevivível
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');
O que deve fazer um gate de capacidade no local de chamada?
Deve ser assimétrico, e essa assimetria é todo o desenho. Uma leitura que não pode correr tem uma resposta vazia honesta. Uma escrita que não pode correr não tem qualquer resposta honesta, pelo que tem de lançar uma exceção. O PDFium Component divide a propriedade de descrição de anexo exatamente ao longo dessa linha, e a divisão é o que impede um export em falta de se transformar em perda silenciosa de dados. TPdf.GetAttachmentDescription testa Assigned(FPDFAttachment_GetDescription) e sai com uma WString vazia. Isso não é uma mentira: numa DLL sem o export, o componente genuinamente não consegue dizer se o anexo transporta uma entrada /Desc, e uma descrição vazia lê-se do mesmo modo que um anexo que nunca teve uma. O resto da API de anexos, coberta em o artigo sobre trabalhar com anexos PDF em Delphi, continua a funcionar sem alterações
TPdf.SetAttachmentDescription toma o percurso oposto. Chama Check sobre o mesmo teste Assigned e lança EPdfError com o texto "Attachment descriptions are not supported by the loaded PDFium DLL". Retornar silenciosamente aqui seria a pior opção disponível: quem chama definiria uma descrição, não obteria erro, gravaria o ficheiro, e enviaria um PDF onde a descrição está simplesmente ausente. Ninguém repara até um consumidor a jusante perguntar para onde foi
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;
Testar a capacidade antes de oferecer a funcionalidade
Apanhar uma exceção é uma forma pobre de descobrir o que a sua implantação consegue fazer, pelo que o PDFium Component expõe o mesmo teste como uma função nomeada. AttachmentDescriptionFeaturesAvailable chama LoadLibrary e devolve se ambas as metades do par resolveram. Fica ao lado de V8FeaturesAvailable, XfaBStrHelpersAvailable e XfaFeaturesAvailable, que seguem o padrão idêntico para os seus próprios grupos opcionais. Nomear a sonda importa mais do que parece: um booleano chamado AttachmentDescriptionFeaturesAvailable diz ao próximo mantenedor que esta funcionalidade é condicional ao binário implantado, o que um teste Assigned nu enterrado num setter de propriedade nunca faz. Também dá à camada de interface algo a que se ligar, para que a caixa de edição de descrição seja desativada à partida em vez de aceitar entrada e a rejeitar ao gravar
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;
Por que tem a cobertura de binding de ser provada por uma ferramenta?
Porque os números já passaram do ponto em que se pode confiar num humano com eles. O PDFium Component auditou 21 cabeçalhos públicos do PDFium contra uma base de referência a montante de 2026-07-29 e encontrou 470 funções exportadas ABI C. O binding já cobria 468 delas. Ninguém localizou essa lacuna de duas ao ler cabeçalhos; um script encontrou, num segundo, e vai voltar a fazê-lo no próximo salto a montante. tools/audit_pdfium_public_api.py é deliberadamente pequeno: faz correspondência de regex a FPDF_EXPORT ... FPDF_CALLCONV name( em cada cabeçalho no diretório público, faz correspondência de regex a cada CheckGetProcAddress('Name') e TryGetProcAddress('Name') em PDFium.pas, e imprime as duas diferenças de conjunto: missing para exports sem binding, stale para bindings cujo export já não existe a montante. Sai com um código diferente de zero quando qualquer um dos conjuntos não está vazio, pelo que se encaixa num passo de build sem mais cerimónia. O resultado atual é 470 de 470 vinculados, 0 em falta, 0 obsoletos
A direção obsoleta ganha o seu lugar tanto quanto a direção em falta. Um export que a montante remove deixa para trás uma linha CheckGetProcAddress que vai falhar irrecuperavelmente cada carregamento futuro, e esse tipo de deterioração é invisível até ao dia em que alguém atualiza a DLL. A revisão manual encontra a função em que estava a pensar; não encontra aquela em que não estava. Note também que a auditoria deliberadamente conta ambos os carregadores como cobertura, o que é a escolha certa para a deriva da API e a razão pela qual a divisão obrigatório/opcional tem de ser uma decisão documentada em vez de um subproduto de quem quer que tenha acrescentado a linha
Onde deixa o binding opcional de ser honesto
Duas fronteiras valem a pena declarar com clareza, porque o padrão é fácil de sobreaplicar. A primeira é que um ponteiro de função nil só é seguro se literalmente todos os percursos que o tocam testarem Assigned primeiro. Numa unidade que declara centenas de variáveis de função cdecl, uma única chamada não protegida é uma violação de acesso num endereço que não significa nada num stack trace. A mesma disciplina que governa convenções de chamada e tempos de vida através da fronteira C aplica-se aqui, e é o assunto de o artigo sobre reforçar o binding PDFium contra falhas de ABI e segurança de memória
A segunda fronteira é o âmbito. O binding opcional não é uma licença geral para tornar tudo tolerante. Se FPDF_RenderPageBitmap fosse opcional, o componente carregaria com sucesso e depois falharia em cada página, convertendo um erro de arranque claro numa dispersão de erros em tempo de execução sem causa óbvia. Obrigatório é a predefinição correta. Opcional é a exceção a que se recorre quando uma funcionalidade é genuinamente uma folha, quando a ausência tem um comportamento degradado defensável do lado da leitura, e quando o lado da escrita pode recusar com uma mensagem que nomeia a razão
O desenho do carregador, as sondas de capacidade e a ferramenta de auditoria aqui descritos fazem parte do PDFium Component para Delphi e C++Builder; a página do produto lista os binários PDFium incluídos e a superfície completa da API que expõem