Artículo técnico

Cargar la biblioteca nativa de PDFium en cualquier destino

El componente PDFium encuentra su biblioteca nativa a través de 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 podéis 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 el artículo entero, porque la causa era una letra mayúscula y el síntoma era silencio

La cadena, en orden

Cuatro ubicaciones, probadas en secuencia, y después el cargador de la plataforma como último recurso. Primera, la disposición preferida: un directorio DLLs junto al ejecutable que contiene un subdirectorio por destino. Segunda, una disposición alternativa con el subdirectorio de destino directamente junto al ejecutable. Tercera, la disposición plana legada, la biblioteca al lado del ejecutable sin subdirectorio alguno. Cuarta, solo en Windows, el directorio del sistema, que requiere cuidado porque un proceso de 32 bits debe mirar en SysWOW64 y uno de 64 bits en System32, y en Windows de 32 bits la primera no existe, así que la búsqueda tiene que caer atrás. 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 PDFium para Delphi, desde el subdirectorio de destino DLLs por las disposiciones alternativa, plana y de directorio del sistema Windows hasta el cargador de la plataforma
Se sondean cuatro ubicaciones explícitas en orden antes de pedirle al cargador del SO que busque por su cuenta

Fuera de Windows no hay deliberadamente ningún paso de directorio del sistema. La propia ruta de búsqueda del cargador de la plataforma, movida por la configuración del linker en tiempo de ejecución y las variables de 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 Windows se cubre por separado en despliegue de la DLL 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 bitness del proceso en ejecución y no el del sistema operativo, porque eso es lo que determina qué binario puede cargarse. En todas las demás partes 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 modo que la carpeta que contiene la biblioteca nativa queda 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 ficheros
  // sensible a mayúsculas esa diferencia es toda la búsqueda
  Result := LowerCase({$I %FPCTARGETCPU%} + '-' + {$I %FPCTARGETOS%});
{$ENDIF}
end;

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

La macro del compilador escribe el sistema operativo de destino con inicial mayúscula: Win64, Linux, Darwin. El paquete de Lazarus escribe su salida de 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 ficheros 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 está instalada a nivel de sistema, y a veces no, y en cualquier caso el árbol de despliegue cuidadosamente dispuesto no aporta nada. El fallo no tiene mensaje de error porque nada falló: cada paso informó correctamente de que el fichero no estaba donde miró

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

El programa sonda, compilado y ejecutado

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

Aquí no funciona. La unidad principal de enlace es muy grande y arrastra la LCL, así que no puede copiarse y compilarse sencillamente con el símbolo Windows desactivado. Así que, en su lugar, el puñado de funciones que tocó el cambio se transcribió verbatim a un pequeño programa autocontenido, y ese programa se ejecutó. Imprimió x86_64-Win64, y la discrepancia quedó visible en una línea de salida. Compilar el mismo programa no os 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. La cuestión es mirar el valor al que una macro
  // se expande realmente 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 atañe al valor de algo y no a su tipo, la verificación de solo compilación no es verificación. Imprimidlo. 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

Dejad 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 resuelve, se mapean a códigos de error que merecen nombrarse 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 volver a derivar categorías de un número de error que significa cosas distintas en sistemas distintos

Resistir el impulso de normalizar esas dos en un único 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 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 con enteros que hacen sombra a las de coma 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 punto de llamada es el arreglo que no depende de que alguien conserve ese orden después

Colisión de nombres UnloadLibrary en el componente PDFium: una llamada sin calificar en la unidad de enlace Delphi recursa sobre sí misma, mientras que una llamada calificada con la unidad llega a la unidad de cargador portable y libera el handle
Calificar el punto de llamada envía la liberación a través de la unidad de cargador en lugar de recursar en la unidad de enlace

Lista de comprobación de despliegue

Tres cosas explican la mayoría de los fallos de carga una vez que la aritmética de rutas es correcta. 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 fichero distinto, así que un despliegue que los mezcle parecerá 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 explícita de subdirectorios antes que instalar nada a nivel de sistema

Para Lazarus en concreto, poned la biblioteca nativa bajo DLLs/<cpu>-<os> en minúsculas, junto al ejecutable, y la encontrará el primer paso de la cadena en todos los destinos. El ejemplo 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 de producto de PDFium Delphi component