O PDFlibPas compila sob o Free Pascal para Windows de 32 bits, e a parte difícil nunca foi o Pascal. Foram os ficheiros de objeto: os objetos AES e OpenJPEG que a build Delphi liga são OMF, o linker interno do Free Pascal exige COFF, e a conversão entre os dois produz nomes de secção e símbolos de definição de secção que fazem o linker falhar com erros internos em vez de mensagens de diagnóstico
Quem já ligou objetos C a uma biblioteca Pascal conhece este terreno. O Win64 é comparativamente civilizado, com um único formato de objeto, uma única convenção de chamada e sem name decoration. O Win32 preserva todas as camadas de história que a plataforma acumulou, e uma biblioteca que liga estaticamente código C de terceiros depara-se com todas ao mesmo tempo
O diretório do compilador não lhe diz qual é o alvo
Comece pelo ponto de entrada da build, porque errar aqui custa horas antes de qualquer ficheiro de objeto entrar em cena. Um nome de diretório de instalação do Free Pascal identifica onde vive o compilador principal, não o que ele produz. Um compilador anfitrião de 32 bits pode invocar um cross-compiler ao lado dele e emitir código de 64 bits quando lhe passa os switches de alvo certos, por isso inferir o alvo a partir de um caminho é um palpite que calha funcionar até alguém reorganizar a toolchain
A abordagem fiável é perguntar ao compilador. Consulte o processador e o sistema operativo alvo reais através dos 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 gestores de toolchain diferentes produzem formas diferentes. Um script de build que fixa um dos layouts no código funciona exatamente numa máquina
Porque é que um ficheiro de objeto convertido parte o linker interno?
Porque a conversão preserva a convenção de nomes de secção do OMF e sintetiza símbolos de definição de secção que não correspondem ao que o linker COFF espera. Converter os objetos OMF para COFF é necessário e insuficiente: os ficheiros resultantes trazem os clássicos nomes de secção _TEXT, _DATA e _BSS, mais nomes de símbolos de definição de secção derivados deles, e alimentar isso ao linker interno do Free Pascal produz erros internos do compilador em vez de uma mensagem sobre nomes de secção
Um erro interno é 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 passagem de normalização pós-conversão sobre o ficheiro COFF: reescrever os nomes das secções para a forma esperada e reescrever os símbolos de definição de secção correspondentes para casarem, 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 liga e depois rebenta
Há um passo prévio para um dos dois conjuntos de objetos. Os objetos OpenJPEG construídos pelo compilador C++ clássico de 32 bits dependem de rotinas privadas do Delphi para inteiros de 64 bits, que o Free Pascal não fornece, por isso nenhuma conversão de formato os torna utilizáveis. Estes são recompilados primeiro com o compilador baseado em Clang, que não emite essas dependências, e convertidos depois
// Os objetos do alvo FPC vivem no seu próprio diretório. Não substituem
// o conjunto de objetos Delphi, porque as duas toolchains compilam a
// partir da mesma árvore de código e cada uma precisa dos seus inputs de ligação
//
// Lib\thirdparty\Win32 objetos OMF Delphi, inalterados
// Lib\thirdparty\Win32f objetos COFF FPC, convertidos e normalizados
//
// Pontos de entrada da build:
// build-Win32-Lib-FPC.cmd
// build-Win64-Lib-FPC.cmd
As rotinas auxiliares privadas do compilador não são portáteis, e as convenções também não
O runtime Delphi fornece trampolins em assembly para operações com inteiros de 64 bits no x86 de 32 bits, e objetos C pré-compilados construídos para o Delphi chamam-nas. O Free Pascal tem o seu próprio arranjo, por isso essas referências têm de ser satisfeitas de outra forma em vez de redirecionadas. O detalhe que torna o redirecionamento impossível é a convenção de chamada: a rotina de temporização usada pelo código de imaging tem o seu argumento de quatro bytes limpo pelo callee, enquanto a rotina de divisão de 64 bits limpa dezasseis bytes e devolve o resultado no clássico par de registos. Duas rotinas, duas convenções, e um trampolim escrito para uma corrompe silenciosamente a stack da outra
A name decoration acrescenta a segunda metade do problema. No Win32, o Free Pascal prefixa automaticamente as importações C externas com um underscore enquanto exporta declarações public name tal como estão, por isso o lado das importações e o lado das exportações da mesma ponte seguem regras diferentes. A ponte do runtime C de que o OpenJPEG precisa tem por isso de exportar os nomes de símbolo C exatos, e os pontos de entrada variádicos precisam de um salto indireto de 32 bits em vez de um direto. Nada disto é exótico depois de dito. Tudo falha como um erro de ligação que nomeia um símbolo que ninguém escreveu
O que fazia um executável Win32 morrer antes do main?
Uma DLL de 64 bits no caminho de pesquisa, alcançada porque a unidade zlib do Free Pascal faz binding dinâmico em vez de ligação estática. O sintoma era uma saída imediata com o código de estado de imagem inválida, antes de qualquer código Pascal do programa correr, o que o manda olhar para o programa que acabou de construir quando a falha está no loader a resolver uma importação contra a arquitetura errada
A lição é sobre pressupostos, não sobre a zlib. Uma unidade com o nome de uma biblioteca de compressão não contém necessariamente uma; pode ser um binding que espera uma biblioteca partilhada em execução, e uma dependência dinâmica que não pretendia é um passivo de deployment mesmo quando calha resolver. Mudar para a implementação de streams em Pascal puro dá a ambos os alvos um caminho de compressão estaticamente incluído sem qualquer dependência externa, que é o que uma biblioteca embebida na aplicação de outra pessoa devia ter desde o início
O mesmo instinto aplica-se ao backend externo do codificador JBIG2. No alvo de 32 bits o codificador externo não é ligado, por isso os pedidos caem no codificador Pascal incorporado, e o teste que verifica isto tem de consultar o estado de registo do alvo corrente em vez de tratar um encode bem-sucedido como prova de que o backend externo está presente. Um fallback que funciona é precisamente a coisa que esconde uma dependência em falta, que é o padrão de falha examinado em diagnóstico de falhas silenciosas de stubs. O trabalho de ligação estática de 64 bits está coberto em ligação estática do jbig2enc sob FPC
Aritmética de 32 bits num memory stream
Código que manipula tamanhos de buffer com aritmética sem sinal à largura do ponteiro está correto no Win64 e no Win32 está a uma imagem grande do overflow. O stream em memória que alimenta o codec JPEG 2000 cresce por duplicação e avança por adição, e num alvo de 32 bits ambas as operações podem dar wrap com inputs grandes mas perfeitamente legítimos
Cada escrita, salto, seek e alocação inicial verifica antes de calcular, e o teto de capacidade é o valor máximo com sinal à largura do ponteiro, escolhido para casar com o que a rotina de movimentação de blocos e os valores de retorno dos callbacks conseguem exprimir. O requisito comportamental quando um pedido é recusado é fácil de errar: recusar não pode alterar a posição do stream nem o seu comprimento. Uma mutação parcial seguida de um erro deixa o stream num estado sobre o qual o chamador não consegue raciocinar, e a operação seguinte agrava
// Verifique antes de calcular. No Win32 ambas estas operações 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 intactos
NewCapacity := FCapacity;
while (NewCapacity < FPosition + Needed) do
begin
if NewCapacity > NativeUInt(High(NativeInt)) shr 1 then
Exit(False); // duplicar daria overflow
NewCapacity := NewCapacity shl 1;
end;
Duas armadilhas de output de build que sobrevivem ao port
Separar executáveis de teste e de exemplos por arquitetura de alvo em diretórios de output por alvo é obviamente certo e parte imediatamente tudo o que localizava os seus dados de teste contando níveis de diretório para cima. A correção é procurar o diretório de assets subindo na árvore em vez de assumir uma profundidade fixa, com uma restrição deliberada: o exemplo de assinatura só aceita um certificado de fallback a partir do seu próprio diretório de projeto, nunca de um antepassado arbitrário, porque um certificado homónimo encontrado mais acima na árvore é uma surpresa de segurança e não uma conveniência
A segunda armadilha sobrevive a todos os ports e vale a pena levar para qualquer projeto FPC. Depois de uma atualização do compilador, ter o compilador a rejeitar ficheiros PPU obsoletos não chega, porque o linker continua a preferir ficheiros de objeto deixados para trás no caminho de pesquisa de unidades mesmo quando o PPU que carregou veio do diretório correto, e acrescentar um caminho de output de objetos explícito não sobrepõe essa preferência. A única resposta fiável é um diretório de unidades temporário fresco por ronda de build. Qualquer coisa menos produz um binário ligado a partir de duas versões do compilador, que falha de formas que parecem bugs de código-fonte
As condicionais de plataforma são a última peça, e escolher o eixo certo importa mais do que parece. A pergunta certa é normalmente se o código é específico do Windows e não se uma determinada widget library está presente, como mostrou o trabalho de conversão de metafiles em importação vetorial EMF e condicionais de plataforma: mudar esse guarda de uma condição de biblioteca de controlos para uma condição de plataforma transformou uma suposta reescrita numa alteração de uma diretiva. O suporte de Free Pascal e Lazarus para ambos os alvos Windows é distribuído com a biblioteca PDF Delphi PDFlibPas, compilada a partir das mesmas fontes que os pacotes Delphi e C++Builder