Artigo Técnico

Carregando a biblioteca nativa PDFium em qualquer alvo

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

Diagrama da cadeia de busca da biblioteca nativa PDFium para Delphi, do subdiretório de alvo DLLs pelos layouts alternativo, plano e de diretório do sistema Windows até o loader da plataforma
Quatro locais explícitos são sondados em ordem antes de se pedir ao loader do SO que busque 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

Como uma letra maiúscula na macro de alvo do FPC quebra a busca da DLL PDFium no Linux: o loader procura DLLs/x86_64-Linux enquanto a pasta implantada é DLLs/x86_64-linux, que só bate no Windows insensível a caixa
O mesmo alvo grafado de dois jeitos bate no Windows e erra em silêncio em um sistema de arquivos sensível a caixa

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

Colisão de nome UnloadLibrary no componente PDFium: uma chamada não qualificada na unidade de binding Delphi recursiona para si mesma, enquanto uma chamada qualificada pela unidade chega à unidade de loader portátil e libera o handle
Qualificar o call site manda a liberação pela unidade de loader em vez de recursionar para dentro da unidade de binding

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