Tu pdfium.dll carga bien y todavía falta un procedimiento. PDFium Component maneja esto dividiendo sus bindings en dos clases: exports requeridos resueltos a través de CheckGetProcAddress, que abortan la carga por completo, y exports opcionales resueltos a través de TryGetProcAddress, que dejan un puntero nil y una verificación de capacidad en su lugar
Este no es el mismo problema que una DLL que no se puede encontrar. Si tu aplicación muere con un error de formato de EXE incorrecto, un archivo faltante, o un desajuste de arquitectura, esa historia se cuenta en el artículo complementario sobre desplegar pdfium.dll y diagnosticar fallos de carga. Aquí el cargador tuvo éxito. El handle del módulo es válido, cientos de exports se resolvieron, y la ejecución todavía termina antes de que se renderice tu primera página porque un punto de entrada que llegó en un build más nuevo de PDFium no está en el binario en disco
¿Por qué un export faltante rompe toda la librería?
Porque un binding requerido es un contrato duro, y se hace cumplir durante una secuencia de vinculación de todo o nada. PDFium Component resuelve toda su tabla de exports dentro de LoadLibrary, una llamada a CheckGetProcAddress tras otra. El primer resultado nil lanza EPdfError y llama a UnloadLibrary antes de hacerlo, que es deliberado: una vinculación parcial de otro modo dejaría punteros ya resueltos apuntando a un módulo que está a punto de liberarse, derrotando silenciosamente cada guardia Assigned río abajo
La consecuencia es el modo de fallo que trae a la gente aquí. Actualizas el componente, envías el mismo pdfium.dll que has enviado durante dos años, y la aplicación no arranca. El error nombra un export para una función que nunca has llamado. Nada de lo que hagas en el punto de llamada ayuda, porque el punto de llamada nunca se ejecuta; el fallo ocurrió durante la vinculación, antes de que se abriera ningún documento
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;
Requerido u opcional: dónde está realmente la línea
La regla que aplica PDFium Component es directa. Un export es requerido cuando su ausencia hace que el componente no pueda hacer el trabajo para el que existe, y opcional cuando su ausencia solo elimina una función hoja. FPDF_InitLibrary, FPDF_LoadDocument, FPDF_RenderPageBitmap, FPDF_ClosePage son requeridos, y fallar ruidosamente en esos es correcto: un visor que no puede renderizar no es un visor degradado, es uno roto
Todo lo alcanzado a través del cargador tolerante hoy es una hoja. FPDFBookmark_GetColor llegó después de M109 y solo suministra el arreglo de color /C opcional de una entrada de esquema, así que una DLL que precede eso simplemente reporta que no hay color de marcador. Los ayudantes V8 FPDF_GetRecommendedV8Flags y FPDF_GetArrayBufferAllocatorSharedInstance, y los ayudantes de cadena XFA FPDF_BStr_Init, FPDF_BStr_Set y FPDF_BStr_Clear, están ausentes de cualquier build no-V8 por construcción, así que tratarlos como requeridos haría que el pdfium.dll normal fuera imposible de cargar. Y el par que motivó este artículo: FPDFAttachment_SetDescription y FPDFAttachment_GetDescription, agregados río arriba el 2026-07-13, más tarde que la fecha de build de los cuatro binarios de PDFium que el proyecto distribuye bajo DLLs/Win32 y DLLs/Win64. Ese último caso es la forma general del problema, no algo único: una capa de binding rastrea encabezados río arriba, que se mueven continuamente, mientras la DLL en tu instalador se mueve en saltos discretos cada vez que alguien la reconstruye. Siempre hay una ventana en la que el lado Pascal conoce exports que el binario desplegado no tiene, y decidir de antemano de qué lado de la línea requerido/opcional cae cada export nuevo es lo único que hace que esa ventana sea sobrevivible
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');
¿Qué debe hacer una puerta de capacidad en el punto de llamada?
Debe ser asimétrica, y esa asimetría es todo el diseño. Una lectura que no puede ejecutarse tiene una respuesta vacía honesta. Una escritura que no puede ejecutarse no tiene ninguna respuesta honesta en absoluto, así que debe lanzar. PDFium Component divide la propiedad de descripción de adjunto exactamente a lo largo de esa línea, y la división es lo que detiene que un export faltante se convierta en pérdida de datos silenciosa. TPdf.GetAttachmentDescription prueba Assigned(FPDFAttachment_GetDescription) y sale con un WString vacío. Eso no es una mentira: en una DLL sin el export, el componente genuinamente no puede decir si el adjunto lleva una entrada /Desc, y una descripción vacía se lee igual que un adjunto que nunca tuvo una. El resto de la API de adjuntos, cubierta en el artículo sobre trabajar con adjuntos PDF en Delphi, sigue funcionando sin tocar
TPdf.SetAttachmentDescription toma la ruta opuesta. Llama a Check sobre la misma prueba Assigned y lanza EPdfError con el texto "Attachment descriptions are not supported by the loaded PDFium DLL". Retornar silenciosamente aquí sería la peor opción disponible: quien llama establecería una descripción, no obtendría error, guardaría el archivo, y distribuiría un PDF donde la descripción simplemente está ausente. Nadie se da cuenta hasta que un consumidor río abajo pregunta a dónde se fue
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;
Sondear la capacidad antes de ofrecer la función
Capturar una excepción es una mala manera de descubrir qué puede hacer tu despliegue, así que PDFium Component expone la misma prueba como una función nombrada. AttachmentDescriptionFeaturesAvailable llama a LoadLibrary y devuelve si ambas mitades del par se resolvieron. Se sitúa junto a V8FeaturesAvailable, XfaBStrHelpersAvailable y XfaFeaturesAvailable, que siguen el patrón idéntico para sus propios grupos opcionales. Nombrar la sonda importa más de lo que parece: un booleano llamado AttachmentDescriptionFeaturesAvailable le dice al próximo mantenedor que esta función es condicional al binario desplegado, algo que una prueba Assigned desnuda enterrada en un setter de propiedad nunca hace. También le da a la capa de interfaz de usuario algo a lo que vincularse, así que el cuadro de edición de descripción se deshabilita por adelantado en vez de aceptar entrada y rechazarla al guardar
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 qué la cobertura de binding debe probarse con una herramienta?
Porque los números están más allá del punto donde se puede confiar en un humano con ellos. PDFium Component auditó 21 encabezados públicos de PDFium contra una línea base río arriba del 2026-07-29 y encontró 470 funciones ABI de C exportadas. El binding ya cubría 468 de ellas. Nadie ubicó esa brecha de dos leyendo encabezados; un script lo hizo, en un segundo, y lo hará de nuevo en el próximo salto río arriba. tools/audit_pdfium_public_api.py es deliberadamente pequeño: busca con regex FPDF_EXPORT ... FPDF_CALLCONV name( a través de cada encabezado en el directorio público, busca con regex cada CheckGetProcAddress('Name') y TryGetProcAddress('Name') en PDFium.pas, e imprime las dos diferencias de conjunto: missing para exports sin binding, stale para bindings cuyo export ya no existe río arriba. Sale con código distinto de cero cuando cualquiera de los conjuntos no está vacío, así que se inserta en un paso de build sin más ceremonia. El resultado actual es 470 de 470 vinculados, 0 faltantes, 0 obsoletos
La dirección obsoleta se gana su lugar tanto como la faltante. Un export que río arriba elimina deja atrás una línea CheckGetProcAddress que fallará duro en cada carga futura, y ese tipo de pudrición es invisible hasta el día en que alguien actualiza la DLL. La revisión manual encuentra la función en la que estabas pensando; no encuentra la que no estabas pensando. Nota también que la auditoría deliberadamente cuenta ambos cargadores como cobertura, que es la decisión correcta para la deriva de API y la razón por la que la división requerido/opcional tiene que ser una decisión documentada en vez de un subproducto de quien sea que agregó la línea
Dónde el binding opcional deja de ser honesto
Vale la pena declarar dos límites con claridad, porque el patrón es fácil de sobreaplicar. El primero es que un puntero de función nil solo es seguro si literalmente cada ruta que lo toca prueba Assigned primero. En una unidad que declara cientos de variables de función cdecl, una sola llamada sin protección es una violación de acceso en una dirección que no significa nada en un stack trace. La misma disciplina que gobierna las convenciones de llamada y las vidas útiles a través del límite C aplica aquí, y es el tema de el artículo sobre reforzar el binding de PDFium contra fallos de ABI y seguridad de memoria
El segundo límite es el alcance. El binding opcional no es una licencia general para hacer todo tolerante. Si FPDF_RenderPageBitmap fuera opcional, el componente cargaría alegremente y luego fallaría en cada página, convirtiendo un error de arranque claro en una dispersión de errores en tiempo de ejecución sin causa obvia. Requerido es el valor por defecto correcto. Opcional es la excepción a la que recurres cuando una función es genuinamente una hoja, cuando la ausencia tiene un comportamiento degradado defendible del lado de lectura, y cuando el lado de escritura puede negarse con un mensaje que nombre la razón
El diseño del cargador, las sondas de capacidad y la herramienta de auditoría descritos aquí vienen como parte del PDFium Component para Delphi y C++Builder; la página de producto lista los binarios de PDFium incluidos y la superficie completa de la API que exponen