O Free Pascal em Win32 adiciona um underscore inicial a todo import cdecl; external automaticamente, enquanto public name exporta a string que você escreveu, caractere por caractere. O HotPDF precisa satisfazer as duas convenções na mesma árvore de fontes, porque o build Delphi já vem com declarações de import que soletram o underscore à mão. Errar essa assimetria produz link errors que citam um símbolo que ninguém escreveu
Estender uma biblioteca Delphi para o Free Pascal costuma ser descrito como um problema de portabilidade, e no Win64 quase isso mesmo que é. O Win32 é diferente. A ABI do Windows x86 de 32 bits carrega trinta anos de convenção acumulada sobre como símbolos C são soletrados, quem limpa a stack, e quais helpers privados de compilador uma translation unit pode assumir, e cada um desses é um ponto onde dois compiladores Pascal que concordam na linguagem ainda podem discordar no arquivo objeto
Por que o mesmo símbolo resolve no Win64 e falha no Win32?
Porque o prefixo de underscore é uma convenção de 32 bits que o Free Pascal aplica aos imports, mas não aos exports. Declare function deflate(...): Integer; cdecl; external; e o FPC procura _deflate no arquivo objeto no Win32, e deflate no Win64. Isso é comportamento correto e bate com o que um compilador C emite. A armadilha está do outro lado da ponte: uma rotina marcada com public name 'deflate' exporta precisamente deflate nos dois targets, sem prefixo algum
Agora acrescente o detalhe histórico que torna isso concreto. O build Delphi já declara alguns desses entry points com o underscore escrito no nome, porque é isso que os próprios arquivos objeto dele contêm. Dê a mesma declaração ao FPC no Win32 e o compilador, obediente, prefixa de novo, então o linker sai à caça de __deflate, um símbolo que nada exporta. O conserto intuitivo, adicionar um underscore em todo lugar, quebra os imports que já estavam soletrados corretamente
O que funciona é um par de constantes de prefixo, e não uma só. HPDFFPCZLib e HPDFFPCCodecStubs usam um prefixo para imports C simples e outro para imports que já carregam um prefixo do lado Delphi, e no Win64 as duas constantes ficam vazias para os nomes de link existentes sobreviverem intactos. Duas constantes em vez de uma é o conserto inteiro, e ele só fica óbvio quando você separa a regra de import da regra de export
// Dois prefixos, não um: imports C simples e imports que já carregam
// um prefixo Delphi escrito à mão se decoram diferente sob FPC/Win32
const
{$IF DEFINED(FPC) and DEFINED(CPU32)}
CPrefix = '_'; // o FPC adiciona isso sozinho para cdecl external
DelphiCName = ''; // já soletrado com o underscore no código-fonte
{$ELSE}
CPrefix = '';
DelphiCName = '';
{$IFEND}
// Lado de export: 'public name' é literal em todo target
procedure hpdf_codec_free(P: Pointer); cdecl;
public name 'hpdf_codec_free';
WIN32 diz a arquitetura, não a ABI
Esse é o erro de compilação condicional com a cauda de debug mais longa, e vale enunciar com clareza: WIN32 e WIN64 descrevem a arquitetura de target e não dizem nada sobre quais helpers privados de runtime existem. O Free Pascal define os dois símbolos nos targets Windows correspondentes, exatamente como o Delphi. Um guard escrito como {$IFDEF WIN32} em volta de código que chama um helper de runtime do Delphi, portanto, compila sob FPC e falha na hora do link
Concretamente, três famílias de código caem nessa armadilha. Os trampolines de inteiros de 64 bits do Delphi alcançados pelos helpers System.@_ll, as rotinas de suporte em assembly do MSVC Win32, e os slots de import que vêm junto existem todos para servir objetos C pré-compilados que o build Delphi linka. O Free Pascal não linka esses objetos, então não precisa de nenhuma dessa maquinaria, e toda referência a ela tem de desaparecer. A sutileza é que declaração e implementação têm de ser excluídas juntas. Exclua só uma e o compilador reporta algo inútil sobre um identificador que não consegue casar com nada
A regra que se extrai daí é curta. Guarde pelo compilador quando a pergunta é sobre ABI ou suporte de runtime, guarde pela arquitetura quando a pergunta é sobre largura de ponteiro ou contagem de registradores, e nunca deixe um substituir o outro
Protegendo declarações e implementações juntas
Um bloco condicional na seção de interface é fácil de cair sem perceber, e a mensagem de erro resultante aponta para qualquer lugar menos para a causa. Adicione uma declaração de método a uma interface de classe e o lugar natural é ao lado dos métodos relacionados, o que funciona bem até o momento em que esses vizinhos estão dentro de um bloco {$IFDEF} já existente. Diretivas condicionais não são indentadas, então um bloco que abriu quarenta linhas acima é essencialmente invisível enquanto você lê as declarações em volta
O que acontece depois é uma compilação que passa numa toolchain e produz uma cascata na outra. Se o guard em volta é uma checagem de versão do Delphi que o Free Pascal não satisfaz, a declaração desaparece para o FPC enquanto a implementação incondicional permanece, e o compilador reporta uma lista longa de reclamações sobre identificadores de método que esperava e não encontrou. Nenhuma das mensagens menciona o bloco condicional que causou tudo
Dois hábitos previnem toda essa classe de falha. Antes de inserir numa seção de interface, olhe para cima atrás do condicional aberto mais próximo em vez de confiar no agrupamento visual. E trate uma suíte de testes Delphi verde como evidência só sobre o Delphi: o build da biblioteca em Free Pascal é um gate separado, e a única forma de saber que ele passa é rodar build-Win32-Lib-FPC.cmd e build-Win64-Lib-FPC.cmd como parte da mesma mudança
O que quebra no código de aritmética de 32 bits
Uma restrição da linguagem aparece exatamente no código menos disposto a mudar: o Free Pascal de 32 bits não aceita um UInt64 como variável de controle de loop for. Nas units de curva elíptica que carregam X25519 e X448, os loops que percorrem arrays de limbs foram escritos com contadores de 64 bits simplesmente porque todo o resto do arquivo é de 64 bits
O conserto tem de ser cirúrgico, porque na aritmética de campo a largura de uma variável é parte do argumento de correção. Os índices de loop viram Integer, já que um array de limbs tem um punhado de elementos e nenhum índice se aproxima da faixa de 32 bits. Tudo que participa da aritmética, os próprios limbs, a propagação de carry e as máscaras, permanece UInt64, porque estreitar qualquer um deles muda em silêncio o resultado módulo o primo do campo
// O FPC de 32 bits rejeita uma variável de loop UInt64. Estreite só o índice;
// limbs, máscaras e carries mantêm a largura ou a matemática do campo muda
var
I: Integer; // era UInt64
Carry, Mask: UInt64;
begin
Carry := 0;
for I := 0 to High(Limbs) do
begin
Limbs[I] := Limbs[I] + Carry;
Carry := Limbs[I] shr 51;
Limbs[I] := Limbs[I] and Mask;
end;
end;
A verificação de uma mudança dessas não pode ser um teste de round-trip. Criptografar e descriptografar com a mesma implementação quebrada concorda perfeitamente consigo mesma, e é por isso que vetores de known-answer são inegociáveis aqui: rode os vetores de teste publicados do X25519 e do X448 e compare os bytes exatos de saída. Essa é a única checagem que distingue uma implementação correta de uma errada autoconsistente, e vale igualmente para as primitivas simétricas discutidas em as fronteiras dos codecs deflate e AES no Free Pascal
Quanto vale um build Win32 em Free Pascal
O retorno prático é que uma aplicação Lazarus com alvo no Windows de 32 bits ganha o mesmo motor de documentos que a sua equivalente Delphi, sem um contrato binário separado para manter. Isso importa mais para os deployments de que as pessoas raramente falam: controladores industriais, terminais de point-of-sale e software de linha de negócio de vida longa, onde o runtime de 32 bits não é uma escolha legada, mas uma restrição de hardware
A história do Win64 veio primeiro e está descrita em suporte a Free Pascal e Lazarus no Win64. O Win32 não é uma reprise dela. O Win64 tem uma calling convention, nenhuma decoração de nome e nenhum helper de inteiros privado do Delphi para contornar, então quase tudo neste artigo é específico do target de 32 bits. As units de aritmética que precisaram da mudança de variável de loop são as mesmas descritas em aritmética de Montgomery sobre as curvas NIST, onde a disciplina de largura é explicada com mais profundidade
A lição geral é que o trabalho de portabilidade entre compiladores não é primariamente sobre recursos da linguagem. Os dois compiladores aceitam o mesmo Object Pascal aqui. O que difere é o arquivo objeto: como símbolos são soletrados, quais rotinas helper se assume que a runtime fornece, e quais objetos pré-compilados estão no link. O HotPDF entrega os pacotes Free Pascal e Lazarus junto com os Delphi e C++Builder no componente PDF HotPDF para Delphi, então a mesma árvore de fontes alimenta toda toolchain em vez de fazer fork por compilador