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
| Typ | Delphi Win32 | Delphi Win64 | Delphi Linux64 | Free Pascal |
|---|---|---|---|---|
Integer / Cardinal | 4 | 4 | 4 | 4 |
Longint / LongWord | 4 | 4 | 8 | 4 na každém cíli |
NativeInt / Pointer | 4 | 8 | 8 | velikost ukazatele cíle |
C long (ABI platformy) | 4 | 4 | 8 | ří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ší
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
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
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ě