Technický článek

Pasti Delphi Linux64 a LP64: Longint, ukazatele a WIN64

Delphi Linux64 se drží datového modelu LP64, takže tam jsou Longint a LongWord osmibajtové, zatímco Delphi na Windows (Win32 i Win64) a Free Pascal na všech cílových platformách je drží na 4 bajtech. Při přenosu PDFlibPas, PDF knihovny pro Delphi, na dcclinux64 se tenhle rozdíl projeví třemi různými způsoby, a přidá se k nim ještě past se šířkou ukazatele: test {$IFDEF WIN64} použitý k výběru celého čísla velikosti ukazatele tiše vybere špatnou šířku na všech ne-Windows 64bitových cílech

Žádná z těchto pastí není exotická. Každá z nich je zvyk, který Windows z nehody dělají bezpečným: psaní Integer tam, kde RTL říká Longint, zrcadlení C struktury s poli Longint nebo otázka „je tohle Win64?", když skutečná otázka zní „je ukazatel 8 bajtů?". Kód může nosit tyhle zvyky roky bez následků, protože na Win32, Win64 a ve FPC oba zápisy znamenají totéž. Na Delphi Linux64 přestanou znamenat totéž a kompilátor si toho všimne jako první

Všechno níže se týká toho, co dokáže odhalit kompilátor. PDFlibrary.pas se builduje pomocí dcc32, dcc64 a dcclinux64 z jednoho zdrojového stromu a tady popsané problémy jsou přesně to, co stojí mezi zvyky vázanými jen na Windows a tím třetím cílem

Co se změní, když Delphi cílí na Linux64?

Celočíselné typy, které na Delphi Linux64 mění šířku, jsou přesně Longint a LongWord; Integer a Cardinal zůstávají na 4 bajtech na každé platformě Delphi a NativeInt se řídí ukazatelem. Windows používají LLP64, kde na 64bitech rostou jen ukazatele. 64bitové POSIX cíle Delphi (Linux64 a kompilátory pro macOS a iOS 64bit) jsou LP64, kde roste i C long, a Embarcadero z pascalského Longint udělalo typ, který ho následuje

TypDelphi Win32Delphi Win64Delphi Linux64Free Pascal
Integer / Cardinal4444
Longint / LongWord4484 na každém cíli
NativeInt / Pointer488velikost ukazatele cíle
C long (ABI platformy)448řídí se ABI operačního systému

Sloupec Free Pascal je pastí pro knihovnu pro víc kompilátorů. PDFlibPas už dnes dodává Linux build přes FPC a FPC drží Longint na 32 bitech i na Linuxu x86-64. Takže „na Linuxu to funguje" byla pravda, ale neřekla o Delphi Linux64 vůbec nic. Ne-Windows backend FPC se ukázal jako správný základ pro port do Delphi, jen velikosti typů pod ním se liší

Matice šířek Integer, Longint, NativeInt a C long napříč cíli Delphi Win32, Win64, Linux64 a Free Pascal v PDF knihovně PDFlibPas
na Delphi Linux64 se rozšiřují jen Longint, LongWord a C long, zatímco FPC je drží na 4 bajtech na každém cíli

Proč test WIN64 na Linuxu zkracuje ukazatele?

Stručná odpověď: WIN64 znamená „64bitové Windows", ne „64bit", a když oba pojmy považujeme za synonyma, vybereme špatnou šířku celého čísla. PDFlibDither, ditherer založený na Floyd-Steinberg error diffusion, který redukuje 24bitový bitmap na pár úrovní na kanál, prochází tři řádky chybových členů čistou aritmetikou s ukazateli, a celé číslo velikosti ukazatele za tou aritmetikou musí následovat šířku CPU, ne operačního systému. Naivní podmíněný překlad se plete:

// Naivní: šířku volí OS, takže každý ne-Windows 64bit cíl dostane Integer
{$IFDEF WIN64}
Type
  OSPtr= IntPtr;
{$ELSE}
Type
  OSPtr= Integer;
{$ENDIF}

// Hadovité skenování posouvá ukazatele chybových řádků po jednom členu
Inc(OSPtr(ErrorR), SizeOf(TErrorTerm));  // FPC x86-64: přepíše jen spodních 32 bitů
Dec(OSPtr(ErrorG), SizeOf(TErrorTerm));  // dcclinux64: E2064, levou stranu nelze přiřadit

// Správně: jeden typ velikosti ukazatele všude
Type
  OSPtr= TPLIntPtr;  // PDFlibTypes: NativeInt mimo Windows, IntPtr / Integer na Win64 / Win32

S tímhle podmíněným překladem se OSPtr na FPC pro Linux x86-64, macOS x64 a ARM64 rozloží na 4bajtový Integer a Inc(OSPtr(ErrorR), ...) přeinterpretuje prvních 4 bajty 8bajtového ukazatele jako celé číslo a inkrementuje je. Na little-endian CPU je to spodní polovina adresy. Horní polovina se nikdy nezmění, a právě proto se chyba tak dobře skrývá: výsledek je správný, dokud spodních 32 bitů nepřeteče, a v tu chvíli ukazatel tiše skočí zpět o 4 GiB. FPC zkompiluje typecast proměnné bez námitek. Delphi odmítá typecast proměnné na typ odlišné velikosti na levé straně přiřazení, takže dcclinux64 se zastaví na E2064 u prvního Inc

To je tady ta užitečná asymetrie: kompilátor, který typecast odmítne, najde chybu v šířce ukazatele, kterou jiný kompilátor tiše přijme, ještě předtím, než kód na novém cíli spustí jediný řádek. PDFlibPas k tomu používá TPLIntPtr z PDFlibTypes, který nejdřív testuje OS_WINDOWS a teprve potom rozlišuje Win64 od Win32, takže ne-Windows cíle vždy dostanou NativeInt

Poučení, které si z toho neseme: nikdy neodvozujte šířku ukazatele z definice operačního systému. Pokud potřebujete celé číslo velikosti ukazatele, použijte NativeInt / NativeUInt (má je Delphi i FPC) nebo projektový alias postavený na nich, a podmíněný překlad úplně vynechte. Když opravdu potřebujete větvit podle šířky v kódu Delphi, CPU64BITS je definice, která odpovídá na otázku, kterou skutečně kladete

Rozhodovací diagram pro typy šířky ukazatele v kódu PDFlibPas pro Delphi: definice WIN64 vybere 4bajtový Integer na ne-Windows 64bitových cílech, dokud to nezaistí NativeInt
definice WIN64 se ptá na operační systém, takže typy velikosti ukazatele musí pocházet z NativeInt nebo z testu CPU64BITS

Přepisované metody TStream: přebírejte signaturu RTL písmeno za písmenem

Potomek TStream na Delphi Linux64 musí deklarovat a implementovat Read, Write a SetSize s parametry Longint, přesně tak, jak je deklaruje RTL, protože Integer už není tentýž typ. PDFlibPas má vlastní TPDFlibWideFileStream pro unicode názvy souborů a jeho deklarace třídy skutečně používala Longint. Implementační sekce používala 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;

// Předtím: přijímané roky na Win32, Win64 a ve FPC, kde Integer = Longint
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Integer): Longint;

// Potom: hlavička implementace opakuje deklaraci přesně
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Longint): Longint;
Begin
  Result:= FileRead(FHandle, Buffer, Count);
  If Result= - 1 Then
    Result:= 0;
End;

Na každém kompilátoru, který knihovna kdy potkala, byly Integer a Longint tentýž 4bajtový typ, takže neshodná hlavička byla k nerozeznání od shodné. Na Delphi Linux64 už implementace neodpovídá vlastní deklaraci a unita se přestane kompilovat. Tohle bolí víc, než by naznačovala jeho drobnost, protože PDFlibExtra je základní unita, na které závisí většina knihovny, takže jediná neshodná implementační hlavička zablokuje kompilaci většiny unit

Stejná úvaha platí pro nový kód obráceně. Když přepisujete metodu RTL, přebírejte typy jejích parametrů doslova, i když „víte", že dva typy jsou totéž. Delphi Linux64 je prostě první cíl, kde tohle vědomství přestane platit

Které rekordy potřebují místo Longint Integer a které si Longint musí nechat?

Rekord, který zrcadlí souborový formát nebo strukturu Windows, musí použít typy s pevnou šířkou (Integer, Cardinal, Int32), zatímco rekord zrcadlící C strukturu deklarovanou s long by si měl ponechat Longint / LongWord. Oba případy ve zdrojáku vypadají identicky a potřebují opačné odpovědi, protože Windows LONG je vždy 32 bitů a C long je to, co řekne ABI platformy

Rozhodovací matice pro pole rekordů Delphi na LP64: Integer s pevnou šířkou pro bajty na disku, ponechaný Longint pro vazby na C long v PDFlibPas
rekordy zrcadlící bajty dostávají pevné šířky a Longint si ponechají jen vazby na C long, podložené jednořádkovou asercí velikosti
Type
  // Uniscribe GOFFSET: dva Windows LONG, celkem vždy 8 bajtů
  TGoffset= Packed Record
    du: Integer;  // dřív Longint: rekord na Delphi Linux64 narástl na 16 bajtů
    dv: Integer;
  End;

  // EMF LOGCOLORSPACE: hlavička plněná fixním 68bajtovým Move z dat rekordu
  TPLEMFLogColorSpace= Packed Record
    Signature, Version, Size, ColorType, Intent: Cardinal;
    Endpoints: Array[0..8] Of Integer;  // dřív Longint: 36 bajtů se zvětšilo na 72
    Gamma: Array[0..2] Of Cardinal;
    Filename: Array[0..259] Of WideChar;
  End;

  // PDFium FPDF_FILEACCESS: m_FileLen je C unsigned long, takže LongWord je správně
  FPDF_FILEACCESS= Record
    m_FileLen: LongWord;  // 4 bajty na Windows, 8 na LP64, stejně jako na straně C
    m_GetBlock: TFPDF_GetBlock;
    m_Param: Pointer;
  End;

Příklad EMF ukazuje, jak tiše se tohle rozbije. Importér kopíruje 68 bajtů do TPLEMFLogColorSpace: 20 bajtů hlavičky, 36 bajtů CIE endpointů, 12 bajtů gamy. S endpointy typu Longint na LP64 by těch samých 68 bajtů spadlo do 72bajtového pole endpointů, hodnoty gamy by se nikdy nezapsaly a nic by nevyhodilo chybu. Oba rekordy leží na cestách kódu určených jen pro Windows a stejně tak používají typy s pevnou šířkou, protože rekord zrcadlící bajty na disku by neměl záviset na tom, který kompilátor ho zrovna builduje

Vazba na PDFium je protipříklad. Její m_FileLen zrcadlí C unsigned long, který je na Windows 32 bitů a na LP64 64 bitů, a LongWord to na obou přesně kopíruje. Přepsat ho na Cardinal při úklidové passáži by ho rozbilo v okamžiku, kdy by PDFium host dostal POSIX loader. PDFium host je jen pro Windows, takže tohle je poznámka do budoucna. Strana konvencí volání u C vazeb má vlastní pasti, popsané v článku o DLL, ActiveX a dylib vazbách PDF knihovny

Levná pojistka pro každý rekord, který zrcadlí bajty: jednou při startu asertujte jeho velikost, třeba Assert(SizeOf(TGoffset) = 8) v inicializační sekci. Nic to nestojí a tichou změnu rozložení promění v okamžitý pád při prvním spuštění na novém cíli

Správné podmíněné definice: DELPHI_LINUX, Android a Types

Než se vůbec mohly projevit problémy s typy, muselo Delphi na Linuxu přestat předstírat, že je Windows. PDFlibDefines.inc nedefinoval pro Delphi Linux vůbec nic, takže kompilátor spadl rovnou do výchozích podmínek Windows/VCL a první chybou bylo, že se nenašel Vcl.Graphics. Definice směrují Delphi Linux na stejný backend, který FPC používá na ne-Windows cílech: Cairo rendering, fontconfig, OpenSSL a libcurl nahrané za běhu

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

Dva detaily v tom bloku jdou snadno přehlédnout. Výjimka ANDROID je tam proto, že Delphi definuje LINUX i pro cíle Android a tenhle backend Android nepokrývá. A nová definice je záměrně oddělená od existující větve Delphi macOS (OS_MAC bez FPC), která závisí na unitách Macapi; definice Linux staví na ne-Windows cestě FPC

Druhou opakovanou chybou bylo nedeklarované TPoint a TRect. Na Windows se tyhle typy k většině unit dostávají nepřímo, přes unitu Windows. Na Delphi Linux unita Windows v uses klauzuli neexistuje, takže unity jako PDFlibFont a PDFlibPageTree teď uvádějí Types explicitně pod DELPHI_LINUX. Pokud váš vlastní kód kompiluje na Windows jen proto, že Windows táhne System.Types s sebou, čekejte stejnou zeď. Obecný vzorec kódu, který funguje na jednom kompilátoru jen náhodou, rozebírá pět návyků Delphi, které fungují jen náhodou, dokud je FPC neodhalí

Kde kompilátor přestává pomáhat

LP64 problémy výše jsou ty, které dokáže kompilátor najít. Ty, které najít nedokáže — třeba rekord o špatné velikosti, který nikdy neprojde typecastem — se ukážou, až testy skutečně poběží na cíli, a přesně k tomu slouží výše popsané aserce velikosti

Pokud vás k ditheringovému kódu přivedl 1bitový výstup, pro renderované stránky vede podporovaná cesta jinam, přes GDI halftone down-conversion mimo renderer, popsané v renderování PDF stránek do 1bitových monochromatických bitmap

Základy pro víc kompilátorů popsané tady žijí v PDF Library for Delphi, která dnes cílí na Delphi a C++Builder na Windows plus headless Free Pascal buildy na Linuxu a macOS; produktová stránka uvádí podporované kompilátory a platformy kompletně