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
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ó
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
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