O Delphi Linux64 segue o modelo de dados LP64, então Longint e LongWord têm 8 bytes por lá, enquanto o Delphi no Windows (Win32 e Win64) e o Free Pascal em todos os alvos os mantêm com 4. Ao portar o PDFlibPas, a PDF Library for Delphi, para o dcclinux64, essa diferença aparece de três maneiras distintas, mais uma armadilha de largura de ponteiro: um teste com {$IFDEF WIN64} usado para escolher um inteiro do tamanho de um ponteiro escolhe silenciosamente a largura errada em todo alvo de 64 bits que não é Windows
Nenhuma dessas armadilhas é exótica. Cada uma é um hábito que o Windows torna seguro por acidente: escrever Integer onde a RTL pede Longint, espelhar uma estrutura C com campos Longint ou perguntar "este é Win64?" quando a pergunta real é "um ponteiro tem 8 bytes?". O código pode carregar esses hábitos por anos sem consequência, porque no Win32, no Win64 e no FPC as duas grafias significam a mesma coisa. No Delphi Linux64 elas deixam de significar a mesma coisa, e o compilador é o primeiro a notar
Tudo abaixo trata do que o compilador consegue capturar. O PDFlibrary.pas compila com dcc32, dcc64 e dcclinux64 a partir de uma única árvore de código-fonte, e estes são os problemas que ficam entre os hábitos só do Windows e esse terceiro alvo
O que muda quando o Delphi tem como alvo o Linux64?
Os tipos inteiros que mudam de largura no Delphi Linux64 são exatamente Longint e LongWord; Integer e Cardinal ficam com 4 bytes em todas as plataformas do Delphi, e o NativeInt segue o ponteiro. O Windows é LLP64, onde só os ponteiros crescem no 64 bits. Os alvos POSIX de 64 bits do Delphi (Linux64 e os compiladores de 64 bits para macOS e iOS) são LP64, onde o long do C também cresce, e a Embarcadero fez o Longint do Pascal segui-lo
| Tipo | Delphi Win32 | Delphi Win64 | Delphi Linux64 | Free Pascal |
|---|---|---|---|---|
Integer / Cardinal | 4 | 4 | 4 | 4 |
Longint / LongWord | 4 | 4 | 8 | 4 em todo alvo |
NativeInt / Pointer | 4 | 8 | 8 | tamanho de ponteiro do alvo |
C long (ABI da plataforma) | 4 | 4 | 8 | segue a ABI do sistema operacional |
A coluna do Free Pascal é a armadilha para uma biblioteca de múltiplos compiladores. O PDFlibPas já entrega um build Linux via FPC, e o FPC mantém o Longint com 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 acabou sendo a base certa para a portação, mas os tamanhos dos tipos por baixo dele são diferentes
Por que um teste de WIN64 trunca ponteiros 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 ponteiro puro, e o inteiro do tamanho de um ponteiro por trás dessa aritmética precisa seguir a largura da CPU, não a do sistema operacional. O condicional ingênuo erra:
// Ingênuo: a largura é escolhida pelo SO, então todo alvo não Windows de 64 bits recebe Integer
{$IFDEF WIN64}
Type
OSPtr= IntPtr;
{$ELSE}
Type
OSPtr= Integer;
{$ENDIF}
// O caminho serpentina avança os ponteiros de linha de erro um termo por 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 do ponteiro em todo lugar
Type
OSPtr= TPLIntPtr; // PDFlibTypes: NativeInt fora do Windows, IntPtr / Integer no Win64 / Win32
Com esse condicional, o OSPtr vira um 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 ponteiro de 8 bytes como inteiro e os incrementa. Numa 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 fica correto até os 32 bits baixos sofrerem wrap, momento em que o ponteiro salta silenciosamente 4 GiB para trás. O FPC compila o typecast de variável sem reclamar. O Delphi recusa um typecast de variável para um tipo de tamanho diferente no lado esquerdo de uma atribuição, então 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 ponteiro que outro compilador aceita em silêncio, antes de o código rodar uma única linha no novo alvo. O PDFlibPas usa o TPLIntPtr do PDFlibTypes para isso, que testa OS_WINDOWS primeiro e só então distingue Win64 de Win32, então os alvos não Windows sempre recebem NativeInt
A regra que levamos daqui: nunca infira a largura do ponteiro a partir de um define de sistema operacional. Se você precisa de um inteiro do tamanho de um ponteiro, use NativeInt / NativeUInt (o Delphi e o FPC têm ambos) ou um alias de projeto construído sobre eles, e elimine o condicional completamente. Se de fato você precisa ramificar pela largura em código Delphi, o CPU64BITS é o define que responde à pergunta que você realmente está fazendo
Overrides de TStream: copie a assinatura da RTL, caractere por caractere
Um descendente de TStream no Delphi Linux64 precisa declarar e implementar Read, Write e SetSize com parâmetros Longint, exatamente como a RTL os declara, porque Integer deixou de ser o mesmo tipo. O PDFlibPas tem o seu próprio TPDFlibWideFileStream para nomes de arquivo Unicode, e a declaração de classe dele usava Longint. A seção de implementation 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: aceito por anos no Win32, Win64 e FPC, onde Integer = Longint
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Integer): Longint;
// Depois: o cabeçalho de 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 todo compilador que a biblioteca já tinha visto, Integer e Longint eram o mesmo tipo de 4 bytes, então um cabeçalho destoante era indistinguível de um coerente. No Delphi Linux64 a implementação deixa de bater com a própria declaração e a unit para de compilar. Esse machuca mais do que o tamanho sugere, porque o PDFlibExtra é uma unit base da qual quase toda a biblioteca depende, então um único cabeçalho de implementação destoante impede a compilação da maioria das units
O mesmo raciocínio vale no sentido contrário para código novo. Ao sobrescrever um método da RTL, copie os tipos dos parâmetros literalmente, mesmo quando você "sabe" que dois tipos são iguais. O Delphi Linux64 é simplesmente o primeiro alvo em que esse conhecimento deixa de ser verdade
Quais records precisam de Integer em vez de Longint, e quais devem mantê-lo?
Um record que espelha um formato de arquivo ou uma estrutura do Windows precisa 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 do Windows tem sempre 32 bits e o long do C tem o que a ABI da plataforma disser
Type
// GOFFSET do Uniscribe: dois LONG do 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: header preenchido com 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 viraram 72
Gamma: Array[0..2] Of Cardinal;
Filename: Array[0..259] Of WideChar;
End;
// PDFium FPDF_FILEACCESS: m_FileLen é um C unsigned long, então LongWord é o certo
FPDF_FILEACCESS= Record
m_FileLen: LongWord; // 4 bytes no Windows, 8 no LP64, igual ao lado C
m_GetBlock: TFPDF_GetBlock;
m_Param: Pointer;
End;
O caso do EMF mostra o quão silenciosamente isso falha. O importador copia 68 bytes para o TPLEMFLogColorSpace: 20 bytes de header, 36 bytes de endpoints CIE, 12 bytes de gamma. Com endpoints Longint no LP64, esses mesmos 68 bytes cairiam num array de endpoints de 72 bytes, os valores de gamma nunca seriam gravados e nada levantaria um erro. Os dois records ficam em caminhos de código só do Windows e usam tipos de largura fixa mesmo assim, porque um record que espelha bytes em disco não deveria depender de qual compilador por acaso o compila
O binding do PDFium é o contraexemplo. O m_FileLen dele espelha um unsigned long do C, que tem 32 bits no Windows e 64 bits no LP64, e o LongWord acompanha isso exatamente nos dois. Reescrevê-lo como Cardinal numa passada de limpeza quebraria o binding no momento em que o host PDFium ganhar um loader POSIX. O host PDFium é só do Windows, então isto é uma nota para o futuro. O lado de calling convention dos bindings C tem armadilhas próprias, cobertas em o artigo sobre os bindings de DLL, ActiveX e dylib da PDF Library
Uma proteção barata para qualquer record que espelha bytes: faça um assert do tamanho dele uma vez na inicialização, por exemplo Assert(SizeOf(TGoffset) = 8) numa seção de initialization. Não custa nada e transforma uma mudança silenciosa de layout em uma falha imediata na primeira execução de um novo alvo
Acertando os defines condicionais: DELPHI_LINUX, Android e Types
Antes que qualquer questão de tipo pudesse aparecer, o Delphi no Linux precisou de parar de fingir que é Windows. O PDFlibDefines.inc não definia absolutamente nada para o Delphi Linux, então o compilador caía direto nos padrões de Windows/VCL, e o primeiro erro foi o Vcl.Graphics não encontrado. Os defines encaminham o Delphi Linux para o mesmo backend que o FPC usa em alvos 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 do ANDROID existe porque o Delphi define LINUX para alvos Android também, e esse 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 do Linux se apoia no caminho não Windows do FPC em vez disso
O outro erro recorrente era TPoint e TRect não declarados. No Windows esses tipos chegam à maioria das units indiretamente, pela unit Windows. No Delphi Linux não há unit Windows na cláusula uses, então units como PDFlibFont e PDFlibPageTree agora listam Types explicitamente sob DELPHI_LINUX. Se o seu próprio código compila no Windows só porque o Windows arrasta System.Types junto, espere a mesma parede. O padrão geral de código que só funciona num compilador por coincidência é o tema de cinco hábitos Delphi que só funcionam por acidente até o FPC expô-los
Onde o compilador para de ajudar
As questões LP64 acima são as que um compilador consegue encontrar. As que ele não consegue, como um record com o tamanho errado que nunca passa por um typecast, só aparecem quando os testes realmente rodam no alvo, que é exatamente o propósito dos asserts de tamanho descritos acima
Se saída de 1 bit é o que trouxe você ao código de dithering em primeiro lugar, o caminho suportado para páginas renderizadas é outro, a conversão descendente halftone via GDI fora do renderer, descrita em renderizar páginas PDF em bitmaps monocromáticos de 1 bit
O trabalho de base para múltiplos compiladores descrito aqui vive na PDF Library for Delphi, que hoje tem como alvos o Delphi e o C++Builder no Windows mais builds headless do Free Pascal no Linux e no macOS; a página do produto lista os compiladores e as plataformas suportados por completo