Delphi Linux64 follows the LP64 data model, so Longint and LongWord are 8 bytes there, while Delphi on Windows (Win32 and Win64) and Free Pascal on every target keep them at 4. Bringing PDFlibPas, the PDF Library for Delphi, to dcclinux64 runs into that difference in three distinct ways, plus a pointer-width trap: a {$IFDEF WIN64} test used to pick a pointer-sized integer quietly picks the wrong width on every non-Windows 64-bit target
None of these traps is exotic. Each one is a habit that Windows makes safe by accident: writing Integer where the RTL says Longint, mirroring a C structure with Longint fields, or asking "is this Win64?" when the real question is "is a pointer 8 bytes?". Code can carry those habits for years without consequence, because on Win32, Win64 and FPC the two spellings mean the same thing. On Delphi Linux64 they stop meaning the same thing, and the compiler is the first to notice
Everything below is about what the compiler catches. PDFlibrary.pas builds with dcc32, dcc64 and dcclinux64 from one source tree, and these are the issues that stand between Windows-only habits and that third target
What changes when Delphi targets Linux64?
The integer types that change width on Delphi Linux64 are exactly Longint and LongWord; Integer and Cardinal stay at 4 bytes on every Delphi platform, and NativeInt follows the pointer. Windows is LLP64, where only pointers grow on 64-bit. Delphi's 64-bit POSIX targets (Linux64, and the macOS and iOS 64-bit compilers) are LP64, where the C long grows too, and Embarcadero made the Pascal Longint follow it
| Type | Delphi Win32 | Delphi Win64 | Delphi Linux64 | Free Pascal |
|---|---|---|---|---|
Integer / Cardinal | 4 | 4 | 4 | 4 |
Longint / LongWord | 4 | 4 | 8 | 4 on every target |
NativeInt / Pointer | 4 | 8 | 8 | pointer size of the target |
C long (platform ABI) | 4 | 4 | 8 | follows the OS ABI |
The Free Pascal column is the trap for a cross-compiler library. PDFlibPas already ships a Linux build through FPC, and FPC keeps Longint at 32 bits on Linux x86-64. So "it works on Linux" was true and told us nothing about Delphi Linux64. The FPC non-Windows backend turned out to be the right foundation for the Delphi port, but the type sizes underneath it differ
Why does a WIN64 check truncate pointers on Linux?
The short answer: WIN64 means "64-bit Windows", not "64-bit", and treating the two as synonyms picks the wrong integer width. PDFlibDither, a Floyd-Steinberg error-diffusion ditherer that reduces a 24-bit bitmap to a few levels per channel, walks three rows of error terms with raw pointer arithmetic, and the pointer-sized integer behind that arithmetic has to follow CPU width, not the operating system. The naive conditional gets it wrong:
// Naive: width chosen by OS, so every non-Windows 64-bit target gets Integer
{$IFDEF WIN64}
Type
OSPtr= IntPtr;
{$ELSE}
Type
OSPtr= Integer;
{$ENDIF}
// The serpentine scan steps the error-row pointers one term at a time
Inc(OSPtr(ErrorR), SizeOf(TErrorTerm)); // FPC x86-64: rewrites the low 32 bits only
Dec(OSPtr(ErrorG), SizeOf(TErrorTerm)); // dcclinux64: E2064, left side cannot be assigned to
// Correct: one pointer-sized type everywhere
Type
OSPtr= TPLIntPtr; // PDFlibTypes: NativeInt off Windows, IntPtr / Integer on Win64 / Win32
With that conditional, OSPtr resolves to a 4-byte Integer on FPC for Linux x86-64, macOS x64 and ARM64, and Inc(OSPtr(ErrorR), ...) reinterprets the first 4 bytes of an 8-byte pointer as an integer and increments them. On a little-endian CPU that is the low half of the address. The high half is never touched, which is why the mistake hides so well: the result is correct right up until the low 32 bits wrap, at which point the pointer silently jumps back by 4 GiB. FPC compiles the variable typecast without complaint. Delphi refuses a variable typecast to a type of a different size on the left side of an assignment, so dcclinux64 stops on E2064 at the first Inc
That is the useful asymmetry here: a compiler that refuses the typecast finds a pointer-width mistake that another compiler accepts silently, before the code has run a single line on the new target. PDFlibPas uses TPLIntPtr from PDFlibTypes for this, which tests OS_WINDOWS first and only then distinguishes Win64 from Win32, so non-Windows targets always get NativeInt
The rule we took away: never infer pointer width from an OS define. If you need a pointer-sized integer, use NativeInt / NativeUInt (both Delphi and FPC have them) or a project alias built on them, and skip the conditional entirely. If you genuinely need to branch on width in Delphi code, CPU64BITS is the define that answers the question you are actually asking
TStream overrides: copy the RTL signature, character for character
A TStream descendant on Delphi Linux64 must declare and implement Read, Write and SetSize with Longint parameters, exactly as the RTL declares them, because Integer is no longer the same type. PDFlibPas has its own TPDFlibWideFileStream for Unicode file names, and its class declaration did use Longint. The implementation section used 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;
// Before: accepted for years on Win32, Win64 and FPC, where Integer = Longint
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Integer): Longint;
// After: the implementation header repeats the declaration exactly
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Longint): Longint;
Begin
Result:= FileRead(FHandle, Buffer, Count);
If Result= - 1 Then
Result:= 0;
End;
On every compiler the library had ever met, Integer and Longint were the same 4-byte type, so a mismatched header was indistinguishable from a matching one. On Delphi Linux64 the implementation no longer matches its own declaration and the unit stops compiling. This one hurts more than its size suggests, because PDFlibExtra is a foundation unit that most of the library depends on, so one mismatched implementation header keeps most of the units from compiling
The same reasoning runs the other way for new code. When you override an RTL method, copy its parameter types literally, even when you "know" two types are the same. Delphi Linux64 is simply the first target where that knowledge stops being true
Which records need Integer instead of Longint, and which must keep it?
A record that mirrors a file format or a Windows structure must use fixed-width types (Integer, Cardinal, Int32), while a record that mirrors a C structure declared with long should keep Longint / LongWord. The two cases look identical in source and need opposite answers, because the Windows LONG is always 32 bits and the C long is whatever the platform ABI says
Type
// Uniscribe GOFFSET: two Windows LONGs, always 8 bytes in total
TGoffset= Packed Record
du: Integer; // was Longint: the record grew to 16 bytes on Delphi Linux64
dv: Integer;
End;
// EMF LOGCOLORSPACE: header filled by a fixed 68-byte Move from the record data
TPLEMFLogColorSpace= Packed Record
Signature, Version, Size, ColorType, Intent: Cardinal;
Endpoints: Array[0..8] Of Integer; // was Longint: 36 bytes became 72
Gamma: Array[0..2] Of Cardinal;
Filename: Array[0..259] Of WideChar;
End;
// PDFium FPDF_FILEACCESS: m_FileLen is a C unsigned long, so LongWord is right
FPDF_FILEACCESS= Record
m_FileLen: LongWord; // 4 bytes on Windows, 8 on LP64, same as the C side
m_GetBlock: TFPDF_GetBlock;
m_Param: Pointer;
End;
The EMF case shows how quietly this fails. The importer copies 68 bytes into TPLEMFLogColorSpace: 20 bytes of header, 36 bytes of CIE endpoints, 12 bytes of gamma. With Longint endpoints on LP64, those same 68 bytes would land in a 72-byte endpoint array, the gamma values would never be written, and nothing would raise an error. Both records sit on Windows-only code paths, and they use fixed-width types anyway, because a record that mirrors bytes on disk should not depend on which compiler happens to build it
The PDFium binding is the counter-example. Its m_FileLen mirrors a C unsigned long, which is 32 bits on Windows and 64 bits on LP64, and LongWord tracks that exactly on both. Rewriting it as Cardinal in a cleanup pass would break it the moment the PDFium host gains a POSIX loader. The PDFium host is Windows-only, so this is a note for the future. The calling-convention side of C bindings has traps of its own, covered in the article on the PDF Library DLL, ActiveX and dylib bindings
A cheap guard for any record that mirrors bytes: assert its size once at startup, for example Assert(SizeOf(TGoffset) = 8) in an initialization section. It costs nothing and turns a silent layout change into an immediate failure on the first run of a new target
Getting the conditional defines right: DELPHI_LINUX, Android and Types
Before any of the type issues could surface, Delphi on Linux had to stop pretending to be Windows. PDFlibDefines.inc defined nothing at all for Delphi Linux, so the compiler fell straight into the Windows/VCL defaults, and the first error was Vcl.Graphics not being found. The defines route Delphi Linux onto the same backend FPC uses on non-Windows targets: Cairo rendering, fontconfig, OpenSSL and libcurl loaded at run time
{$IFDEF LINUX}
{$IFNDEF FPC}
{$IFNDEF ANDROID}
{$DEFINE DELPHI_LINUX}
{$DEFINE NOVCL}
{$DEFINE MD5_MAC}
{$DEFINE NOCRYPTAPI}
{$DEFINE USE_Cairo}
{$ENDIF}
{$ENDIF}
{$ENDIF}
Two details in that block are easy to miss. The ANDROID exclusion is there because Delphi defines LINUX for Android targets as well, and this backend does not cover Android. And the new define is deliberately separate from the existing Delphi macOS branch (OS_MAC without FPC), which depends on Macapi units; the Linux define builds on the FPC non-Windows path instead
The other recurring error was TPoint and TRect going undeclared. On Windows those types reach most units indirectly, through the Windows unit. On Delphi Linux there is no Windows unit in the uses clause, so units such as PDFlibFont and PDFlibPageTree now list Types explicitly under DELPHI_LINUX. If your own code compiles on Windows only because Windows drags System.Types along, expect the same wall. The general pattern of code that only works on one compiler by coincidence is the subject of five Delphi habits that only work by accident until FPC exposes them
Where the compiler stops helping
The LP64 issues above are the ones a compiler can find. The ones it cannot find, such as a record that is the wrong size but never passes through a typecast, only show up when tests actually run on the target, which is exactly what the size assertions described above are for
If 1-bit output is what brought you to the dithering code in the first place, the supported route for rendered pages is a different one, GDI halftone down-conversion outside the renderer, described in rendering PDF pages to 1-bit monochrome bitmaps
The cross-compiler groundwork described here lives in the PDF Library for Delphi, which today targets Delphi and C++Builder on Windows plus headless Free Pascal builds on Linux and macOS; the product page lists the supported compilers and platforms in full