Artículo técnico

Exportaciones opcionales de PDFium: gates de capacidad en Delphi

Tu pdfium.dll carga sin problemas y aun así falta un procedimiento. PDFium Component gestiona esto dividiendo sus bindings en dos clases: exportaciones obligatorias resueltas mediante CheckGetProcAddress, que abortan la carga directamente, y exportaciones opcionales resueltas mediante TryGetProcAddress, que dejan atrás un puntero nil y una comprobació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 EXE incorrecto, un archivo ausente, o una discrepancia de arquitectura, esa historia se cuenta en el artículo complementario sobre el despliegue de pdfium.dll y el diagnóstico de fallos de carga. Aquí el cargador tuvo éxito. El manejador del módulo es válido, cientos de exportaciones se resolvieron, y la ejecución sigue terminando 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é una única exportación ausente rompe toda la librería?

Porque un binding obligatorio es un contrato estricto, y se aplica durante una única secuencia de vinculación de todo o nada. PDFium Component resuelve toda su tabla de exportaciones dentro de LoadLibrary, una llamada a CheckGetProcAddress tras otra. El primer resultado nil lanza EPdfError y llama a UnloadLibrary antes de hacerlo, lo cual es deliberado: una vinculación parcial dejaría, de otro modo, punteros ya resueltos apuntando a un módulo que está a punto de liberarse, derrotando silenciosamente cada guarda Assigned posterior

La consecuencia es el modo de fallo que trae a la gente hasta aquí. Actualizas el componente, distribuyes el mismo pdfium.dll que has distribuido durante dos años, y la aplicación no arranca. El error nombra una exportación de 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;

Obligatoria u opcional: dónde está realmente el límite

La regla que aplica PDFium Component es directa. Una exportación es obligatoria 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 obligatorias, y fallar de forma ruidosa en esas es correcto: un visor que no puede renderizar no es un visor degradado, es uno roto

Todo lo que alcanza hoy el cargador tolerante es una hoja. FPDFBookmark_GetColor llegó después de M109 y solo suministra el array de color /C opcional de una entrada de esquema, así que una DLL anterior a él simplemente informa de que no hay color de marcador. Los ayudantes de 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 sin V8 por construcción, así que tratarlos como obligatorios haría que el pdfium.dll simple no se pudiera cargar. Y el par que motivó este artículo: FPDFAttachment_SetDescription y FPDFAttachment_GetDescription, añadidos aguas arriba el 2026-07-13, más tarde que la fecha de build de los cuatro binarios de PDFium que distribuye el proyecto bajo DLLs/Win32 y DLLs/Win64. Ese último caso es la forma general del problema, no un hecho aislado: una capa de binding sigue las cabeceras aguas arriba, que se mueven continuamente, mientras que la DLL de tu instalador se mueve en saltos discretos cada vez que alguien la reconstruye. Siempre hay una ventana en la que el lado Pascal conoce exportaciones que el binario desplegado no tiene, y decidir de antemano en qué lado de la línea obligatorio/opcional cae cada exportación nueva 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 un gate de capacidad en el punto de llamada?

Debe ser asimétrico, 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 una excepción. PDFium Component divide la propiedad de descripción de adjunto exactamente por esa línea, y esa división es lo que evita que una exportación ausente se convierta en pérdida silenciosa de datos. TPdf.GetAttachmentDescription comprueba Assigned(FPDFAttachment_GetDescription) y sale con un WString vacío. Eso no es una mentira: en una DLL sin la exportación, el componente genuinamente no puede saber 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 de PDF en Delphi, sigue funcionando sin cambios

TPdf.SetAttachmentDescription toma la ruta opuesta. Llama a Check sobre la misma comprobación Assigned y lanza EPdfError con el texto "Attachment descriptions are not supported by the loaded PDFium DLL". Devolver el control en silencio aquí sería la peor opción disponible: el llamador establecería una descripción, no obtendría ningún error, guardaría el archivo, y distribuiría un PDF donde la descripción está simplemente ausente. Nadie se da cuenta hasta que un consumidor posterior pregunta adónde fue a parar

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 con nombre. 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 mismo 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 siguiente mantenedor que esta función es condicional al binario desplegado, algo que una comprobación Assigned desnuda enterrada en un setter de propiedad nunca hace. También le da a la capa de interfaz algo a lo que enlazarse, así que el cuadro de edición de descripción se desactiva de entrada en lugar de aceptar la 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 del binding debe demostrarla una herramienta?

Porque las cifras han superado el punto en el que se puede confiar en un humano para ellas. PDFium Component auditó 21 cabeceras públicas de PDFium contra una línea base aguas arriba del 2026-07-29 y encontró 470 funciones exportadas de ABI en C. El binding ya cubría 468 de ellas. Nadie localizó esa brecha de dos leyendo cabeceras; lo hizo un script, en un segundo, y lo volverá a hacer en la siguiente actualización aguas arriba. tools/audit_pdfium_public_api.py es deliberadamente pequeño: busca con expresiones regulares FPDF_EXPORT ... FPDF_CALLCONV name( en cada cabecera del directorio público, busca con expresiones regulares cada CheckGetProcAddress('Name') y TryGetProcAddress('Name') en PDFium.pas, e imprime las dos diferencias de conjunto: missing para exportaciones sin binding, stale para bindings cuya exportación ya no existe aguas arriba. Sale con código distinto de cero cuando cualquiera de los conjuntos no está vacío, así que se integra en un paso de build sin más ceremonia. El resultado actual es 470 de 470 vinculadas, 0 ausentes, 0 obsoletas

La dirección "obsoleta" se gana su lugar tanto como la de "ausente". Una exportación que se elimina aguas arriba deja atrás una línea CheckGetProcAddress que hará fallar de forma irrecuperable cada carga futura, y ese tipo de deterioro 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. Nótese también que la auditoría cuenta deliberadamente ambos cargadores como cobertura, que es la decisión correcta ante la deriva de la API y el motivo por el que la división obligatorio/opcional tiene que ser una decisión documentada y no un subproducto de quien haya añadido la línea

Dónde el binding opcional deja de ser honesto

Vale la pena señalar con claridad dos límites, 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 comprueba Assigned primero. En una unidad que declara cientos de variables de función cdecl, una única llamada sin proteger es una violación de acceso en una dirección que no significa nada en una traza de pila. La misma disciplina que gobierna las convenciones de llamada y las vidas útiles a través del límite con C se 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 hacerlo 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. Obligatorio 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 en el lado de lectura, y cuando el lado de escritura puede negarse con un mensaje que nombre el motivo

El diseño del cargador, las sondas de capacidad y la herramienta de auditoría descritos aquí se incluyen como parte de PDFium Component para Delphi y C++Builder; la página del producto enumera los binarios de PDFium incluidos y la superficie completa de la API que exponen