Artículo técnico

Desplegar la DLL de PDFium en Delphi: Solución de fallos de carga

Cuando pdfium.dll se niega a cargarse en el equipo de un cliente, el PDFium Component para Delphi y Lazarus convierte el críptico error nativo en una causa específica. La rutina CheckLoadLibrary interpreta ERROR_BAD_EXE_FORMAT (193) como una incompatibilidad de arquitectura de 32/64 bits, y CheckGetProcAddress reporta la falta de una función de exportación si el archivo pdfium.dll es más antiguo que el enlace (binding). Ambos mensajes indican la solución en lugar de dejar que usted intente adivinarla

Eso importa porque una DLL nativa puede fallar de tres maneras distintas, y el texto simple del sistema operativo tiende a confundirlas todas. La arquitectura puede ser incorrecta, el binario desplegado puede ser demasiado antiguo para el enlace Pascal que lo llama, o dos hilos pueden competir para enlazarlo al mismo tiempo. Cada caso tiene una solución diferente, y el objetivo de los diagnósticos agregados en el trabajo del ciclo de vida de carga de la versión v2.11.0 es indicarle a cuál de ellos se enfrenta realmente

¿Por qué falla la carga de pdfium.dll con un error de formato EXE no válido?

El error de Windows 193, ERROR_BAD_EXE_FORMAT, significa que el archivo se encontró y abrió, pero su tipo de máquina PE no coincide con el proceso principal. Un ejecutable de 64 bits no puede cargar una versión de pdfium.dll de 32 bits, y un ejecutable de 32 bits no puede cargar una de 64 bits. El archivo está presente, por lo que el instinto de buscar una DLL faltante lo guiará exactamente en la dirección equivocada

El PDFium Component intercepta este caso en CheckLoadLibrary: cuando LoadLibrary devuelve un controlador nulo y GetLastError es 193, agrega una indicación de que la DLL fue encontrada pero su arquitectura no coincide con la del proceso principal, y señala hacia la compilación correspondiente en DLLs/Win32 o DLLs/Win64. El subdirectorio es elegido por BuildDllSubDir, que devuelve Win64 cuando IsWin64 es verdadero y Win32 en caso contrario. Despliegue la DLL en el subárbol que el enlace realmente busca y la incompatibilidad desaparecerá

uses
  SysUtils, PDFium;

procedure OpenDocument(const AFileName: string);
var
  Pdf: TPdf;
begin
  // Deploy pdfium.dll next to the executable, matched to the build target:
  //   <AppDir>\DLLs\Win32\pdfium.dll   for a 32-bit host process
  //   <AppDir>\DLLs\Win64\pdfium.dll   for a 64-bit host process
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := AFileName;
    try
      Pdf.Active := True;   // first use lazily binds pdfium.dll
    except
      on E: EPdfError do
        // The message already names the real cause: a 32/64-bit mismatch
        // (ERROR_BAD_EXE_FORMAT) or a pdfium.dll older than this binding
        raise Exception.CreateFmt('PDF engine did not start: %s', [E.Message]);
    end;
    if Pdf.Active then
      RenderFirstPage(Pdf);
  finally
    Pdf.Free;
  end;
end;

¿Qué le indica la falta de una función de exportación de PDFium?

La segunda clase de fallo es la discrepancia de versiones. PDFium distribuye nuevas funciones de exportación con el tiempo, y el enlace Pascal resuelve cada función que necesita a través de CheckGetProcAddress durante el LoadLibrary. Si una exportación requerida no está presente, significa que el enlace es más nuevo que el archivo pdfium.dll en el disco, y el diagnóstico correcto es que la DLL desplegada está obsoleta en lugar de dañada. La función CheckGetProcAddress indica exactamente eso: genera una excepción EPdfError reportando que la versión de pdfium.dll desplegada es más antigua que esta compilación del enlace PDFiumPas, y nombra la ruta DLLs/<subdir> donde debería estar el binario correspondiente

Dos detalles hacen que esa ruta sea confiable. Primero, CheckGetProcAddress llama a UnloadLibrary antes de generar la excepción, por lo que un enlace parcial nunca deja punteros de función medio resueltos que compliquen el siguiente intento. Segundo, el nombre de la DLL que reporta proviene de BuildDllName, que devuelve pdfium.dll normalmente y pdfium.v8.dll cuando la bandera global EnableV8Engine está activa. Si su aplicación habilita el motor JavaScript V8 para formularios XFA o con scripts, el texto del error apuntará a la compilación V8, no a la básica, para que reemplace el archivo correcto desde el primer intento

¿Es seguro cargar la DLL de PDFium desde un hilo en segundo plano?

Cargar la biblioteca ahora es seguro desde cualquier hilo; sin embargo, llamar a la API de PDFium desde varios hilos al mismo tiempo sigue sin serlo. Estas son dos garantías independientes, y mantenerlas separadas marca la diferencia entre un grupo de trabajo estable y un fallo intermitente. La renderización en segundo plano suele ejecutar la renderización de cada página a través de un hilo de trabajo TPdfFuture<T>, y el primer futuro que interactúa con TPdf es el que desencadena el enlace diferido (lazy bind). Sin protección, dos hilos de trabajo podrían observar que la biblioteca no está cargada, ejecutar la secuencia de enlace simultáneamente, sobrescribir el controlador del módulo y provocar una fuga de la primera carga

El trabajo en la versión v2.11.0 cierra esa brecha con PDFiumLoadLock, una sección crítica de Windows TRTLCriticalSection global del proceso que envuelve el acceso tanto a LoadLibrary como a UnloadLibrary. La sección crítica hace que la secuencia de verificación y posterior carga sea atómica, por lo que dos hilos no pueden ver Loaded=False a la vez y realizar un enlace doble, y ningún hilo puede liberar la DLL mientras otro está en pleno enlace. El bloqueo se crea en la sección de inicialización (initialization) de la unidad y se destruye en la de finalización (finalization), protegido por una bandera PDFiumLoadLockReady para que el acoplamiento sea seguro incluso durante el apagado del programa. Si necesita el patrón completo de hilo de trabajo y respuesta con cancelación, el artículo complementario sobre renderización en segundo plano con futuros cancelables lo explica detalladamente de principio a fin

uses
  PDFium, FPdfAsync;

// Worker method runs on a background thread. The first future to bind
// pdfium.dll is serialised by PDFiumLoadLock, so a second concurrent
// worker cannot double-bind or leak the module handle.
function TReportForm.RenderThumbnail(
  const AToken: IPdfCancellationToken): TBitmap;
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Active := True;         // safe concurrent bind
    AToken.ThrowIfCancelled;
    Result := Pdf.Thumbnail;    // this TPdf is owned by one thread only
  finally
    Pdf.Free;
  end;
end;

procedure TReportForm.StartRender;
begin
  TPdfFuture<TBitmap>.Run(RenderThumbnail, ThumbnailReady);
end;

procedure TReportForm.ThumbnailReady(
  const AResult: TPdfFutureResult<TBitmap>);
begin
  if AResult.IsSuccess then
    Preview.Picture.Assign(AResult.Value);
end;

Lo que el bloqueo de carga no protege

Vale la pena definir claramente el límite aquí, ya que es fácil malinterpretar el alcance de la corrección. PDFiumLoadLock solo serializa el estado de enlace global: el controlador del módulo y las asignaciones de punteros de función FPDF_*. La API de C de PDFium subyacente sigue sin ser segura para múltiples hilos, exactamente como lo declaran sus encabezados originales, por lo que serializar las llamadas a las funciones FPDF_* o compartir un controlador FPDF_DOCUMENT entre hilos sigue siendo responsabilidad de usted. La estructura segura es la descrita anteriormente: cada hilo de trabajo posee su propio objeto TPdf y nunca entrega un documento activo a otro hilo. Para un análisis más profundo de ese límite y las reglas de ABI relacionadas, consulte fortalecer el enlace VCL de PDFium contra fallas de memoria y ABI

Por qué importan la finalización y FPDF_DestroyLibrary

Una omisión más sutil se encontraba en la ruta de apagado. La unidad PDFium no tenía una sección finalization, lo que significaba que FPDF_DestroyLibrary nunca se ejecutaba al salir del proceso; el sistema operativo reclamaba la memoria de la DLL y omitía la propia limpieza de PDFium (el acoplamiento del hilo de trabajo, la liberación del aislamiento de V8 cuando la bandera EnableV8Engine está activa, y el desmantelamiento de fuentes y caché). En una carga de trabajo de renderizado simple, esto representa una fuga silenciosa al cerrar el programa, pero con el motor V8 habilitado, provoca una fuga de aislamiento de JavaScript en cada ejecución, lo cual se hace evidente rápidamente bajo automatizaciones repetidas

La sección finalization ahora llama a UnloadLibrary, que a su vez invoca a FPDF_DestroyLibrary y libera el módulo. Esto se mantiene a nivel de alcance de la unidad por una razón: TPdf.Destroy solo establece Active := False para cerrar el documento actual, y deliberadamente no descarga la biblioteca global, de modo que una aplicación con múltiples instancias mantiene activo un enlace PDFium compartido durante toda la vida útil del proceso. Colocar la destrucción en la sección finalization garantiza que la biblioteca compartida se libere exactamente una vez, cuando la unidad se descarga, y UnloadLibrary verifica primero su bandera Loaded, por lo que la llamada es una operación inocua y sin efecto si PDFium nunca se llegó a utilizar

Una breve lista de verificación para el despliegue

Tres hábitos evitan casi todos los fallos de carga en entornos de producción. Asegúrese de que la arquitectura de la DLL coincida con la del proceso principal y distribúyala en DLLs/Win32 o DLLs/Win64; mantenga la versión de pdfium.dll en sincronía con la del enlace para que no falte ninguna función de exportación requerida; y nunca comparta un objeto TPdf o un controlador de documento entre hilos, aunque el enlace en sí sea ahora atómico. Si compila el mismo código fuente tanto para Delphi como para Lazarus, las diferencias de empaquetado que complican el despliegue en múltiples plataformas se detallan en las notas sobre dificultades del compilador cruzado en Delphi y FPC. Los diagnósticos, el bloqueo de carga y la limpieza de finalización descritos aquí se incluyen en el PDFium Component para Delphi y C++Builder