Articolo tecnico

Trappole LP64 in Delphi Linux64: Longint, puntatori, WIN64

Delphi Linux64 segue il data model LP64, quindi Longint e LongWord lì valgono 8 byte, mentre Delphi su Windows (Win32 e Win64) e Free Pascal su ogni target li tengono a 4. Portare PDFlibPas, la PDF Library per Delphi, su dcclinux64 incappa in questa differenza in tre modi distinti, più una trappola sulla larghezza dei puntatori: un test {$IFDEF WIN64} usato per scegliere un intero grande come un puntatore sceglie in silenzio la larghezza sbagliata su ogni target 64 bit non Windows

Nessuna di queste trappole è esotica. Ognuna è un'abitudine che su Windows resta sicura per puro caso: scrivere Integer dove la RTL dichiara Longint, specchiare una struttura C con campi Longint, o chiedersi se siamo su Win64 quando la vera domanda è se un puntatore sia lungo 8 byte. Il codice può portarsi dietro queste abitudini per anni senza conseguenze, perché su Win32, Win64 e FPC le due grafie significano la stessa cosa. Su Delphi Linux64 smettono di significare la stessa cosa, ed è il compilatore il primo ad accorgersene

Tutto quanto segue riguarda ciò che il compilatore riesce a beccare. PDFlibrary.pas compila con dcc32, dcc64 e dcclinux64 da un unico albero sorgente, e questi sono i problemi che separano le abitudini da solo-Windows da quel terzo target

Cosa cambia quando Delphi punta a Linux64?

I tipi interi che cambiano larghezza su Delphi Linux64 sono esattamente Longint e LongWord; Integer e Cardinal restano a 4 byte su ogni piattaforma Delphi, e NativeInt segue il puntatore. Windows è LLP64, dove su 64 bit crescono solo i puntatori. I target POSIX 64 bit di Delphi (Linux64 e i compilatori macOS e iOS a 64 bit) sono LP64, dove cresce anche il long del C, ed Embarcadero ha fatto sì che il Longint Pascal lo seguisse

TipoDelphi Win32Delphi Win64Delphi Linux64Free Pascal
Integer / Cardinal4444
Longint / LongWord4484 su ogni target
NativeInt / Pointer488dimensione del puntatore del target
C long (ABI della piattaforma)448segue l'ABI del sistema

La colonna Free Pascal è la trappola per una libreria cross-compiler. PDFlibPas spedisce già una build Linux tramite FPC, e FPC mantiene Longint a 32 bit su Linux x86-64. Quindi «funziona su Linux» era vero e non ci diceva nulla su Delphi Linux64. Il backend non-Windows di FPC si è rivelato la base giusta per il porting Delphi, ma le dimensioni dei tipi sotto di esso cambiano

Matrice delle larghezze di Integer, Longint, NativeInt e long del C sui target Delphi Win32, Win64, Linux64 e Free Pascal nella PDF library PDFlibPas
solo Longint, LongWord e il long del C si allargano su Delphi Linux64, mentre FPC li tiene a 4 byte su ogni target

Perché un controllo WIN64 tronca i puntatori su Linux?

Risposta breve: WIN64 significa «Windows a 64 bit», non «64 bit», e trattare i due come sinonimi sceglie la larghezza intera sbagliata. PDFlibDither, un ditherer error-diffusion Floyd-Steinberg che riduce una bitmap a 24 bit a pochi livelli per canale, percorre tre righe di termini di errore con aritmetica raw sui puntatori, e l'intero grande come un puntatore dietro quell'aritmetica deve seguire la larghezza della CPU, non il sistema operativo. Il condizionale ingenuo sbaglia così:

// Ingenuo: larghezza scelta dal sistema operativo, così ogni target 64 bit non Windows riceve Integer
{$IFDEF WIN64}
Type
  OSPtr= IntPtr;
{$ELSE}
Type
  OSPtr= Integer;
{$ENDIF}

// La scansione serpentina avanza i puntatori delle righe di errore di un termine alla volta
Inc(OSPtr(ErrorR), SizeOf(TErrorTerm));  // FPC x86-64: riscrive solo i 32 bit bassi
Dec(OSPtr(ErrorG), SizeOf(TErrorTerm));  // dcclinux64: E2064, left side cannot be assigned to

// Corretto: un unico tipo grande come un puntatore ovunque
Type
  OSPtr= TPLIntPtr;  // PDFlibTypes: NativeInt fuori da Windows, IntPtr / Integer su Win64 / Win32

Con quel condizionale, OSPtr si risolve in un Integer a 4 byte su FPC per Linux x86-64, macOS x64 e ARM64, e Inc(OSPtr(ErrorR), ...) reinterpreta i primi 4 byte di un puntatore a 8 byte come un intero e li incrementa. Su una CPU little-endian quella è la metà bassa dell'indirizzo. Quella alta non viene mai toccata, ed è per questo che l'errore si nasconde così bene: il risultato è corretto fino a quando i 32 bit bassi non vanno in wrap, momento in cui il puntatore salta indietro di 4 GiB in silenzio. FPC compila il typecast di variabile senza protestare. Delphi rifiuta un typecast di variabile verso un tipo di dimensione diversa sul lato sinistro di un assegnamento, così dcclinux64 si ferma su E2064 al primo Inc

Questa è l'asimmetria utile: un compilatore che rifiuta il typecast trova un errore di larghezza del puntatore che un altro compilatore accetta in silenzio, prima che il codice abbia eseguito una sola riga sul nuovo target. PDFlibPas usa TPLIntPtr da PDFlibTypes per questo, che testa prima OS_WINDOWS e solo dopo distingue Win64 da Win32, così i target non Windows ricevono sempre NativeInt

La regola che ne abbiamo ricavato: non dedurre mai la larghezza del puntatore da un define del sistema operativo. Se vi serve un intero grande come un puntatore, usate NativeInt / NativeUInt (li hanno sia Delphi che FPC) o un alias di progetto costruito su di essi, e saltate del tutto il condizionale. Se vi serve davvero fare un branch sulla larghezza in codice Delphi, CPU64BITS è il define che risponde alla domanda che state davvero ponendo

Flusso decisionale per i tipi larghi come un puntatore nel codice Delphi di PDFlibPas: un define WIN64 sceglie un Integer a 4 byte sui target 64 bit non Windows finché non subentra NativeInt
il define WIN64 chiede del sistema operativo, così i tipi larghi come un puntatore devono venire da NativeInt o da un test su CPU64BITS

Override di TStream: copiate la firma della RTL, carattere per carattere

Un discendente di TStream su Delphi Linux64 deve dichiarare e implementare Read, Write e SetSize con parametri Longint, esattamente come li dichiara la RTL, perché Integer non è più lo stesso tipo. PDFlibPas ha il suo TPDFlibWideFileStream per i nomi file Unicode, e la sua dichiarazione di classe usava effettivamente Longint. La sezione 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;

// Prima: accettato per anni su Win32, Win64 e FPC, dove Integer = Longint
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Integer): Longint;

// Dopo: l'header della implementation ripete la dichiarazione alla lettera
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Longint): Longint;
Begin
  Result:= FileRead(FHandle, Buffer, Count);
  If Result= - 1 Then
    Result:= 0;
End;

Su ogni compilatore che la libreria aveva mai incontrato, Integer e Longint erano lo stesso tipo a 4 byte, quindi un header disallineato era indistinguibile da uno allineato. Su Delphi Linux64 la implementation non corrisponde più alla sua stessa dichiarazione e l'unità smette di compilare. Questo fa più male di quanto suggerisca la sua dimensione, perché PDFlibExtra è un'unità fondante su cui si appoggia gran parte della libreria, quindi un solo header della implementation disallineato blocca la compilazione della maggior parte delle unità

Lo stesso ragionamento vale al contrario per il codice nuovo. Quando fate override di un metodo della RTL, copiate alla lettera i tipi dei suoi parametri, anche quando «sapete» che due tipi coincidono. Delphi Linux64 è semplicemente il primo target in cui quel sapere smette di essere vero

Quali record vogliono Integer invece di Longint, e quali devono tenerlo?

Un record che specchia un formato file o una struttura Windows deve usare tipi a larghezza fissa (Integer, Cardinal, Int32), mentre un record che specchia una struttura C dichiarata con long deve conservare Longint / LongWord. I due casi sembrano identici nel sorgente e vogliono risposte opposte, perché il LONG di Windows è sempre a 32 bit e il long del C è ciò che dice l'ABI della piattaforma

Matrice decisionale per i campi dei record Delphi su LP64: Integer a larghezza fissa per i byte su disco, Longint conservato per i binding a long del C in PDFlibPas
i record che specchiano byte prendono larghezze fisse, e solo i binding a un long del C conservano Longint, coperti da un'asserzione di dimensione di una riga
Type
  // GOFFSET di Uniscribe: due LONG di Windows, sempre 8 byte in totale
  TGoffset= Packed Record
    du: Integer;  // era Longint: il record cresceva a 16 byte su Delphi Linux64
    dv: Integer;
  End;

  // LOGCOLORSPACE EMF: header riempito con un Move fisso di 68 byte dai dati del record
  TPLEMFLogColorSpace= Packed Record
    Signature, Version, Size, ColorType, Intent: Cardinal;
    Endpoints: Array[0..8] Of Integer;  // era Longint: 36 byte diventavano 72
    Gamma: Array[0..2] Of Cardinal;
    Filename: Array[0..259] Of WideChar;
  End;

  // FPDF_FILEACCESS di PDFium: m_FileLen è un unsigned long del C, quindi LongWord è giusto
  FPDF_FILEACCESS= Record
    m_FileLen: LongWord;  // 4 byte su Windows, 8 su LP64, come il lato C
    m_GetBlock: TFPDF_GetBlock;
    m_Param: Pointer;
  End;

Il caso EMF mostra con quanta quiete questa cosa fallisce. L'importer copia 68 byte dentro TPLEMFLogColorSpace: 20 byte di header, 36 byte di endpoint CIE, 12 byte di gamma. Con endpoint Longint su LP64, gli stessi 68 byte finirebbero in un array di endpoint da 72 byte, i valori gamma non verrebbero mai scritti, e nulla solleverebbe un errore. Entrambi i record stanno su percorsi di codice solo Windows, e comunque usano tipi a larghezza fissa, perché un record che specchia byte su disco non dovrebbe dipendere da quale compilatore capita a compilarlo

Il binding PDFium è il controesempio. Il suo m_FileLen specchia un unsigned long del C, che è 32 bit su Windows e 64 bit su LP64, e LongWord lo segue esattamente su entrambi. Riscriverlo come Cardinal in un passaggio di pulizia lo romperebbe nel momento in cui l'host PDFium guadagnasse un loader POSIX. L'host PDFium è solo Windows, quindi questa è una nota per il futuro. Il lato calling convention dei binding C ha trappole sue, trattate in articolo su DLL, ActiveX e binding dylib della PDF Library

Una guardia economica per qualunque record che specchia byte: asserite la sua dimensione una volta all'avvio, per esempio Assert(SizeOf(TGoffset) = 8) in una sezione initialization. Non costa nulla e trasforma un cambio di layout silenzioso in un fallimento immediato al primo avvio di un nuovo target

Incasellare bene i define condizionali: DELPHI_LINUX, Android e Types

Prima che qualunque problema di tipi potesse affiorare, Delphi su Linux ha dovuto smettere di fingersi Windows. PDFlibDefines.inc non definiva proprio nulla per Delphi Linux, così il compilatore cascava dritto nei default Windows/VCL, e il primo errore è stato Vcl.Graphics non trovato. I define incanalano Delphi Linux sullo stesso backend che FPC usa sui target non Windows: rendering Cairo, fontconfig, OpenSSL e libcurl caricati a runtime

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

Due dettagli in quel blocco sono facili da perdere. L'esclusione di ANDROID c'è perché Delphi definisce LINUX anche per i target Android, e questo backend non copre Android. E il nuovo define è deliberatamente separato dal ramo macOS Delphi esistente (OS_MAC senza FPC), che dipende dalle unità Macapi; il define Linux si appoggia invece al percorso non-Windows di FPC

L'altro errore ricorrente era TPoint e TRect non dichiarati. Su Windows quei tipi arrivano nella maggior parte delle unità per via indiretta, attraverso l'unità Windows. Su Delphi Linux non c'è alcuna unità Windows nella uses, così unità come PDFlibFont e PDFlibPageTree ora elencano Types esplicitamente sotto DELPHI_LINUX. Se il vostro codice compila su Windows solo perché Windows si trascina dietro System.Types, aspettatevi lo stesso muro. Il pattern generale del codice che funziona su un solo compilatore per coincidenza è il tema di cinque abitudini Delphi che funzionano solo per caso finché FPC non le smaschera

Dove il compilatore smette di aiutare

I problemi LP64 di sopra sono quelli che un compilatore sa trovare. Quelli che non sa trovare, come un record della dimensione sbagliata che non passa mai per un typecast, affiorano solo quando i test girano davvero sul target, che è esattamente lo scopo delle asserzioni di dimensione descritte sopra

Se l'output a 1 bit è ciò che vi ha portato al codice di dithering in primo luogo, la strada supportata per le pagine renderizzate è un'altra, la down-conversion halftone GDI fuori dal renderer, descritta in rendering di pagine PDF in bitmap monocromatiche a 1 bit

Il lavoro di base cross-compiler descritto qui vive nella PDF Library per Delphi, che oggi punta a Delphi e C++Builder su Windows più build headless di Free Pascal su Linux e macOS; la pagina del prodotto elenca per intero i compilatori e le piattaforme supportati