Artigo Técnico

Free Pascal Win32: decoração de símbolos C no HotPDF

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

As mesmas declarações de símbolos C resolvidas por Free Pascal e Delphi no Win64 e no Win32: imports cdecl ganham um underscore só no target de 32 bits, uma declaração Delphi já soletrada com underscore vira __deflate e falha no link, enquanto exports public name continuam literais nas duas arquiteturas
Uma constante de prefixo não serve às duas regras: imports cdecl simples e imports que já carregam o underscore do Delphi se decoram de forma diferente sob FPC no Win32, então o HotPDF mantém duas e deixa as duas vazias no Win64
// 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

Os dois pontos de quebra de um build Win32 Free Pascal do HotPDF: um guard {$IFDEF WIN32} em volta de helpers de runtime do Delphi que compila mas falha no link a menos que declaração e implementação sejam excluídas juntas, e a variável de loop UInt64 nas caminhadas de limbs do X25519 e X448 estreitada para Integer enquanto limbs, carries e máscaras mantêm a largura
Guarde pelo compilador quando a pergunta é ABI ou suporte de runtime e pela arquitetura quando é largura de ponteiro, depois prove mudanças de aritmética contra vetores de known-answer publicados em vez de testes de round-trip

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