Artículo técnico

Despliegue de la DLL PDFium en Delphi: solución de errores de carga

Cuando pdfium.dll se niega a cargarse en la máquina de un cliente, 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 un conflicto de arquitectura de 32/64 bits, y CheckGetProcAddress informa de una exportación faltante cuando pdfium.dll es más antigua que la vinculación (binding). Ambos mensajes indican la solución en lugar de dejarle adivinar

Eso es importante porque una DLL nativa falla de tres maneras distintas, y el texto sin procesar del sistema operativo las confunde todas. La arquitectura puede ser incorrecta, el binario desplegado puede ser demasiado antiguo para la vinculación de Pascal que lo llama, o dos hilos pueden competir para vincularlo al mismo tiempo. Cada uno tiene una solución diferente, y el objetivo principal de los diagnósticos añadidos en el trabajo del ciclo de vida de carga de la versión 2.11.0 es indicarle a cuál de ellos se enfrenta realmente

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

El error 193 de Windows, ERROR_BAD_EXE_FORMAT, significa que el archivo se encontró y abrió, pero su tipo de máquina PE no coincide con el proceso anfitrión. Un ejecutable de 64 bits no puede cargar una 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 le envía exactamente en la dirección equivocada

PDFium Component intercepta este caso en CheckLoadLibrary: cuando LoadLibrary devuelve un controlador nulo y GetLastError es 193, añade una sugerencia de que la DLL se encontró pero su arquitectura no coincide con la del proceso anfitrión, y apunta a la compilación correspondiente en DLLs/Win32 o DLLs/Win64. El subdirectorio es elegido por BuildDllSubDir, que devuelve Win64 cuando IsWin64 is true y Win32 en caso contrario. Despliegue la DLL bajo la estructura que la vinculación realmente busca y el conflicto desaparecerá

uses
  SysUtils, PDFium;

procedure OpenDocument(const AFileName: string);
var
  Pdf: TPdf;
begin
  // Despliega pdfium.dll junto al ejecutable, acorde con el objetivo de compilación:
  //   <AppDir>\DLLs\Win32\pdfium.dll   para un proceso anfitrión de 32 bits
  //   <AppDir>\DLLs\Win64\pdfium.dll   para un proceso anfitrión de 64 bits
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := AFileName;
    try
      Pdf.Active := True;   // el primer uso vincula pdfium.dll de forma diferida
    except
      on E: EPdfError do
        // El mensaje ya nombra la causa real: un desajuste de 32/64 bits
        // (ERROR_BAD_EXE_FORMAT) o una pdfium.dll más antigua que esta vinculación
        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 una exportación faltante de PDFium?

La segunda clase de fallo es la discrepancia de versiones. PDFium publica nuevas exportaciones a lo largo del tiempo, y la vinculación de Pascal resuelve cada función que necesita a través de CheckGetProcAddress durante LoadLibrary. Si una exportación requerida no está presente, la vinculación es más reciente que la pdfium.dll en el disco, y el diagnóstico honesto es que la DLL desplegada está obsoleta en lugar de corrupta. La función CheckGetProcAddress indica precisamente eso: genera un EPdfError informando que la pdfium.dll desplegada es más antigua que esta compilación de la vinculación PDFiumPas, e indica la ruta DLLs/<subdir> donde corresponde colocar el binario coincidente

Dos detalles hacen que esa ruta sea sólida. En primer lugar, CheckGetProcAddress llama a UnloadLibrary antes de generar el error, por lo que una vinculación parcial nunca deja punteros de función medio resueltos para que el próximo intento tropiece con ellos. En segundo lugar, 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á configurada. Si su aplicación habilita el motor de JavaScript V8 para formularios XFA o con scripts, el texto del error apunta a la compilación V8, no a la normal, para que reemplace el archivo correcto a la primera

¿Es seguro cargar la DLL PDFium desde un hilo de fondo?

Cargar la biblioteca ahora es seguro desde cualquier hilo; llamar a la API de PDFium desde varios hilos todavía no lo es. Esas son dos garantías separadas, y mantenerlas separadas representa la diferencia entre un grupo de trabajadores estable y un bloqueo intermitente. El renderizado en segundo plano normalmente ejecuta cada renderizado de página a través de un trabajador TPdfFuture<T>, y el primer futuro que interactúa con TPdf es lo que desencadena la vinculación diferida. Sin protección, dos trabajadores podrían observar una biblioteca no cargada, ejecutar la secuencia de vinculación, sobrescribir el controlador del módulo y filtrar la primera carga

Lo que el bloqueo de carga no protege

El trabajo de la versión 2.11.0 cierra esa ventana de oportunidad con PDFiumLoadLock, una TRTLCriticalSection global del proceso que envuelve la entrada tanto a LoadLibrary como a UnloadLibrary. La sección crítica hace que la secuencia de comprobación y posterior carga sea atómica, por lo que no puede ocurrir que dos hilos vean Loaded=False y realicen una doble vinculación, y ningún hilo puede liberar la DLL mientras otro está a mitad del proceso. El bloqueo se crea en la sección de initialization de la unidad y se destruye en finalization, protegido por una bandera PDFiumLoadLockReady para que el emparejamiento sea seguro incluso durante el apagado. Si necesita el patrón completo de trabajador y respuesta con cancelación, el artículo complementario sobre renderizado en segundo plano con futuros cancelables lo detalla de principio a fin

uses
  PDFium, FPdfAsync;

// El método worker se ejecuta en un hilo en segundo plano. El primer futuro que vincula
// pdfium.dll se serializa mediante PDFiumLoadLock, así que un segundo worker concurrente
// no puede vincular dos veces ni filtrar el identificador del módulo.
function TReportForm.RenderThumbnail(
  const AToken: IPdfCancellationToken): TBitmap;
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Active := True;         // vinculación concurrente segura
    AToken.ThrowIfCancelled;
    Result := Pdf.Thumbnail;    // este TPdf pertenece solo a un hilo
  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;

Vale la pena exponer claramente el límite aquí, porque es fácil malinterpretar la solución. PDFiumLoadLock serializa únicamente el estado de vinculación global: el controlador del módulo y las asignaciones de punteros de función FPDF_*. La API C de PDFium subyacente sigue sin ser segura para hilos, tal como declaran los encabezados originales, por lo que llamar a las funciones FPDF_* o compartir un FPDF_DOCUMENT entre hilos sigue siendo su responsabilidad serializar. La estructura segura es la anterior: cada trabajador posee su propio 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 a su alrededor, consulte protección de la vinculación PDFium VCL contra fallos de memoria y ABI

Por qué son importantes la finalización y FPDF_DestroyLibrary

Un vacío más sutil se encontraba en la ruta de apagado. La unidad PDFium no tenía una sección de 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, concretamente la unión del hilo de trabajo, la liberación del entorno aislado (isolate) de V8 cuando se configura EnableV8Engine, y el desmontaje de fuentes y caché. En una carga de trabajo de renderizado simple, esto representa una fuga silenciosa en el desmontaje, pero con el motor V8 habilitado, filtra un entorno aislado de JavaScript en cada ejecución, lo que se nota rápidamente bajo una automatización repetida

La sección de finalization ahora llama a UnloadLibrary, que invoca a FPDF_DestroyLibrary y libera el módulo. Esto reside en el ámbito de la unidad por una razón: TPdf.Destroy solo establece Active := False to close el documento actual, y deliberadamente no descarga la biblioteca global, de modo que una aplicación de múltiples instancias mantiene una vinculación de PDFium compartida activa durante toda la vida del proceso. Colocar el desmontaje en la sección de finalization significa que la biblioteca compartida se libera exactamente una vez, cuando la unidad se descarga, y UnloadLibrary comprueba primero su bandera Loaded para que la llamada sea una instrucción inactiva inofensiva si PDFium nunca se utilizó

Una breve lista de comprobación de despliegue

Tres hábitos evitan casi cualquier fallo de carga en el entorno real. Haga coincidir la arquitectura de la DLL con el proceso anfitrión y distribúyala bajo DLLs/Win32 or DLLs/Win64; mantenga la pdfium.dll sincronizada con la versión de la vinculación para que no falte ninguna exportación requerida; y nunca comparta un TPdf o un controlador de documento entre hilos, aunque la vinculación en sí sea ahora atómica. Si compila el mismo código fuente tanto para Delphi como para Lazarus, las diferencias de empaquetado que complican el despliegue multiplataforma se detallan en las notas sobre dificultades del compilador cruzado Delphi y FPC. Los diagnósticos, el bloqueo de carga y la limpieza de finalización descritos aquí se distribuyen en PDFium Component para Delphi y C++Builder