Delphi Linux64 folgt dem LP64-Datenmodell, dort sind Longint und LongWord 8 Bytes groß, während Delphi unter Windows (Win32 und Win64) und Free Pascal auf allen Zielen bei 4 bleiben. Beim Portieren von PDFlibPas, der PDF Library for Delphi, auf dcclinux64 stößt man auf diesen Unterschied auf drei verschiedene Arten, dazu kommt eine Zeigerbreiten-Falle: ein {$IFDEF WIN64}-Test, der einen zeigergroßen Integer auswählen soll, wählt still und leise die falsche Breite auf jedem 64-Bit-Ziel außerhalb von Windows
Keine dieser Fallen ist exotisch. Jede ist eine Angewohnheit, die Windows nur zufällig sicher macht: Integer schreiben, wo die RTL Longint verlangt, eine C-Struktur mit Longint-Feldern nachbilden oder fragen "ist das Win64?", obwohl die eigentliche Frage lautet "ist ein Zeiger 8 Bytes groß?". Code kann diese Angewohnheiten jahrelang mit sich herumtragen, ohne dass etwas passiert, weil auf Win32, Win64 und FPC beide Schreibweisen dasselbe bedeuten. Auf Delphi Linux64 hören sie auf, dasselbe zu bedeuten, und der Compiler bemerkt es als Erster
Alles Folgende dreht sich um das, was der Compiler findet. PDFlibrary.pas baut mit dcc32, dcc64 und dcclinux64 aus einem Quellbaum, und genau diese Probleme stehen zwischen den Windows-only-Angewohnheiten und diesem dritten Ziel
Was ändert sich, wenn Delphi auf Linux64 zielt?
Die Integertypen, die auf Delphi Linux64 ihre Breite ändern, sind genau Longint und LongWord; Integer und Cardinal bleiben auf jeder Delphi-Plattform bei 4 Bytes, und NativeInt folgt dem Zeiger. Windows ist LLP64, dort wachsen auf 64 Bit nur die Zeiger. Delphis 64-Bit-POSIX-Ziele (Linux64 sowie die 64-Bit-Compiler für macOS und iOS) sind LP64, dort wächst auch das C-long mit, und Embarcadero hat das Pascal-Longint mitziehen lassen
| Typ | Delphi Win32 | Delphi Win64 | Delphi Linux64 | Free Pascal |
|---|---|---|---|---|
Integer / Cardinal | 4 | 4 | 4 | 4 |
Longint / LongWord | 4 | 4 | 8 | 4 auf jedem Ziel |
NativeInt / Pointer | 4 | 8 | 8 | Zeigergröße des Ziels |
C long (Plattform-ABI) | 4 | 4 | 8 | folgt der OS-ABI |
Die Free-Pascal-Spalte ist die Falle für eine Cross-Compiler-Bibliothek. PDFlibPas liefert bereits einen Linux-Build über FPC aus, und FPC hält Longint auf Linux x86-64 bei 32 Bit. "Es läuft unter Linux" war also wahr und sagte dennoch nichts über Delphi Linux64 aus. Das FPC-Non-Windows-Backend entpuppte sich als die richtige Grundlage für den Delphi-Port, aber die darunterliegenden Typgrößen unterscheiden sich
Warum schneidet eine WIN64-Prüfung Zeiger unter Linux ab?
Die kurze Antwort: WIN64 bedeutet "64-Bit-Windows", nicht "64 Bit", und beide als Synonyme zu behandeln wählt die falsche Integerbreite. PDFlibDither, ein Floyd-Steinberg-Ditherer mit Error-Diffusion, der eine 24-Bit-Bitmap auf wenige Stufen pro Kanal reduziert, wandert mit roher Zeigerarithmetik durch drei Reihen von Fehlertermen, und der zeigergroße Integer hinter dieser Arithmetik muss der CPU-Breite folgen, nicht dem Betriebssystem. Die naive Conditional-Abfrage macht es falsch:
// Naiv: Breite vom OS gewählt, bekommt also jedes 64-Bit-Ziel außerhalb von Windows einen Integer
{$IFDEF WIN64}
Type
OSPtr= IntPtr;
{$ELSE}
Type
OSPtr= Integer;
{$ENDIF}
// Der Serpentinen-Scan setzt die Fehlerreihen-Zeiger Term für Term weiter
Inc(OSPtr(ErrorR), SizeOf(TErrorTerm)); // FPC x86-64: überschreibt nur die unteren 32 Bits
Dec(OSPtr(ErrorG), SizeOf(TErrorTerm)); // dcclinux64: E2064, linke Seite kann nicht zugewiesen werden
// Korrekt: überall ein zeigergroßer Typ
Type
OSPtr= TPLIntPtr; // PDFlibTypes: NativeInt außerhalb von Windows, IntPtr / Integer auf Win64 / Win32
Mit dieser Conditional-Abfrage löst sich OSPtr auf FPC für Linux x86-64, macOS x64 und ARM64 zu einem 4-Byte-Integer auf, und Inc(OSPtr(ErrorR), ...) interpretiert die ersten 4 Bytes eines 8-Byte-Zeigers als Integer und inkrementiert sie. Auf einer Little-Endian-CPU ist das die untere Hälfte der Adresse. Die obere Hälfte wird nie angefasst, deshalb versteckt sich der Fehler so gut: Das Ergebnis stimmt, bis die unteren 32 Bits überlaufen, und in dem Moment springt der Zeiger lautlos um 4 GiB zurück. FPC kompiliert den Variablen-Typecast ohne Murren. Delphi lehnt einen Variablen-Typecast auf einen Typ anderer Größe auf der linken Seite einer Zuweisung ab, deshalb stoppt dcclinux64 mit E2064 beim ersten Inc
Das ist die nützliche Asymmetrie hier: Ein Compiler, der den Typecast ablehnt, findet einen Zeigerbreiten-Fehler, den ein anderer Compiler stillschweigend akzeptiert, noch bevor der Code eine einzige Zeile auf dem neuen Ziel gelaufen ist. PDFlibPas nimmt dafür TPLIntPtr aus PDFlibTypes, das zuerst OS_WINDOWS testet und erst danach Win64 von Win32 unterscheidet, sodass Nicht-Windows-Ziele immer NativeInt bekommen
Die Regel, die wir mitgenommen haben: Leite die Zeigerbreite nie aus einem OS-Define ab. Brauchst du einen zeigergroßen Integer, nimm NativeInt / NativeUInt (Delphi und FPC haben beide) oder ein darauf aufbauendes Projekt-Alias und lass die Conditional-Abfrage ganz weg. Wenn du im Delphi-Code wirklich nach Breite verzweigen musst, ist CPU64BITS das Define, das die Frage beantwortet, die du eigentlich stellst
TStream-Overrides: die RTL-Signatur zeichenweise übernehmen
Ein TStream-Nachfahre auf Delphi Linux64 muss Read, Write und SetSize mit Longint-Parametern deklarieren und implementieren, exakt so, wie die RTL sie deklariert, weil Integer nicht mehr derselbe Typ ist. PDFlibPas hat einen eigenen TPDFlibWideFileStream für Unicode-Dateinamen, und dessen Klassendeklaration verwendete Longint. Der Implementation-Teil verwendete 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;
// Vorher: jahrelang akzeptiert auf Win32, Win64 und FPC, wo Integer = Longint gilt
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Integer): Longint;
// Nachher: der Implementation-Header wiederholt die Deklaration exakt
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Longint): Longint;
Begin
Result:= FileRead(FHandle, Buffer, Count);
If Result= - 1 Then
Result:= 0;
End;
Auf jedem Compiler, den die Bibliothek je gesehen hat, waren Integer und Longint derselbe 4-Byte-Typ, also war ein abweichender Header von einem passenden nicht zu unterscheiden. Auf Delphi Linux64 passt die Implementation nicht mehr zu ihrer eigenen Deklaration, und die Unit hört auf zu kompilieren. Das tut mehr weh, als es klein aussieht, denn PDFlibExtra ist eine Basis-Unit, von der der Großteil der Bibliothek abhängt, also hält ein einziger abweichender Implementation-Header die meisten Units vom Kompilieren ab
Für neuen Code läuft dieselbe Überlegung andersherum. Wenn du eine RTL-Methode überschreibst, übernimm ihre Parametertypen wörtlich, selbst wenn du "weißt", dass zwei Typen gleich sind. Delphi Linux64 ist einfach das erste Ziel, an dem dieses Wissen aufhört zu stimmen
Welche Records brauchen Integer statt Longint, und welche müssen es behalten?
Ein Record, der ein Dateiformat oder eine Windows-Struktur abbildet, muss feste Typbreiten verwenden (Integer, Cardinal, Int32), während ein Record, der eine mit long deklarierte C-Struktur abbildet, Longint / LongWord behalten sollte. Beide Fälle sehen im Quelltext identisch aus und brauchen gegenteilige Antworten, denn das Windows-LONG ist immer 32 Bit breit und das C-long ist, was die Plattform-ABI sagt
Type
// Uniscribe GOFFSET: zwei Windows-LONGs, zusammen immer 8 Bytes
TGoffset= Packed Record
du: Integer; // war Longint: der Record wuchs auf Delphi Linux64 auf 16 Bytes
dv: Integer;
End;
// EMF LOGCOLORSPACE: Header wird per festem 68-Byte-Move aus den Record-Daten gefüllt
TPLEMFLogColorSpace= Packed Record
Signature, Version, Size, ColorType, Intent: Cardinal;
Endpoints: Array[0..8] Of Integer; // war Longint: aus 36 Bytes wurden 72
Gamma: Array[0..2] Of Cardinal;
Filename: Array[0..259] Of WideChar;
End;
// PDFium FPDF_FILEACCESS: m_FileLen ist ein C-unsigned long, also ist LongWord richtig
FPDF_FILEACCESS= Record
m_FileLen: LongWord; // 4 Bytes unter Windows, 8 auf LP64, wie auf der C-Seite
m_GetBlock: TFPDF_GetBlock;
m_Param: Pointer;
End;
Der EMF-Fall zeigt, wie leise das scheitert. Der Importer kopiert 68 Bytes in TPLEMFLogColorSpace: 20 Bytes Header, 36 Bytes CIE-Endpoints, 12 Bytes Gamma. Mit Longint-Endpoints auf LP64 würden dieselben 68 Bytes in einem 72-Byte-Endpoint-Array landen, die Gamma-Werte würden nie geschrieben werden, und nichts würde einen Fehler werfen. Beide Records liegen auf Windows-only-Codepfaden, und sie verwenden ohnehin feste Typbreiten, denn ein Record, der Bytes auf der Platte abbildet, sollte nicht davon abhängen, welcher Compiler ihn gerade baut
Die PDFium-Bindung ist das Gegenbeispiel. Ihr m_FileLen bildet ein C-unsigned long ab, das unter Windows 32 Bit und auf LP64 64 Bit hat, und LongWord bildet das auf beiden exakt ab. In einem Aufräum-Durchgang in Cardinal umzuschreiben würde es brechen, sobald der PDFium-Host einen POSIX-Loader bekommt. Der PDFium-Host ist Windows-only, also ist das eine Notiz für die Zukunft. Die Calling-Convention-Seite von C-Bindings hat ihre eigenen Fallen, behandelt in dem Artikel zur PDF-Library-DLL und den ActiveX- und dylib-Bindings
Ein billiger Schutz für jeden Byte-abbildenden Record: Prüfe seine Größe einmalig beim Start, zum Beispiel Assert(SizeOf(TGoffset) = 8) im initialization-Abschnitt. Das kostet nichts und macht aus einer stillen Layout-Änderung einen sofortigen Fehler beim ersten Lauf auf einem neuen Ziel
Die Conditional-Defines richtig setzen: DELPHI_LINUX, Android und Types
Bevor eines der Typ-Probleme auftauchen konnte, musste Delphi unter Linux aufhören, sich als Windows auszugeben. PDFlibDefines.inc definierte für Delphi Linux überhaupt nichts, also fiel der Compiler direkt in die Windows-/VCL-Defaults, und der erste Fehler war ein nicht gefundenes Vcl.Graphics. Die Defines lotsen Delphi Linux auf dasselbe Backend, das FPC auf Nicht-Windows-Zielen nutzt: Cairo-Rendering, fontconfig, OpenSSL und libcurl zur Laufzeit geladen
{$IFDEF LINUX}
{$IFNDEF FPC}
{$IFNDEF ANDROID}
{$DEFINE DELPHI_LINUX}
{$DEFINE NOVCL}
{$DEFINE MD5_MAC}
{$DEFINE NOCRYPTAPI}
{$DEFINE USE_Cairo}
{$ENDIF}
{$ENDIF}
{$ENDIF}
Zwei Details in diesem Block übersieht man leicht. Der ANDROID-Ausschluss steckt darin, weil Delphi LINUX auch für Android-Ziele definiert, und dieses Backend deckt Android nicht ab. Und das neue Define ist bewusst getrennt vom bestehenden Delphi-macOS-Zweig (OS_MAC ohne FPC), der von Macapi-Units abhängt; das Linux-Define baut stattdessen auf dem FPC-Non-Windows-Pfad auf
Der andere wiederkehrende Fehler war TPoint und TRect, die nicht deklariert waren. Unter Windows erreichen diese Typen die meisten Units indirekt, über die Windows-Unit. Auf Delphi Linux gibt es keine Windows-Unit in der uses-Klausel, deshalb listen Units wie PDFlibFont und PDFlibPageTree jetzt Types explizit unter DELPHI_LINUX auf. Wenn dein eigener Code unter Windows nur kompiliert, weil Windows System.Types mitschleift, erwarte dieselbe Wand. Das allgemeine Muster von Code, der nur zufällig auf einem Compiler läuft, ist Thema von fünf Delphi-Angewohnheiten, die nur zufällig funktionieren, bis FPC sie entlarvt
Wo der Compiler aufhört zu helfen
Die LP64-Probleme oben sind die, die ein Compiler finden kann. Die, die er nicht finden kann, etwa ein Record in der falschen Größe, der nie durch einen Typecast läuft, zeigen sich erst, wenn Tests tatsächlich auf dem Ziel laufen, und genau dafür sind die oben beschriebenen Größen-Assertions da
Wenn dich die 1-Bit-Ausgabe überhaupt erst zum Dithering-Code geführt hat, ist der unterstützte Weg für gerenderte Seiten ein anderer: die GDI-Halftone-Herabsetzung außerhalb des Renderers, beschrieben in PDF-Seiten in 1-Bit-Monochrom-Bitmaps rendern
Die hier beschriebene Cross-Compiler-Grundlage steckt in der PDF Library for Delphi, die heute Delphi und C++Builder unter Windows plus headless-Free-Pascal-Builds unter Linux und macOS zielt; die Produktseite listet die unterstützten Compiler und Plattformen vollständig auf