O componente PDFium encontra sua biblioteca nativa por uma cadeia de busca fixa e ordenada em vez de deixá-lo ao loader do sistema operacional, porque uma árvore de implantação explícita é uma árvore de implantação que você pode depurar. No Windows essa cadeia procura um subdiretório Win32 ou Win64 que o instalador já distribui. Em outros alvos ela constrói o nome do subdiretório a partir das macros de alvo do Free Pascal, como <cpu>-<os>, para que a árvore de implantação se leia exatamente como a árvore de unidades compiladas. Essa última decisão introduziu um bug que vale o artigo inteiro, porque a causa era uma letra maiúscula e o sintoma era silêncio
A cadeia, em ordem
Quatro locais, tentados em sequência, então o loader da plataforma como último recurso. Primeiro o layout preferido, um diretório DLLs ao lado do executável contendo um subdiretório por alvo. Segundo um layout alternativo com o subdiretório de alvo diretamente ao lado do executável. Terceiro o layout legado plano, a biblioteca ao lado do executável sem subdiretório algum. Quarto, só no Windows, o diretório do sistema, que exige cuidado porque um processo de 32 bits deve olhar em SysWOW64 e um de 64 bits em System32, e no Windows de 32 bits o primeiro não existe, então a busca precisa recuar. Só depois de tudo isso o loader é pedido para buscar por conta própria
Deliberadamente não há passo de diretório do sistema fora do Windows. O próprio caminho de busca do loader da plataforma, dirigido pela configuração do linker de runtime e pelo ambiente de caminho de bibliotecas, já cobre esse terreno, e duplicá-lo em Pascal significaria reimplementar regras que variam por distribuição. Diagnosticar falhas na cadeia Windows é coberto separadamente em implantar a DLL PDFium e diagnosticar falhas de carregamento
De onde vem o nome do subdiretório
No Windows é Win32 ou Win64, decidido pelo bitness do processo em execução em vez do do sistema operacional, porque é isso que determina qual binário pode ser carregado. Em todo o resto o nome é construído a partir das macros de alvo do compilador para que uma máquina compilando para duas arquiteturas produza duas árvores claramente separadas, e para que a pasta que guarda a biblioteca nativa fique ao lado da pasta que guarda as unidades compiladas com o mesmo nome
function BuildDllSubDir(UseV8: Boolean): string;
begin
{$IFDEF MSWINDOWS}
if IsWin64 then
Result := 'Win64'
else
Result := 'Win32';
{$ELSE}
// As macros do compilador capitalizam o SO ("Linux", "Darwin") enquanto
// o diretório de saída de unidades do pacote não, então os dois só
// coincidem depois de dobrar a caixa. Em um sistema de arquivos
// sensível a caixa essa diferença é a busca inteira
Result := LowerCase({$I %FPCTARGETCPU%} + '-' + {$I %FPCTARGETOS%});
{$ENDIF}
end;
Por que uma letra maiúscula quebrou a cadeia inteira
A macro do compilador escreve o sistema operacional alvo com inicial maiúscula: Win64, Linux, Darwin. O pacote Lazarus escreve sua saída de unidades em um diretório nomeado a partir de sua própria variável de alvo, que é minúscula: win64, linux, darwin. Duas grafias da mesma coisa, e sem como notar no Windows, onde o sistema de arquivos não as distingue
No Linux são dois diretórios diferentes. Uma implantação que põe o objeto compartilhado em DLLs/x86_64-linux é invisível para um loader procurando DLLs/x86_64-Linux, então todos os quatro passos explícitos da cadeia erram e o código recua para deixar o loader da plataforma buscar. Às vezes isso funciona, se a biblioteca por acaso estiver instalada no sistema todo, e às vezes não, e de qualquer jeito a árvore de implantação cuidadosamente arrumada não contribui com nada. A falha não tem mensagem de erro porque nada falhou: cada passo reportou corretamente que o arquivo não estava onde olhou
O programa de sonda, compilado e executado
Essa classe de bug não pode ser encontrada lendo, e também não pode ser encontrada compilando. A técnica usual para verificar um ramo de plataforma que nunca compila na máquina de desenvolvimento é copiar a unidade para um diretório temporário, renomeá-la, substituir o condicional de plataforma por um símbolo que nunca é definido e compilar a cópia; se compila, a cláusula uses e as assinaturas de chamada naquele caminho são ao menos autocoerentes. Isso funciona bem para uma unidade autossuficiente
Aqui não funciona. A unidade principal de binding é muito grande e puxa a LCL, então não pode simplesmente ser copiada e compilada com o símbolo Windows desligado. Então em vez disso o punhado de funções que a mudança tocou foi transcrito verbatim em um pequeno programa autossuficiente, e esse programa foi executado. Ele imprimiu x86_64-Win64, e a incompatibilidade ficou visível em uma linha de saída. Compilar o mesmo programa não teria dito nada, porque a string é perfeitamente válida; só seu valor está errado
program ProbeSubDir;
{$MODE DELPHI}
uses
SysUtils;
begin
// Imprima, não afirme. O ponto é olhar o valor que uma macro de fato
// expande nesta toolchain
Writeln('raw: ', {$I %FPCTARGETCPU%}, '-', {$I %FPCTARGETOS%});
Writeln('folded: ', LowerCase({$I %FPCTARGETCPU%} + '-' +
{$I %FPCTARGETOS%}));
end.
A lição geral: quando uma mudança entre plataformas concerne o valor de algo em vez de seu tipo, verificação só de compilação não é verificação. Imprima-o. O conjunto mais amplo de diferenças entre compiladores Delphi e Free Pascal está reunido em o artigo de armadilhas entre compiladores Delphi e FPC
Deixe a plataforma explicar suas próprias falhas de carga
O ramo Windows do loader enumera à mão as razões pelas quais uma carga pode falhar, porque as distinções úteis ali, uma incompatibilidade de arquitetura, uma dependência transitiva ausente, um caminho que não resolve, mapeiam para códigos de erro que valem ser nomeados individualmente. Fora do Windows a unidade de loader portátil já retorna uma string descritiva que cobre o mesmo terreno, então o ramo não Windows a usa diretamente em vez de rederivar categorias de um número de erro que significa coisas diferentes em sistemas diferentes
Resistir ao impulso de normalizar esses dois em uma mensagem é deliberado. Uma falha de carga é um problema de implantação, e a pessoa lendo a mensagem precisa do vocabulário da própria plataforma para pesquisá-la
Uma colisão de nomes que entra em recursão
Mais uma armadilha, pequena e afiada. A unidade de loader portátil exporta um procedimento chamado UnloadLibrary, e a unidade de binding tem um procedimento de mesmo nome que faz sua própria contabilidade antes de liberar o handle. Dentro desse procedimento, uma chamada não qualificada a UnloadLibrary resolve para a da unidade atual, que chama a si mesma. A correção é qualificar a chamada com o nome da unidade
Essa é a mesma forma dos problemas de sombreamento de identificadores que dominam ports para Free Pascal em geral: a unidade Windows exporta funções de mínimo e máximo tipadas como inteiro que sombreiam as de ponto flutuante, e um tipo de sincronização que sombreia a classe de mesmo nome, e em todo caso a resolução depende da ordem da cláusula uses. Qualificar o call site é a correção que não depende de alguém preservar essa ordem depois
Checklist de implantação
Três coisas respondem pela maioria das falhas de carga quando a aritmética de caminhos está certa. A arquitetura precisa bater com o processo, não com a máquina, então um aplicativo de 32 bits no Windows de 64 bits precisa do binário de 32 bits. O build com V8 tem um nome de arquivo diferente, então uma implantação que os mistura parecerá correta e não carregará nada. E só uma variante pode viver em um diretório do sistema por vez, o que é uma boa razão para preferir o layout explícito de subdiretórios em vez de instalar algo no sistema todo
Para o Lazarus especificamente, ponha a biblioteca nativa sob DLLs/<cpu>-<os> em minúsculas, ao lado do executável, e ela será encontrada pelo primeiro passo da cadeia em todo alvo. O exemplo de visualizador que exercita isso no Lazarus é descrito em o artigo do visualizador Lazarus e FPC, e o suporte atual de plataformas está listado na página de produto do PDFium Delphi component