Artigo Técnico

Carregar a biblioteca nativa do PDFium em qualquer alvo

O componente PDFium encontra a sua biblioteca nativa através de uma cadeia de procura fixa e ordenada em vez de o deixar ao carregador do sistema operativo, porque uma árvore de implantação explícita é uma árvore de implantação que se pode depurar. No Windows essa cadeia procura um subdiretório Win32 ou Win64 que o instalador já distribui. Nos outros alvos constrói o nome do subdiretório a partir das macros de alvo do Free Pascal, como <cpu>-<os>, pelo que a árvore de implantação se lê exatamente como a árvore de unidades compiladas. Essa última decisão introduziu um bug que vale o artigo inteiro, porque a causa era uma maiúscula e o sintoma era silêncio

A cadeia, pela ordem

Quatro lugares, tentados em sequência, e depois o carregador da plataforma como último recurso. Primeiro a disposição preferida, um diretório DLLs ao lado do executável contendo um subdiretório por alvo. Segundo uma disposição alternativa com o subdiretório de alvo diretamente ao lado do executável. Terceiro a disposição legada plana, a biblioteca ao lado do executável sem subdiretório nenhum. Quarto, só no Windows, o diretório de sistema, que precisa de cuidado porque um processo de 32 bits tem de procurar em SysWOW64 e um de 64 bits em System32, e no Windows de 32 bits o primeiro não existe, pelo que a procura tem de recuar. Só depois de tudo isso se pede ao carregador que procure por conta própria

Diagrama da cadeia de procura da biblioteca nativa PDFium para Delphi, do subdiretório de alvo DLLs através das disposições alternativa, plana e de diretório de sistema Windows até ao carregador da plataforma
Quatro lugares explícitos são sondados pela ordem antes de se pedir ao carregador do SO que procure por conta própria

Fora do Windows não há deliberadamente nenhum passo de diretório de sistema. O caminho de procura do próprio carregador da plataforma, dirigido pela configuração do linker de runtime e pelas variáveis de 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 está coberto separadamente em implantação da DLL PDFium e diagnóstico de falhas de carregamento

De onde vem o nome do subdiretório

No Windows é Win32 ou Win64, decidido pela largura de bits do processo em execução e não pela do sistema operativo, porque é isso que determina que 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 que compile 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 escrevem o SO com maiúscula inicial
  // ("Linux", "Darwin") enquanto o diretório de saída de unidades do
  // pacote não o faz, pelo que os dois só coincidem depois de dobrar
  // a caixa. Num sistema de ficheiros sensível a maiúsculas essa
  // diferença é a procura inteira
  Result := LowerCase({$I %FPCTARGETCPU%} + '-' + {$I %FPCTARGETOS%});
{$ENDIF}
end;

Porque é que uma maiúscula partiu a cadeia inteira

A macro do compilador escreve o sistema operativo alvo com maiúscula inicial: Win64, Linux, Darwin. O pacote Lazarus escreve a sua saída de unidades num diretório nomeado a partir da sua própria variável de alvo, que é minúscula: win64, linux, darwin. Duas grafias da mesma coisa, e nenhuma forma de dar por isso no Windows, onde o sistema de ficheiros as não distingue

No Linux são dois diretórios diferentes. Uma implantação que ponha o objeto partilhado em DLLs/x86_64-linux é invisível para um carregador que procure DLLs/x86_64-Linux, pelo que todos os quatro passos explícitos da cadeia falham e o código cai para deixar o carregador da plataforma procurar. Às vezes isso funciona, se a biblioteca por acaso estiver instalada em todo o sistema, e às vezes não, e de qualquer forma a árvore de implantação cuidadosamente arranjada não contribui com nada. A falha não tem mensagem de erro porque nada falhou: cada passo reportou corretamente que o ficheiro não estava onde procurou

Como uma maiúscula na macro de alvo do FPC parte a procura da DLL PDFium no Linux: o carregador procura DLLs/x86_64-Linux enquanto a pasta implantada é DLLs/x86_64-linux, que só coincide no Windows insensível a maiúsculas
O mesmo alvo escrito de duas formas coincide no Windows e falha em silêncio num sistema de ficheiros sensível a maiúsculas

O programa de sondagem, compilado e corrido

Esta classe de bug não se encontra lendo, e também não se encontra compilando. A técnica habitual 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 a condicional de plataforma por um símbolo nunca definido, e compilar a cópia; se compilar, a cláusula uses e as assinaturas de chamada nesse caminho são pelo menos autoconsistentes. Isso funciona bem para uma unidade autónoma

Aqui não funciona. A unidade principal de vinculação é muito grande e puxa a LCL, pelo que não pode simplesmente ser copiada e compilada com o símbolo Windows desligado. Em vez disso, o punhado de funções que a alteração tocou foi transcrito verbatim para um pequeno programa autónomo, e esse programa foi corrido. Imprimiu x86_64-Win64, e a discrepância era visível numa linha de saída. Compilar o mesmo programa não lhe teria dito nada, porque a string é perfeitamente válida; só o seu valor está errado

program ProbeSubDir;
{$MODE DELPHI}
uses
  SysUtils;
begin
  // Imprimir, não afirmar. O ponto é ver o valor a que uma macro
  // realmente se expande nesta toolchain
  Writeln('raw:    ', {$I %FPCTARGETCPU%}, '-', {$I %FPCTARGETOS%});
  Writeln('folded: ', LowerCase({$I %FPCTARGETCPU%} + '-' +
    {$I %FPCTARGETOS%}));
end.

A lição geral: quando uma alteração multiplataforma concerne o valor de alguma coisa em vez do seu tipo, verificação só por compilação não é verificação. Imprima-o. O conjunto mais amplo de diferenças entre compiladores entre Delphi e Free Pascal está recolhido no artigo sobre armadilhas de compilação cruzada Delphi e FPC

Deixar a plataforma explicar as suas próprias falhas de carregamento

O ramo Windows do carregador enumera à mão as razões pelas quais um carregamento pode falhar, porque as distinções úteis aí, uma discrepância de arquitetura, uma dependência transitiva em falta, um caminho que não resolve, mapeiam para códigos de erro que valem a pena nomear individualmente. Fora do Windows a unidade de carregamento portátil já devolve uma string descritiva que cobre o mesmo terreno, pelo que o ramo não Windows usa-a diretamente em vez de re-derivar categorias de um número de erro que significa coisas diferentes em sistemas diferentes

Resistir ao impulso de normalizar os dois numa única mensagem é deliberado. Uma falha de carregamento é um problema de implantação, e quem lê a mensagem precisa do vocabulário próprio da plataforma para a pesquisar

Uma colisão de nomes que recorre

Mais uma armadilha, pequena e aguda. A unidade de carregamento portátil exporta um procedimento chamado UnloadLibrary, e a unidade de vinculação tem um procedimento do mesmo nome que faz a sua própria contabilidade antes de libertar o handle. Dentro desse procedimento, uma chamada não qualificada a UnloadLibrary resolve à da unidade atual, que se chama a si própria. A correção é qualificar a chamada com o nome da unidade

É a mesma forma que os problemas de sombreamento de identificadores que dominam os ports Free Pascal em geral: a unidade Windows exporta funções de mínimo e máximo de tipo inteiro que fazem sombra às de vírgula flutuante, e um tipo de sincronização que faz sombra à classe do mesmo nome, e em todos os casos a resolução depende da ordem da cláusula uses. Qualificar o ponto de chamada é a correção que não depende de alguém preservar essa ordem mais tarde

Colisão de nomes UnloadLibrary no componente PDFium: uma chamada não qualificada na unidade de vinculação Delphi recorre a si própria, enquanto uma chamada qualificada pela unidade chega à unidade de carregamento portátil e liberta o handle
Qualificar o ponto de chamada envia a libertação através da unidade de carregamento em vez de recorrer à unidade de vinculação

Lista de verificação de implantação

Três coisas explicam a maioria das falhas de carregamento uma vez que a aritmética de caminhos está certa. A arquitetura tem de corresponder ao processo, não à máquina, pelo que uma aplicação de 32 bits no Windows de 64 bits precisa do binário de 32 bits. A compilação com V8 tem um nome de ficheiro diferente, pelo que uma implantação que as misture parecerá correta e não carregará nada. E só uma variante pode viver num diretório de sistema de cada vez, que é uma boa razão para preferir a disposição explícita de subdiretórios a instalar qualquer coisa em todo o sistema

Para o Lazarus especificamente, ponha a biblioteca nativa sob DLLs/<cpu>-<os> em minúsculas, ao lado do executável, e será encontrada pelo primeiro passo da cadeia em todos os alvos. O exemplo de visualizador que exercita isto no Lazarus está descrito no artigo sobre o visualizador Lazarus e FPC, e o suporte atual de plataformas está listado na página de produto do PDFium Delphi component