Delphi Linux64 realizuje model danych LP64, więc Longint i LongWord mają tam 8 bajtów, podczas gdy Delphi pod Windows (Win32 i Win64) oraz Free Pascal na każdej platformie trzymają je na 4. Przenoszenie PDFlibPas, biblioteki PDF dla Delphi, na dcclinux64 uderza w tę różnicę na trzy różne sposoby, dochodzi do tego pułapka szerokości wskaźnika: test {$IFDEF WIN64} używany do wyboru typu o rozmiarze wskaźnika po cichu wybiera złą szerokość na każdej 64-bitowej platformie spoza Windows
Żadna z tych pułapek nie jest egzotyczna. Każda to nawyk, który pod Windows przypadkiem jest bezpieczny: pisanie Integer tam, gdzie RTL mówi Longint, odwzorowanie struktury C z polami Longint albo pytanie „czy to Win64?", gdy naprawdę pytasz „czy wskaźnik ma 8 bajtów". Kod może nosić takie nawyki latami bez konsekwencji, bo na Win32, Win64 i w FPC oba zapisy znaczą to samo. Na Delphi Linux64 przestają znaczyć to samo i kompilator zauważa to pierwszy
Wszystko poniżej dotyczy tego, co wyłapie kompilator. PDFlibrary.pas buduje się jednym drzewem źródeł pod dcc32, dcc64 i dcclinux64, a opisane problemy stoją między nawykami z Windows a tym trzecim celem
Co zmienia się, gdy Delphi celuje w Linux64?
Typy całkowite, które zmieniają szerokość na Delphi Linux64, to dokładnie Longint i LongWord; Integer i Cardinal zostają przy 4 bajtach na każdej platformie Delphi, a NativeInt podąża za wskaźnikiem. Windows jest LLP64, gdzie w 64 bitach rosną tylko wskaźniki. 64-bitowe cele POSIX Delphi (Linux64 oraz kompilatory macOS i iOS) są LP64, gdzie rośnie też C long, i Embarcadero kazał pascalowskiemu Longint iść w jego ślady
| Typ | Delphi Win32 | Delphi Win64 | Delphi Linux64 | Free Pascal |
|---|---|---|---|---|
Integer / Cardinal | 4 | 4 | 4 | 4 |
Longint / LongWord | 4 | 4 | 8 | 4 na każdej platformie |
NativeInt / Pointer | 4 | 8 | 8 | rozmiar wskaźnika platformy |
C long (ABI platformy) | 4 | 4 | 8 | zgodnie z ABI systemu |
Kolumna Free Pascal to pułapka dla biblioteki międzykompilatorowej. PDFlibPas już dziś dostarcza build na Linuksa przez FPC, a FPC trzyma Longint na 32 bitach pod Linuksem x86-64. „Działa na Linuksie" było prawdą i nic nam nie mówiło o Delphi Linux64. Backend FPC dla platform spoza Windows okazał się właściwym fundamentem portu dla Delphi, tylko rozmiary typów pod spodem są inne
Dlaczego test WIN64 ucina wskaźniki na Linuksie?
Krótka odpowiedź: WIN64 znaczy „64-bitowy Windows", a nie „64 bity", i traktowanie ich jako synonimów wybiera złą szerokość typu całkowitego. PDFlibDither, ditherer Floyd–Steinberga z dyfuzją błędu redukujący bitmapę 24-bitową do kilku poziomów na kanał, przechodzi trzy wiersze wyrazów błędu surową arytmetyką wskaźników, a typ całkowity o rozmiarze wskaźnika stojący za tą arytmetyką musi podążać za szerokością CPU, nie za systemem operacyjnym. Naiwny warunek myli:
// Naiwnie: szerokość wybiera system, więc każdy 64-bitowy cel spoza Windows dostaje Integer
{$IFDEF WIN64}
Type
OSPtr= IntPtr;
{$ELSE}
Type
OSPtr= Integer;
{$ENDIF}
// Skan sierpowy przesuwa wskaźniki wierszy błędu o jeden wyraz na raz
Inc(OSPtr(ErrorR), SizeOf(TErrorTerm)); // FPC x86-64: przepisuje tylko młodsze 32 bity
Dec(OSPtr(ErrorG), SizeOf(TErrorTerm)); // dcclinux64: E2064, lewej strony nie da się przypisać
// Poprawnie: jeden typ o rozmiarze wskaźnika wszędzie
Type
OSPtr= TPLIntPtr; // PDFlibTypes: NativeInt poza Windows, IntPtr / Integer na Win64 / Win32
Przy tym warunku OSPtr rozwiązuje się do 4-bajtowego Integer w FPC dla Linuksa x86-64 oraz macOS x64 i ARM64, a Inc(OSPtr(ErrorR), ...) reinterpretuje pierwsze 4 bajty 8-bajtowego wskaźnika jako liczbę i je zwiększa. Na CPU little-endian to młodsza połowa adresu. Starsza połowa nigdy nie jest ruszana, stąd ten błąd tak dobrze się chowa: wynik jest poprawny, dopóki młodsze 32 bity się nie przepełnią, a wtedy wskaźnik po cichu cofa się o 4 GiB. FPC kompiluje rzutowanie zmiennej bez mrugnięcia. Delphi odmawia rzutowania zmiennej na typ o innej wielkości po lewej stronie przypisania, więc dcclinux64 zatrzymuje się na E2064 przy pierwszym Inc
To jest tu użyteczna asymetria: kompilator odmawiający rzutowania wyłapuje błąd szerokości wskaźnika, który inny kompilator po cichu przepuści, zanim kod wykona choć jedną linię na nowej platformie. PDFlibPas używa do tego TPLIntPtr z PDFlibTypes, który najpierw testuje OS_WINDOWS i dopiero potem rozdziela Win64 od Win32, więc cele spoza Windows zawsze dostają NativeInt
Wniosek, który zabraliśmy: nigdy nie wyprowadzaj szerokości wskaźnika z define systemu. Jeśli potrzebujesz typu całkowitego o rozmiarze wskaźnika, użyj NativeInt / NativeUInt (ma je i Delphi, i FPC) albo aliasu projektowego zbudowanego na nich i pomiń warunek całkiem. Jeśli naprawdę musisz rozgałęziać kod Delphi po szerokości, CPU64BITS jest definem odpowiadającym na pytanie, które naprawdę zadajesz
Nadpisania TStream: przepisuj sygnaturę RTL znak w znak
Potomek TStream na Delphi Linux64 musi deklarować i implementować Read, Write i SetSize z parametrami Longint, dokładnie tak, jak deklaruje je RTL, bo Integer nie jest już tym samym typem. PDFlibPas ma własny TPDFlibWideFileStream dla unikodowych nazw plików i jego deklaracja klasy rzeczywiście używała Longint. Sekcja implementacji używała 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;
// Przed: akceptowane latami na Win32, Win64 i FPC, gdzie Integer = Longint
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Integer): Longint;
// Po: nagłówek implementacji powtarza deklarację co do znaku
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Longint): Longint;
Begin
Result:= FileRead(FHandle, Buffer, Count);
If Result= - 1 Then
Result:= 0;
End;
Na każdym kompilatorze, jaki biblioteka dotąd spotkała, Integer i Longint były tym samym 4-bajtowym typem, więc niezgodny nagłówek nie dało się odróżnić od zgodnego. Na Delphi Linux64 implementacja przestaje pasować do własnej deklaracji i unit przestaje się kompilować. To boli bardziej, niż wskazuje rozmiar, bo PDFlibExtra to fundament, od którego zależy większość biblioteki, więc jeden niezgodny nagłówek implementacji blokuje kompilację większości unitów
To samo rozumowanie działa w drugą stronę przy nowym kodzie. Nadpisując metodę RTL, przepisz jej typy parametrów dosłownie, nawet gdy „wiesz", że dwa typy to to samo. Delphi Linux64 po prostu jest pierwszą platformą, na której ta wiedza przestaje być prawdą
Które rekordy potrzebują Integer zamiast Longint, a które muszą go zachować?
Rekord odwzorujący format pliku albo strukturę Windows musi używać typów o stałej szerokości (Integer, Cardinal, Int32), a rekord odwzorujący strukturę C zadeklarowaną z long powinien zachować Longint / LongWord. Oba przypadki wyglądają w źródle identycznie i potrzebują przeciwnych odpowiedzi, bo Windows LONG ma zawsze 32 bity, a C long ma tyle, ile mówi ABI platformy
Type
// Uniscribe GOFFSET: dwa Windows LONG, razem zawsze 8 bajtów
TGoffset= Packed Record
du: Integer; // było Longint: rekord urósł do 16 bajtów na Delphi Linux64
dv: Integer;
End;
// EMF LOGCOLORSPACE: nagłówek wypełniany stałym Move 68 bajtów z danych rekordu
TPLEMFLogColorSpace= Packed Record
Signature, Version, Size, ColorType, Intent: Cardinal;
Endpoints: Array[0..8] Of Integer; // było Longint: 36 bajtów zrobiło się 72
Gamma: Array[0..2] Of Cardinal;
Filename: Array[0..259] Of WideChar;
End;
// PDFium FPDF_FILEACCESS: m_FileLen to C unsigned long, więc LongWord jest słuszny
FPDF_FILEACCESS= Record
m_FileLen: LongWord; // 4 bajty na Windows, 8 na LP64, jak po stronie C
m_GetBlock: TFPDF_GetBlock;
m_Param: Pointer;
End;
Przypadek EMF pokazuje, jak po cichu to się wali. Importer kopiuje 68 bajtów do TPLEMFLogColorSpace: 20 bajtów nagłówka, 36 bajtów końców CIE, 12 bajtów gamma. Przy końcach Longint na LP64 te same 68 bajtów wylądowałoby w 72-bajtowej tablicy końców, wartości gamma nie zostałyby nigdy zapisane i nic nie rzuciłoby błędu. Oba rekordy leżą na ścieżkach tylko dla Windows i i tak używają typów o stałej szerokości, bo rekord odwzorujący bajty na dysku nie powinien zależeć od tego, który kompilator akurat go buduje
Wiązanie PDFium to kontrprzykład. Jego m_FileLen odwzorowuje C unsigned long, które ma 32 bity na Windows i 64 na LP64, i LongWord oddaje to dokładnie w obu przypadkach. Przepisanie go na Cardinal w porządkującej przebudowie złamałoby je w chwili, gdy host PDFium dostanie loader POSIX. Host PDFium jest tylko dla Windows, więc to zapiska na przyszłość. Strona konwencji wywołań wiązań C ma własne pułapki, opisane w artykule o wiązaniach DLL, ActiveX i dylib biblioteki PDF
Tania ochrona dla każdego rekordu odwzorującego bajty: zasertuj jego rozmiar raz przy starcie, na przykład Assert(SizeOf(TGoffset) = 8) w sekcji inicjalizacyjnej. Nic nie kosztuje i zamienia cichą zmianę układu w natychmiastową porażkę przy pierwszym uruchomieniu na nowej platformie
Poprawne define warunkowe: DELPHI_LINUX, Android i Types
Zanim ktokolwiek z problemów typów zdążył się pokazać, Delphi na Linuksie musiało przestać udawać Windows. PDFlibDefines.inc nie definiowało niczego dla Delphi Linux, więc kompilator wpadał prosto w domyślne Windows/VCL, a pierwszym błędem był nieznaleziony Vcl.Graphics. Define kierują Delphi Linux na ten sam backend, z którego FPC korzysta na celach spoza Windows: rendering Cairo, fontconfig, OpenSSL i libcurl ładowane w czasie działania
{$IFDEF LINUX}
{$IFNDEF FPC}
{$IFNDEF ANDROID}
{$DEFINE DELPHI_LINUX}
{$DEFINE NOVCL}
{$DEFINE MD5_MAC}
{$DEFINE NOCRYPTAPI}
{$DEFINE USE_Cairo}
{$ENDIF}
{$ENDIF}
{$ENDIF}
Dwa szczegóły tego bloku łatwo przeoczyć. Wykluczenie ANDROID jest tam, bo Delphi definiuje LINUX także dla celów Android, a ten backend Androida nie obejmuje. Nowy define jest też celowo osobny od istniejącej gałęzi Delphi macOS (OS_MAC bez FPC), która zależy od unitów Macapi; define Linuksa buduje się na ścieżce FPC spoza Windows
Drugi powtarzający się błąd to niezadeklarowane TPoint i TRect. Na Windows te typy docierają do większości unitów pośrednio, przez unit Windows. Na Delphi Linux nie ma unitu Windows w klauzuli uses, więc unity takie jak PDFlibFont i PDFlibPageTree wymieniają teraz Types wprost pod DELPHI_LINUX. Jeśli twój kod kompiluje się pod Windows tylko dlatego, że Windows ciągnie za sobą System.Types, spodziewaj się tego samego muru. Ogólny wzorzec kodu działającego na jednym kompilatorze wyłącznie przypadkiem to temat pięciu nawyków Delphi, które działają tylko przypadkiem, dopóki FPC ich nie obnaży
Gdzie kompilator przestaje pomagać
Opisane wyżej problemy LP64 to te, które kompilator potrafi znaleźć. Te, których znaleźć nie umie — jak rekord o złym rozmiarze, który nigdy nie przechodzi przez rzutowanie — wychodzą dopiero, gdy testy faktycznie ruszą na platformie, i właśnie do tego są opisane wyżej asercje rozmiaru
Jeśli to wyjście 1-bitowe zaprowadziło cię do kodu ditheringu, wspierana droga dla renderowanych stron jest inna — konwersja w dół halftonem GDI poza rendererem, opisana w renderowaniu stron PDF do 1-bitowych bitmap monochromatycznych
Praca fundamentowa międzykompilatorowa opisana tutaj mieszka w bibliotece PDF dla Delphi, która dziś celuje w Delphi i C++Builder na Windows plus buildy headless Free Pascal na Linuksie i macOS; strona produktu wylicza wspierane kompilatory i platformy w całości