Artículo técnico

Cargar la biblioteca nativa de PDFium en cualquier destino

El componente PDFium encuentra su biblioteca nativa mediante una cadena de búsqueda fija y ordenada en lugar de dejarlo al cargador del sistema operativo, porque un árbol de despliegue explícito es un árbol de despliegue que pueden depurar. En Windows esa cadena busca un subdirectorio Win32 o Win64 que el instalador ya distribuye. En otros destinos construye el nombre del subdirectorio a partir de las macros de destino de Free Pascal, como <cpu>-<os>, de modo que el árbol de despliegue se lee exactamente igual que el árbol de unidades compiladas. Esa última decisión introdujo un error que merece todo el artículo, porque la causa fue una letra mayúscula y el síntoma fue silencio

La cadena, en orden

Cuatro ubicaciones, probadas en secuencia, y luego el cargador de la plataforma como último recurso. Primero la disposición preferida, un directorio DLLs junto al ejecutable que contiene un subdirectorio por destino. Segundo una disposición alternativa con el subdirectorio de destino directamente junto al ejecutable. Tercero la disposición plana antigua, la biblioteca sentada junto al ejecutable sin subdirectorio alguno. Cuarto, solo en Windows, el directorio del sistema, que necesita cuidado porque un proceso de 32 bits debe mirar en SysWOW64 y uno de 64 bits en System32, y en Windows de 32 bits el primero no existe, así que la búsqueda tiene que replegarse. Solo después de todo eso se le pide al cargador que busque por su cuenta

Diagrama de la cadena de búsqueda de la biblioteca nativa de PDFium para Delphi, desde el subdirectorio de destino DLLs por la disposición alternativa, plana y del directorio del sistema de Windows hasta el cargador de la plataforma
Cuatro ubicaciones explícitas se sondean en orden antes de pedirle al cargador del SO que busque por su cuenta

Fuera de Windows deliberadamente no hay paso de directorio del sistema. La propia ruta de búsqueda del cargador de la plataforma, guiada por la configuración del enlazador de runtime y el entorno de rutas de bibliotecas, ya cubre ese terreno, y duplicarla en Pascal significaría reimplementar reglas que varían por distribución. El diagnóstico de fallos en la cadena de Windows se cubre aparte en despliegue de la DLL de PDFium y diagnóstico de fallos de carga

De dónde sale el nombre del subdirectorio

En Windows es Win32 o Win64, decidido por el ancho de bits del proceso en ejecución y no por el del sistema operativo, porque eso es lo que determina qué binario se puede cargar. En todos los demás lugares el nombre se construye a partir de las macros de destino del compilador, de modo que una máquina que compila para dos arquitecturas produce dos árboles claramente separados, y de que la carpeta que aloja la biblioteca nativa se siente junto a la carpeta de unidades compiladas con el mismo nombre

function BuildDllSubDir(UseV8: Boolean): string;
begin
{$IFDEF MSWINDOWS}
  if IsWin64 then
    Result := 'Win64'
  else
    Result := 'Win32';
{$ELSE}
  // Las macros del compilador ponen mayúscula al SO ("Linux", "Darwin")
  // mientras que el directorio de salida de unidades del paquete no, así
  // que ambos coinciden solo tras plegar a minúsculas. En un sistema de
  // archivos sensible a mayúsculas esa diferencia es toda la búsqueda
  Result := LowerCase({$I %FPCTARGETCPU%} + '-' + {$I %FPCTARGETOS%});
{$ENDIF}
end;

Por qué una letra mayúscula rompió toda la cadena

La macro del compilador escribe el sistema operativo de destino con mayúscula inicial: Win64, Linux, Darwin. El paquete de Lazarus escribe la salida de sus unidades en un directorio nombrado a partir de su propia variable de destino, que va en minúsculas: win64, linux, darwin. Dos grafías de lo mismo, y sin forma de notarlo en Windows, donde el sistema de archivos no las distingue

En Linux son dos directorios distintos. Un despliegue que pone el objeto compartido en DLLs/x86_64-linux es invisible para un cargador que busca DLLs/x86_64-Linux, así que los cuatro pasos explícitos de la cadena fallan y el código cae a dejar que el cargador de la plataforma busque. A veces eso funciona, si la biblioteca casualmente está instalada en todo el sistema, y a veces no, y de cualquier forma el árbol de despliegue cuidadosamente arreglado no aporta nada. El fallo no tiene mensaje de error porque nada falló: cada paso reportó correctamente que el archivo no estaba donde miró

Cómo una letra mayúscula en la macro de destino de FPC rompe la búsqueda de la DLL de PDFium en Linux: el cargador busca DLLs/x86_64-Linux mientras la carpeta desplegada es DLLs/x86_64-linux, que solo coincide en Windows sin distinción de mayúsculas
El mismo destino escrito de dos formas coincide en Windows y falla en silencio en un sistema de archivos sensible a mayúsculas

El programa de sondeo, compilado y ejecutado

Esta clase de error no se encuentra leyendo, y tampoco compilando. La técnica usual para verificar una rama de plataforma que nunca compila en la máquina de desarrollo es copiar la unidad a un directorio temporal, renombrarla, reemplazar el condicional de plataforma por un símbolo que nunca se define y compilar la copia; si compila, la cláusula uses y las firmas de llamada de esa vía son al menos autoconsistentes. Eso funciona bien para una unidad autocontenida

Aquí no funciona. La unidad de enlace principal es muy grande y arrastra la LCL, así que no puede copiarse y compilarse simplemente con el símbolo de Windows apagado. En su lugar, el puñado de funciones que el cambio tocó se transcribió verbatim a un programa pequeño autocontenido, y ese programa se ejecutó. Imprimió x86_64-Win64, y el desajuste quedó visible en una línea de salida. Compilar el mismo programa no les habría dicho nada, porque la cadena es perfectamente válida; solo su valor está mal

program ProbeSubDir;
{$MODE DELPHI}
uses
  SysUtils;
begin
  // Imprimir, no afirmar. El punto es mirar el valor al que una macro
  // realmente se expande en esta toolchain
  Writeln('raw:    ', {$I %FPCTARGETCPU%}, '-', {$I %FPCTARGETOS%});
  Writeln('folded: ', LowerCase({$I %FPCTARGETCPU%} + '-' +
    {$I %FPCTARGETOS%}));
end.

La lección general: cuando un cambio multiplataforma concierne al valor de algo y no a su tipo, la verificación de solo compilación no es verificación. Imprímanlo. El conjunto más amplio de diferencias entre compiladores de Delphi y Free Pascal está reunido en el artículo de trampas entre compiladores de Delphi y FPC

Dejen que la plataforma explique sus propios fallos de carga

La rama Windows del cargador enumera a mano las razones por las que una carga puede fallar, porque las distinciones útiles allí, un desajuste de arquitectura, una dependencia transitiva ausente, una ruta que no se resuelve, se corresponden con códigos de error que vale nombrar uno por uno. Fuera de Windows la unidad de cargador portable ya devuelve una cadena descriptiva que cubre el mismo terreno, así que la rama no Windows la usa directamente en lugar de rederivar categorías de un número de error que significa cosas distintas en sistemas distintos

Resistir el impulso de normalizar esas dos en un solo mensaje es deliberado. Un fallo de carga es un problema de despliegue, y quien lee el mensaje necesita el vocabulario propio de la plataforma para buscarlo

Una colisión de nombres que recursa

Una trampa más, pequeña y afilada. La unidad de cargador portable exporta un procedimiento llamado UnloadLibrary, y la unidad de enlace tiene un procedimiento del mismo nombre que hace su propia contabilidad antes de liberar el handle. Dentro de ese procedimiento, una llamada sin calificar a UnloadLibrary se resuelve a la de la unidad actual, que se llama a sí misma. El arreglo es calificar la llamada con el nombre de la unidad

Es la misma forma que los problemas de sombreado de identificadores que dominan los ports a Free Pascal en general: la unidad Windows exporta funciones de mínimo y máximo tipadas entero que hacen sombra a las de punto flotante, y un tipo de sincronización que hace sombra a la clase del mismo nombre, y en cada caso la resolución depende del orden de la cláusula uses. Calificar el sitio de llamada es el arreglo que no depende de que alguien preserve ese orden después

Colisión de nombres de UnloadLibrary en el componente PDFium: una llamada sin calificar en la unidad de enlace de Delphi recursa sobre sí misma, mientras que una llamada calificada por unidad llega a la unidad de cargador portable y libera el handle
Calificar el sitio de llamada manda la liberación por la unidad del cargador en lugar de recursar dentro de la unidad de enlace

Lista de verificación de despliegue

Tres cosas explican la mayoría de los fallos de carga una vez que la aritmética de rutas está bien. La arquitectura debe coincidir con el proceso, no con la máquina, así que una aplicación de 32 bits en Windows de 64 bits necesita el binario de 32 bits. La compilación con V8 tiene un nombre de archivo distinto, así que un despliegue que las mezcle se verá correcto y no cargará nada. Y solo una variante puede vivir en un directorio del sistema a la vez, lo que es una buena razón para preferir la disposición de subdirectorios explícita antes que instalar algo a nivel de sistema

Para Lazarus en concreto, pongan la biblioteca nativa bajo DLLs/<cpu>-<os> en minúsculas, junto al ejecutable, y la encontrará el primer paso de la cadena en cualquier destino. La muestra de visor que ejercita esto en Lazarus se describe en el artículo del visor Lazarus y FPC, y el soporte de plataformas actual está en la página del producto PDFium Delphi component