O PDFlibPas compila com Free Pascal para Windows de 32 bits, e a parte difícil nunca foi o Pascal: foram os arquivos objeto. Os objetos de AES e OpenJPEG que o build do Delphi linka são OMF, o linker interno do Free Pascal exige COFF, e converter entre os dois gera nomes de seção e símbolos de definição de seção que fazem o linker falhar com internal errors em vez de um diagnóstico útil
Quem já linkou objetos C em uma biblioteca Pascal conhece esse terreno. O Win64 é comparativamente civilizado: um formato de objeto, uma calling convention e nenhuma decoração de nome. O Win32 preserva todas as camadas de história que a plataforma acumulou, e uma biblioteca que linka código C de terceiros estaticamente esbarra em todas elas ao mesmo tempo
O diretório do compilador não diz qual é o target
Comece pelo entry point do build, porque errar aqui queima horas antes de qualquer arquivo objeto entrar em cena. O nome do diretório de instalação do Free Pascal identifica onde mora o compilador principal, não o que ele produz. Um compilador hospedeiro de 32 bits pode invocar um cross-compiler ao lado dele e emitir código de 64 bits quando você passa os switches de target certos, então inferir o target pelo caminho é chute que funciona por acaso até alguém reorganizar a toolchain
A abordagem confiável é perguntar ao compilador: consulte o processador e o sistema operacional de target de verdade pelos switches de informação do próprio compilador, e aceite os dois layouts de instalação comuns, o diretório binário plano e o aninhado por versão, porque instaladores e gerenciadores de toolchain diferentes produzem formatos diferentes. Um script de build que fixa um dos layouts no código funciona em exatamente uma máquina
Por que um arquivo objeto convertido quebra o linker interno?
Porque a conversão preserva a convenção de nomes de seção do OMF e sintetiza símbolos de definição de seção que não batem com o que o linker COFF espera. Converter os objetos OMF para COFF é necessário e não suficiente: os arquivos resultantes carregam os nomes de seção clássicos _TEXT, _DATA e _BSS, além de nomes de símbolos de definição de seção derivados deles, e servir isso ao linker interno do Free Pascal produz internal compiler errors em vez de uma mensagem sobre nomes de seção
Um internal error é o pior modo de falha para um problema de build, porque não diz nada sobre o que estava errado na entrada. A correção é uma passada de normalização após a conversão sobre o arquivo COFF: reescrever os nomes de seção para a forma esperada e reescrever os símbolos de definição de seção correspondentes para casar, deixando intactos o índice de símbolos, os bytes de código e as relocações. Essa última restrição é toda a dificuldade. Uma reescrita que renumera símbolos ou desloca offsets produz um objeto que linka e depois crasha
Existe um passo preliminar para um dos dois conjuntos de objetos. Os objetos OpenJPEG construídos pelo compilador C++ clássico de 32 bits dependem de rotinas helper privadas do Delphi para inteiros de 64 bits, que o Free Pascal não fornece, então nenhuma conversão de formato os torna utilizáveis. Eles são recompilados primeiro com o compilador baseado em Clang, que não emite essas dependências, e convertidos depois
// Os objetos para o target FPC vivem no próprio diretório. Eles não
// substituem o conjunto de objetos do Delphi, porque as duas toolchains
// compilam a partir da mesma árvore de fontes e cada uma precisa dos próprios inputs de link
//
// Lib\thirdparty\Win32 objetos OMF do Delphi, inalterados
// Lib\thirdparty\Win32f objetos COFF do FPC, convertidos e normalizados
//
// Entry points de build:
// build-Win32-Lib-FPC.cmd
// build-Win64-Lib-FPC.cmd
Helpers privados de compilador não são portáveis, e as convenções deles também não
A runtime do Delphi fornece trampolines em assembly para operações com inteiros de 64 bits em x86 de 32 bits, e objetos C pré-compilados feitos para o Delphi chamam essas rotinas. O Free Pascal tem o próprio arranjo, então essas referências precisam ser satisfeitas de outra forma, não redirecionadas. O detalhe que torna o redirecionamento impossível é a calling convention: o helper de timing usado pelo código de imagem tem o argumento de quatro bytes limpo pelo callee, enquanto o helper de divisão de 64 bits limpa dezesseis bytes e devolve o resultado no par de registradores clássico. Dois helpers, duas convenções, e um trampoline escrito para um corrompe a stack do outro em silêncio
A decoração de nomes adiciona a segunda metade do problema. No Win32, o Free Pascal prefixa imports C externos com um underscore automaticamente enquanto exporta declarações public name verbatim, então o lado de import e o lado de export da mesma ponte seguem regras diferentes. A ponte da C runtime que o OpenJPEG precisa, portanto, tem que exportar os nomes de símbolos C exatos, e os entry points variádicos precisam de um jump indireto de 32 bits em vez de um direto. Nada disso é exótico quando explicado. Tudo isso falha como um link error citando um símbolo que ninguém escreveu
O que fazia um executável Win32 morrer antes do main?
Uma DLL de 64 bits no path de busca, alcançada porque a unit zlib do Free Pascal faz binding dinâmico em vez de link estático. O sintoma era uma saída imediata com o status code de imagem inválida, antes de qualquer código Pascal do programa rodar, o que manda você olhar para o programa que acabou de compilar quando a falha está no loader resolvendo um import contra a arquitetura errada
A lição é sobre suposições, não sobre a zlib. Uma unit com nome de biblioteca de compressão não necessariamente contém uma; pode ser um binding que espera uma shared library em tempo de execução, e uma dependência dinâmica que você não planejou é um passivo de deployment mesmo quando por acaso resolve. Trocar pela implementação de stream em Pascal puro dá aos dois targets um caminho de compressão embutido estaticamente, sem dependência externa nenhuma, que é o que uma biblioteca embutida no aplicativo de outra pessoa deveria ter desde o início
O mesmo instinto vale para o backend do encoder JBIG2 externo. No target de 32 bits o encoder externo não é linkado, então as requisições caem no encoder Pascal embutido, e o teste que verifica isso tem de checar o estado de registro do target atual em vez de tratar um encode bem-sucedido como prova de que o backend externo está presente. Um fallback que funciona é exatamente o que esconde uma dependência ausente, que é o padrão de falha examinado em diagnóstico de falhas silenciosas de stubs. O trabalho de link estático de 64 bits está coberto em link estático do jbig2enc sob FPC
Aritmética de 32 bits em um memory stream
Código que manipula tamanhos de buffer com aritmética unsigned de largura de ponteiro está correto no Win64 e a uma imagem grande do overflow no Win32. O stream em memória que alimenta o codec JPEG 2000 cresce dobrando e avança por soma, e num target de 32 bits as duas operações podem dar wrap com inputs grandes mas totalmente legítimos
Toda escrita, skip, seek e alocação inicial, portanto, checa antes de calcular, e o teto de capacidade é o valor máximo signed de largura de ponteiro, escolhido para casar com o que a rotina de movimentação de blocos e os valores de retorno dos callbacks conseguem expressar. O requisito de comportamento quando uma requisição é recusada é fácil de errar: recusar não pode mudar a posição do stream nem o tamanho dele. Uma mutação parcial seguida de erro deixa o stream num estado sobre o qual o caller não consegue raciocinar, e a próxima operação piora tudo
// Cheque antes de calcular. No Win32 as duas dão wrap com inputs que
// uma imagem JPEG 2000 grande produz legitimamente
if Needed > NativeUInt(High(NativeInt)) - FPosition then
Exit(False); // recusa, deixa posição e tamanho em paz
NewCapacity := FCapacity;
while (NewCapacity < FPosition + Needed) do
begin
if NewCapacity > NativeUInt(High(NativeInt)) shr 1 then
Exit(False); // dobrar daria overflow
NewCapacity := NewCapacity shl 1;
end;
Duas armadilhas de output de build que sobrevivem ao port
Separar os executáveis de teste e de exemplo por arquitetura de target em diretórios de output por target é obviamente o certo e quebra imediatamente qualquer coisa que localizava os dados de teste contando níveis de diretório para cima. A correção é buscar o diretório de assets para cima em vez de assumir uma profundidade fixa, com uma restrição deliberada: o exemplo de assinatura aceita fallback de certificado apenas do próprio diretório do projeto, nunca de um ancestral qualquer, porque um certificado homônimo encontrado mais acima na árvore é uma surpresa de segurança, não uma conveniência
A segunda armadilha sobrevive a todo port e vale levar para qualquer projeto FPC. Depois de um upgrade de compilador, fazer o compilador rejeitar arquivos PPU obsoletos não basta, porque o linker ainda prefere objetos sobrando no path de busca de units mesmo quando o PPU carregado veio do diretório certo, e adicionar um object output path explícito não sobrepõe essa preferência. A única resposta confiável é um diretório temporário de units novo a cada rodada de build. Menos que isso produz um binário linkado a partir de duas versões do compilador, que falha de jeitos que parecem bugs de código-fonte
Condicionais de plataforma são a última peça, e escolher o eixo certo importa mais do que parece. A pergunta certa costuma ser se o código é específico de Windows, e não se uma biblioteca de widgets específica está presente, como o trabalho de conversão de metafiles em importação vetorial EMF e condicionais de plataforma mostrou: trocar aquele guard de uma condição de biblioteca de controles para uma condição de plataforma transformou uma suposta reescrita na mudança de uma única diretiva. O suporte a Free Pascal e Lazarus para os dois targets Windows vem com a biblioteca PDF PDFlibPas para Delphi, compilada a partir das mesmas fontes dos pacotes Delphi e C++Builder