Artigo Técnico

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

O Free Pascal em Win32 acrescenta um underscore inicial a cada importação cdecl; external automaticamente, enquanto public name exporta a string que escreveu, caráter a caráter. O HotPDF tem de satisfazer as duas convenções na mesma árvore de código, porque a build Delphi já distribui declarações de importação que soletram o underscore à mão. Errar esta assimetria produz erros de ligação que nomeiam um símbolo que ninguém escreveu

Estender uma biblioteca Delphi ao Free Pascal costuma ser descrito como um problema de portabilidade, e no Win64 é quase sempre isso. O Win32 é diferente. A ABI do Windows x86 de 32 bits carrega trinta anos de convenção acumulada sobre como os símbolos C são soletrados, quem limpa a stack, e que rotinas privadas do compilador uma unidade de tradução pode assumir, e cada uma delas é um sítio onde dois compiladores Pascal que concordam na língua ainda podem discordar no ficheiro de objeto

Porque é 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 às importações mas não às exportações. Declare function deflate(...): Integer; cdecl; external; e o FPC procura _deflate no ficheiro de objeto no Win32, e deflate no Win64. Esse é o comportamento correto e casa com o que um compilador C emite. A armadilha está do outro lado da ponte: uma rotina marcada public name 'deflate' exporta precisamente deflate nos dois alvos, sem prefixo acrescentado

Acrescente agora o detalhe histórico que torna isto concreto. A build Delphi já declara alguns destes pontos de entrada com o underscore escrito no nome, porque é isso que os seus próprios ficheiros de objeto contêm. Dê a mesma declaração ao FPC no Win32 e o compilador prefixa-a diligentemente outra vez, por isso o linker caça __deflate, um símbolo que nada exporta. A correção intuitiva, acrescentar um underscore em todo o lado, parte as importações que já estavam soletradas corretamente

O que funciona é um par de constantes de prefixo em vez de uma única. HPDFFPCZLib e HPDFFPCCodecStubs usam um prefixo para importações C simples e outro para importações que já trazem um prefixo do lado Delphi, e no Win64 ambas as constantes são vazias para que os nomes de ligação existentes sobrevivam intactos. Duas constantes em vez de uma é a correção inteira, e só é óbvia depois de separar a regra das importações da regra das exportações

As mesmas declarações de símbolos C resolvidas pelo Free Pascal e pelo Delphi no Win64 e no Win32: importações cdecl ganham um underscore só no alvo de 32 bits, uma declaração Delphi já soletrada com underscore torna-se __deflate e falha a ligação, enquanto exportações public name permanecem literais nas duas arquiteturas
Uma única constante de prefixo não serve as duas regras: importações cdecl simples e importações que já trazem o underscore Delphi decoram de forma diferente sob FPC no Win32, por isso o HotPDF mantém duas e deixa ambas vazias no Win64
// Dois prefixos, não um: importações C simples e importações que já trazem
// um prefixo Delphi escrito à mão decoram de forma diferente sob FPC/Win32
const
{$IF DEFINED(FPC) and DEFINED(CPU32)}
  CPrefix     = '_';   // o FPC acrescenta este por conta própria para cdecl external
  DelphiCName = '';    // já soletrado com o underscore no código-fonte
{$ELSE}
  CPrefix     = '';
  DelphiCName = '';
{$IFEND}

// Lado das exportações: 'public name' é literal em todos os alvos
procedure hpdf_codec_free(P: Pointer); cdecl;
  public name 'hpdf_codec_free';

WIN32 diz-lhe a arquitetura, não a ABI

Este é o erro de compilação condicional com a cauda de debugging mais longa, e vale a pena enunciá-lo com clareza: WIN32 e WIN64 descrevem a arquitetura de destino e não dizem nada sobre que rotinas de runtime privadas do compilador existem. O Free Pascal define ambos os símbolos nos respetivos alvos Windows, exatamente como o Delphi. Um guarda escrito como {$IFDEF WIN32} à volta de código que chama uma rotina de runtime Delphi por isso compila sob FPC e falha na hora de ligar

Concretamente, três famílias de código caem nesta armadilha. Os trampolins de inteiros de 64 bits do Delphi alcançados através das rotinas System.@_ll, as rotinas de suporte em assembly Win32 do MSVC, e as slots de importação que as acompanham existem todos para servir objetos C pré-compilados que a build Delphi liga. O Free Pascal não liga esses objetos, por isso não precisa de nenhuma dessa maquinaria, e toda a referência a ela tem de desaparecer. A subtileza é que a declaração e a implementação têm de ser excluídas em conjunto. Exclua só uma e o compilador diz algo pouco útil sobre um identificador que não consegue casar com nada

A regra que daí decorre é curta. Condicione ao compilador quando a pergunta é sobre ABI ou suporte de runtime, condicione à arquitetura quando a pergunta é sobre largura de ponteiros ou contagem de registos, e nunca deixe uma fazer vezes pela outra

Condicionar declarações e implementações em conjunto

Um bloco condicional na secção de interface é uma armadilha fácil de cair sem dar por isso, e a mensagem de erro resultante aponta para qualquer sítio menos para a causa. Acrescente a declaração de um método a uma interface de classe e o sítio natural para a pôr é ao lado dos métodos relacionados, o que é perfeito até ao momento em que esses vizinhos calham estar dentro de um bloco {$IFDEF} existente. As diretivas condicionais não têm indentação, por isso um bloco que abriu quarenta linhas acima é praticamente invisível enquanto lê as declarações à volta

O que acontece a seguir é uma compilação que tem sucesso numa toolchain e produz uma cascata na outra. Se o guarda à volta é uma verificação de versão 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 longa lista de queixas sobre identificadores de métodos que esperava e não encontrou. Nenhuma das mensagens menciona o bloco condicional que causou tudo

Dois hábitos previnem toda esta classe de falha. Antes de inserir numa secção de interface, olhe para cima à procura da condicional aberta mais próxima em vez de confiar no agrupamento visual. E trate uma suite de testes Delphi verde como evidência sobre o Delphi apenas: a build da biblioteca Free Pascal é um portão separado, e a única forma de saber que passa é correr build-Win32-Lib-FPC.cmd e build-Win64-Lib-FPC.cmd como parte da mesma alteração

O que parte no código de aritmética de 32 bits

Uma restrição da linguagem aparece precisamente no código menos disposto a mudar: o Free Pascal de 32 bits não aceita um UInt64 como variável de controlo de um ciclo for. Nas unidades de curvas elípticas que transportam X25519 e X448, os ciclos que percorrem arrays de limbs foram escritos com contadores de 64 bits simplesmente porque tudo o resto no ficheiro é de 64 bits

A correção tem de ser cirúrgica, porque na aritmética de campo a largura de uma variável faz parte do argumento de correção. Os índices de ciclo tornam-se Integer, já que um array de limbs tem um punhado de elementos e nenhum índice se aproxima do intervalo de 32 bits. Tudo o que participa na aritmética, os próprios limbs, a propagação de carry e as máscaras, permanece UInt64, porque estreitar qualquer um desses muda silenciosamente o resultado módulo o primo do campo

// O FPC de 32 bits rejeita uma variável de ciclo 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 alteração destas não pode ser um teste de ida e volta. Encriptar e desencriptar com a mesma implementação partida concorda consigo própria na perfeição, e é por isso que os vetores de known-answer são inegociáveis aqui: corra os vetores de teste publicados do X25519 e do X448 e compare os bytes de output exatos. É a única verificação que distingue uma implementação correta de uma errada mas autoconsistente, e aplica-se por igual às primitivas simétricas discutidas em os limites dos codecs deflate e AES no Free Pascal

Os dois pontos de rutura de uma build Win32 Free Pascal do HotPDF: um guarda {$IFDEF WIN32} à volta de rotinas de runtime Delphi que compila mas falha na ligação a menos que declaração e implementação sejam excluídas em conjunto, e a variável de ciclo UInt64 nas travessias de limbs de X25519 e X448 estreitada para Integer enquanto limbs, carries e máscaras mantêm a sua largura
Condicione ao compilador quando a pergunta é ABI ou suporte de runtime e à arquitetura quando é largura de ponteiros, e depois prove as alterações de aritmética contra vetores known-answer publicados em vez de testes de ida e volta

O que vale uma build Free Pascal Win32

O ganho prático é que uma aplicação Lazarus destinada ao Windows de 32 bits recebe o mesmo motor de documentos que a sua contraparte Delphi, sem um contrato binário separado para manter. Isso importa mais nas implantações de que as pessoas raramente falam: controladores industriais, terminais de ponto de venda e software de linha de negócio de longa vida 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 de Free Pascal e Lazarus no Win64. O Win32 não é uma repetição dela. O Win64 tem uma convenção de chamada, sem name decoration e sem rotinas de inteiros privadas do Delphi para contornar, por isso quase tudo neste artigo é específico do alvo de 32 bits. As unidades de aritmética que precisaram da alteração da variável de ciclo 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 funcionalidades da linguagem. Ambos os compiladores aceitam aqui o mesmo Object Pascal. O que difere é o ficheiro de objeto: como os símbolos são soletrados, que rotinas de suporte se assume que o runtime fornece, e que objetos pré-compilados estão na ligação. O HotPDF distribui os pacotes Free Pascal e Lazarus a par dos Delphi e C++Builder no componente PDF Delphi HotPDF, por isso a mesma árvore de código alimenta todas as toolchains em vez de se bifurcar por compilador