Artigo Técnico

Armadilhas LP64 no Delphi Linux64: Longint e WIN64

O Delphi Linux64 segue o modelo de dados LP64, por isso lá o Longint e o LongWord têm 8 bytes, enquanto o Delphi em Windows (Win32 e Win64) e o Free Pascal em todos os targets os mantêm em 4. Trazer o PDFlibPas, a PDF Library for Delphi, para dcclinux64 tropeça nessa diferença de três formas distintas, mais uma armadilha de largura de apontadores: um teste {$IFDEF WIN64} usado para escolher um inteiro do tamanho de um apontador escolhe silenciosamente a largura errada em todos os targets de 64 bits fora do Windows

Nenhuma destas armadilhas é exótica. Cada uma é um hábito que o Windows torna seguro por acidente: escrever Integer onde a RTL diz Longint, espelhar uma estrutura C com campos Longint, ou perguntar "isto é Win64?" quando a questão verdadeira é "um apontador tem 8 bytes?". O código pode carregar esses hábitos durante anos sem consequências, porque no Win32, no Win64 e no FPC as duas grafias significam o mesmo. No Delphi Linux64 deixam de significar o mesmo, e o compilador é o primeiro a dar conta

Tudo o que se segue é sobre o que o compilador apanha. O PDFlibrary.pas compila com dcc32, dcc64 e dcclinux64 a partir de uma única árvore de código, e estes são os problemas que separam os hábitos exclusivos de Windows desse terceiro target

O que muda quando o Delphi tem como target o Linux64?

Os tipos inteiros que mudam de largura no Delphi Linux64 são precisamente o Longint e o LongWord; o Integer e o Cardinal mantêm-se em 4 bytes em todas as plataformas Delphi, e o NativeInt segue o apontador. O Windows é LLP64, onde só os apontadores crescem em 64 bits. Os targets POSIX de 64 bits do Delphi (Linux64 e os compiladores de 64 bits para macOS e iOS) são LP64, onde o long de C também cresce, e a Embarcadero fez o Longint Pascal segui-lo

TipoDelphi Win32Delphi Win64Delphi Linux64Free Pascal
Integer / Cardinal4444
Longint / LongWord4484 em todos os targets
NativeInt / Pointer488tamanho do apontador do target
C long (ABI da plataforma)448segue a ABI do SO

A coluna do Free Pascal é a armadilha para uma biblioteca multi-compilador. O PDFlibPas já distribui uma build Linux via FPC, e o FPC mantém o Longint em 32 bits no Linux x86-64. Ou seja, "funciona no Linux" era verdade e não dizia nada sobre o Delphi Linux64. O backend não-Windows do FPC revelou-se a base certa para o port para Delphi, mas os tamanhos dos tipos por baixo diferem

Matriz das larguras de Integer, Longint, NativeInt e long de C nos targets Delphi Win32, Win64, Linux64 e Free Pascal na biblioteca PDF PDFlibPas
no Delphi Linux64 só o Longint, o LongWord e o long de C alargam, enquanto o FPC os mantém em 4 bytes em todos os targets

Porque é que um teste WIN64 trunca apontadores no Linux?

Resposta curta: WIN64 significa "Windows de 64 bits", não "64 bits", e tratar os dois como sinónimos escolhe a largura de inteiro errada. O PDFlibDither, um ditherer de difusão de erro Floyd-Steinberg que reduz um bitmap de 24 bits a poucos níveis por canal, percorre três linhas de termos de erro com aritmética de apontadores crua, e o inteiro do tamanho de um apontador por trás dessa aritmética tem de seguir a largura da CPU, não a do sistema operativo. O condicional ingénuo erra:

// Inocente: a largura escolhida pelo SO dá Integer a todos os targets de 64 bits fora do Windows
{$IFDEF WIN64}
Type
  OSPtr= IntPtr;
{$ELSE}
Type
  OSPtr= Integer;
{$ENDIF}

// A varredura em serpentina avança os apontadores das linhas de erro um termo de cada vez
Inc(OSPtr(ErrorR), SizeOf(TErrorTerm));  // FPC x86-64: reescreve só os 32 bits baixos
Dec(OSPtr(ErrorG), SizeOf(TErrorTerm));  // dcclinux64: E2064, o lado esquerdo não pode ser atribuído

// Correto: um tipo do tamanho de um apontador em todo o lado
Type
  OSPtr= TPLIntPtr;  // PDFlibTypes: NativeInt fora do Windows, IntPtr / Integer em Win64 / Win32

Com esse condicional, o OSPtr resolve-se num Integer de 4 bytes no FPC para Linux x86-64, macOS x64 e ARM64, e o Inc(OSPtr(ErrorR), ...) reinterpreta os primeiros 4 bytes de um apontador de 8 bytes como um inteiro e incrementa-os. Num CPU little-endian isso é a metade baixa do endereço. A metade alta nunca é tocada, e é por isso que o erro se esconde tão bem: o resultado está correto até os 32 bits baixos fazerem wrap, momento em que o apontador salta silenciosamente 4 GiB para trás. O FPC compila o typecast de variável sem queixas. O Delphi recusa um typecast de variável para um tipo de tamanho diferente no lado esquerdo de uma atribuição, por isso o dcclinux64 para no E2064 no primeiro Inc

Essa é a assimetria útil aqui: um compilador que recusa o typecast encontra um erro de largura de apontador que outro compilador aceita em silêncio, antes de o código correr uma única linha no novo target. O PDFlibPas usa o TPLIntPtr de PDFlibTypes para isto, que testa OS_WINDOWS primeiro e só depois distingue Win64 de Win32, por isso os targets não-Windows recebem sempre NativeInt

A regra que retirámos daqui: nunca inferir a largura de um apontador a partir de um define de SO. Se precisar de um inteiro do tamanho de um apontador, use NativeInt / NativeUInt (o Delphi e o FPC têm ambos) ou um alias de projeto construído sobre eles, e salte o condicional por inteiro. Se genuinamente precisar de ramificar por largura em código Delphi, o CPU64BITS é o define que responde à pergunta que está realmente a fazer

Fluxo de decisão para tipos da largura de apontadores em código Delphi do PDFlibPas: um define WIN64 escolhe um Integer de 4 bytes em targets de 64 bits fora do Windows até o NativeInt o guardar
o define WIN64 pergunta pelo sistema operativo, por isso os tipos da largura de apontadores têm de vir do NativeInt ou de um teste CPU64BITS

Overrides de TStream: copie a assinatura da RTL, caráter a caráter

Um descendente de TStream no Delphi Linux64 tem de declarar e implementar Read, Write e SetSize com parâmetros Longint, exatamente como a RTL os declara, porque o Integer já não é o mesmo tipo. O PDFlibPas tem o seu próprio TPDFlibWideFileStream para nomes de ficheiros Unicode, e a declaração da classe usava Longint. A secção de implementação usava Integer

Type
  TPDFlibWideFileStream= Class(TStream)
  Protected
    Procedure SetSize(NewSize: Longint); Override;
  Public
    Function Read(Var Buffer; Count: Longint): Longint; Override;
    Function Write(Const Buffer; Count: Longint): Longint; Override;
  End;

// Antes: aceite durante anos em Win32, Win64 e FPC, onde Integer = Longint
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Integer): Longint;

// Depois: o cabeçalho da implementação repete a declaração exatamente
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Longint): Longint;
Begin
  Result:= FileRead(FHandle, Buffer, Count);
  If Result= - 1 Then
    Result:= 0;
End;

Em todos os compiladores que a biblioteca alguma vez encontrou, Integer e Longint eram o mesmo tipo de 4 bytes, por isso um cabeçalho desalinhado era indistinguível de um alinhado. No Delphi Linux64 a implementação já não casa com a própria declaração e a unit deixa de compilar. Este dói mais do que o tamanho sugere, porque o PDFlibExtra é uma unit de fundação de que a maior parte da biblioteca depende, por isso um único cabeçalho de implementação desalinhado impede de compilar a maioria das units

O mesmo raciocínio corre no sentido contrário para código novo. Quando fizer override de um método da RTL, copie os tipos dos parâmetros literalmente, mesmo quando "sabe" que dois tipos são o mesmo. O Delphi Linux64 é simplesmente o primeiro target onde esse conhecimento deixa de ser verdade

Que records precisam de Integer em vez de Longint, e quais têm de o manter?

Um record que espelha um formato de ficheiro ou uma estrutura de Windows tem de usar tipos de largura fixa (Integer, Cardinal, Int32), enquanto um record que espelha uma estrutura C declarada com long deve manter Longint / LongWord. Os dois casos parecem idênticos no código-fonte e pedem respostas opostas, porque o LONG de Windows tem sempre 32 bits e o long de C é o que a ABI da plataforma disser

Matriz de decisão para campos de records Delphi em LP64: Integer de largura fixa para bytes em disco, Longint mantido nos bindings de long de C no PDFlibPas
records que espelham bytes usam larguras fixas, e só bindings de um long de C mantêm Longint, com o respaldo de uma asserção de tamanho de uma linha
Type
  // GOFFSET do Uniscribe: dois LONG de Windows, sempre 8 bytes no total
  TGoffset= Packed Record
    du: Integer;  // era Longint: o record cresceu para 16 bytes no Delphi Linux64
    dv: Integer;
  End;

  // EMF LOGCOLORSPACE: cabeçalho preenchido por um Move fixo de 68 bytes a partir dos dados do record
  TPLEMFLogColorSpace= Packed Record
    Signature, Version, Size, ColorType, Intent: Cardinal;
    Endpoints: Array[0..8] Of Integer;  // era Longint: 36 bytes passaram a 72
    Gamma: Array[0..2] Of Cardinal;
    Filename: Array[0..259] Of WideChar;
  End;

  // FPDF_FILEACCESS do PDFium: m_FileLen é um unsigned long de C, por isso LongWord é o certo
  FPDF_FILEACCESS= Record
    m_FileLen: LongWord;  // 4 bytes em Windows, 8 em LP64, igual ao lado C
    m_GetBlock: TFPDF_GetBlock;
    m_Param: Pointer;
  End;

O caso EMF mostra como isto falha em silêncio. O importador copia 68 bytes para TPLEMFLogColorSpace: 20 bytes de cabeçalho, 36 bytes de endpoints CIE, 12 bytes de gamma. Com endpoints Longint em LP64, os mesmos 68 bytes cairiam num array de endpoints de 72 bytes, os valores de gamma nunca seriam escritos e nada levantaria um erro. Ambos os records assentam em caminhos de código exclusivos de Windows e usam tipos de largura fixa de qualquer forma, porque um record que espelha bytes em disco não deve depender de qual compilador o compila

O binding do PDFium é o contra-exemplo. O seu m_FileLen espelha um unsigned long de C, que é 32 bits em Windows e 64 bits em LP64, e o LongWord acompanha isso exatamente em ambos. Reescrevê-lo como Cardinal numa passada de limpeza partia-o no momento em que o host PDFium ganhasse um loader POSIX. O host PDFium é exclusivo de Windows, por isso isto é uma nota para o futuro. O lado das convenções de chamada dos bindings C tem armadilhas próprias, cobertas em o artigo sobre os bindings DLL, ActiveX e dylib da PDF Library

Uma guarda barata para qualquer record que espelhe bytes: afirme o tamanho uma vez no arranque, por exemplo Assert(SizeOf(TGoffset) = 8) numa secção de inicialização. Não custa nada e transforma uma mudança silenciosa de layout numa falha imediata na primeira execução num novo target

Acertar nos defines condicionais: DELPHI_LINUX, Android e Types

Antes de qualquer uma destas questões de tipos pudesse aparecer, o Delphi em Linux tinha de deixar de fazer-se passar por Windows. O PDFlibDefines.inc não definia nada para Delphi Linux, por isso o compilador caía diretamente nos defaults de Windows/VCL, e o primeiro erro foi o Vcl.Graphics não ser encontrado. Os defines encaminham o Delphi Linux para o mesmo backend que o FPC usa em targets não-Windows: renderização Cairo, fontconfig, OpenSSL e libcurl carregados em tempo de execução

{$IFDEF LINUX}
{$IFNDEF FPC}
{$IFNDEF ANDROID}
{$DEFINE DELPHI_LINUX}
{$DEFINE NOVCL}
{$DEFINE MD5_MAC}
{$DEFINE NOCRYPTAPI}
{$DEFINE USE_Cairo}
{$ENDIF}
{$ENDIF}
{$ENDIF}

Dois detalhes nesse bloco são fáceis de perder. A exclusão ANDROID está lá porque o Delphi define LINUX também para targets Android, e este backend não cobre Android. E o novo define é deliberadamente separado do ramo Delphi macOS existente (OS_MAC sem FPC), que depende de units Macapi; o define Linux assenta no caminho não-Windows do FPC

O outro erro recorrente era TPoint e TRect ficarem por declarar. Em Windows esses tipos chegam à maioria das units indiretamente, através da unit Windows. No Delphi Linux não há unit Windows na cláusula uses, por isso units como PDFlibFont e PDFlibPageTree passaram a listar Types explicitamente sob DELPHI_LINUX. Se o seu próprio código só compila em Windows porque o Windows arrasta System.Types consigo, espere a mesma parede. O padrão geral de código que só funciona num compilador por acidente é o tema de cinco hábitos Delphi que só funcionam por acidente até o FPC os expor

Onde o compilador deixa de ajudar

As questões LP64 acima são as que um compilador consegue encontrar. As que não consegue, como um record com o tamanho errado que nunca passa por um typecast, só aparecem quando os testes realmente correm no target, que é precisamente para isso que servem as asserções de tamanho descritas acima

Se o que o trouxe ao código de dithering foi a saída a 1 bit, o caminho suportado para páginas renderizadas é outro, a conversão descendente halftone GDI fora do renderer, descrita em renderizar páginas PDF para bitmaps monocromáticos de 1 bit

O trabalho de base multi-compilador descrito aqui vive na PDF Library for Delphi, que hoje tem como targets o Delphi e o C++Builder em Windows mais builds headless de Free Pascal em Linux e macOS; a página do produto lista na íntegra os compiladores e plataformas suportados