Delphi Linux64 volgt het LP64-datamodel, dus Longint en LongWord zijn daar 8 bytes, terwijl Delphi op Windows (Win32 en Win64) en Free Pascal op elke target ze op 4 houden. PDFlibPas, de PDF-bibliotheek voor Delphi, naar dcclinux64 porteren stuit op drie manieren tegen dat verschil, plus een valkuil rond pointerbreedte: een {$IFDEF WIN64}-test die een integer-type op pointerbreedte moet opleveren, kiest op elke niet-Windows 64-bit target stilletjes de verkeerde breedte
Geen van deze valkuilen is exotisch. Elk ervan is een gewoonte die Windows toevallig veilig maakt: Integer schrijven waar de RTL Longint zegt, een C-structuur spiegelen met Longint-velden, of de vraag "is dit Win64?" stellen terwijl de echte vraag luidt "is een pointer 8 bytes?". Code kan zulke gewoontes jarenlang meedragen zonder gevolgen, want op Win32, Win64 en FPC betekenen beide schrijfwijzen hetzelfde. Op Delphi Linux64 betekenen ze ineens niet meer hetzelfde, en de compiler merkt het als eerste
Alles hieronder gaat over wat de compiler vangt. PDFlibrary.pas bouwt met dcc32, dcc64 en dcclinux64 vanuit één bronboom, en dit zijn de problemen die staan tussen Windows-only gewoontes en die derde target
Wat verandert er als Delphi Linux64 als target heeft?
De integer-types die op Delphi Linux64 van breedte veranderen zijn precies Longint en LongWord; Integer en Cardinal blijven op elk Delphi-platform 4 bytes, en NativeInt volgt de pointer. Windows is LLP64, waar op 64-bit alleen pointers groeien. De 64-bit POSIX-targets van Delphi (Linux64 en de 64-bit macOS- en iOS-compilers) zijn LP64, waar ook de C long groeit, en Embarcadero heeft de Pascal-Longint dat mee laten doen
| Type | Delphi Win32 | Delphi Win64 | Delphi Linux64 | Free Pascal |
|---|---|---|---|---|
Integer / Cardinal | 4 | 4 | 4 | 4 |
Longint / LongWord | 4 | 4 | 8 | 4 op elke target |
NativeInt / Pointer | 4 | 8 | 8 | pointergrootte van de target |
C long (platform-ABI) | 4 | 4 | 8 | volgt de OS-ABI |
De Free Pascal-kolom is de valkuil voor een bibliotheek met meerdere compilers. PDFlibPas levert al een Linux-build via FPC, en FPC houdt Longint op Linux x86-64 op 32 bits. "Het werkt op Linux" was dus waar en vertelde ons niets over Delphi Linux64. De FPC non-Windows-backend bleek de juiste basis voor de Delphi-port, maar de typegroottes eronder verschillen
Waarom kapt een WIN64-check pointers af op Linux?
Het korte antwoord: WIN64 betekent "64-bit Windows", niet "64-bit", en de twee als synoniemen behandelen kiest de verkeerde integerbreedte. PDFlibDither, een Floyd-Steinberg-ditherer met error diffusion die een 24-bit bitmap terugbrengt tot een paar niveaus per kanaal, loopt met pure pointeraritmetiek door drie rijen fouttermen, en het integer-type op pointerbreedte achter die rekenkunst moet de CPU-breedte volgen, niet het besturingssysteem. De naïeve conditional zit ernaast:
// Naïef: breedte gekozen door het OS, dus elke niet-Windows 64-bit target krijgt Integer
{$IFDEF WIN64}
Type
OSPtr= IntPtr;
{$ELSE}
Type
OSPtr= Integer;
{$ENDIF}
// De serpentine-scan zet de foutrij-pointers per stap één term verder
Inc(OSPtr(ErrorR), SizeOf(TErrorTerm)); // FPC x86-64: herschrijft alleen de lage 32 bits
Dec(OSPtr(ErrorG), SizeOf(TErrorTerm)); // dcclinux64: E2064, left side cannot be assigned to
// Correct: één type op pointerbreedte overal
Type
OSPtr= TPLIntPtr; // PDFlibTypes: NativeInt buiten Windows, IntPtr / Integer op Win64 / Win32
Met die conditional valt OSPtr op FPC voor Linux x86-64, macOS x64 en ARM64 terug op een 4-byte Integer, en Inc(OSPtr(ErrorR), ...) herinterpreteert de eerste 4 bytes van een 8-byte pointer als integer en telt er iets bij op. Op een little-endian-CPU is dat de lage helft van het adres. De hoge helft wordt nooit aangeraakt, en juist daarom verbergt deze fout zich zo goed: het resultaat klopt totdat de lage 32 bits omslaan, en op dat moment springt de pointer stilletjes 4 GiB terug. FPC compileert de variable typecast zonder mopperen. Delphi weigert een variable typecast naar een type met een andere grootte links van een toewijzing, dus dcclinux64 stopt bij de eerste Inc met E2064
Dat is hier de nuttige asymmetrie: een compiler die de typecast weigert, vangt een pointerbreedte-fout die een andere compiler stilletjes accepteert, nog voordat de code één regel op de nieuwe target heeft gedraaid. PDFlibPas gebruikt hiervoor TPLIntPtr uit PDFlibTypes, dat eerst op OS_WINDOWS test en pas daarna Win64 van Win32 onderscheidt, zodat niet-Windows-targets altijd NativeInt krijgen
De regel die we eraan overhielden: leid de pointerbreedte nooit af uit een OS-define. Heb je een integer op pointerbreedte nodig, gebruik dan NativeInt / NativeUInt (zowel Delphi als FPC heeft die) of een projectalias die daarop bouwt, en laat de conditional helemaal vallen. Moet je in Delphi-code echt op breedte vertakken, dan is CPU64BITS de define die de vraag beantwoordt die je eigenlijk stelt
TStream-overrides: kopieer de RTL-handtekening, teken voor teken
Een TStream-afgeleide op Delphi Linux64 moet Read, Write en SetSize declareren én implementeren met Longint-parameters, precies zoals de RTL ze declareert, want Integer is niet langer hetzelfde type. PDFlibPas heeft een eigen TPDFlibWideFileStream voor Unicode-bestandsnamen, en de klassedeclaratie gebruikte wel degelijk Longint. De implementation-sectie gebruikte 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;
// Voorheen: jarenlang geaccepteerd op Win32, Win64 en FPC, waar Integer = Longint
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Integer): Longint;
// Daarna: de implementation-header herhaalt de declaratie exact
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Longint): Longint;
Begin
Result:= FileRead(FHandle, Buffer, Count);
If Result= - 1 Then
Result:= 0;
End;
Op elke compiler die de bibliotheek ooit was tegengekomen, waren Integer en Longint hetzelfde 4-byte type, dus een header die niet matchte was niet te onderscheiden van een matchende. Op Delphi Linux64 matcht de implementation niet langer met de eigen declaratie en stopt de unit met compileren. Deze doet meer pijn dan zijn omvang doet vermoeden, want PDFlibExtra is een funderingsunit waar het grootste deel van de bibliotheek op leunt, dus één niet-matchende implementation-header houdt de meeste units uit de lucht
Voor nieuwe code geldt hetzelfde in omgekeerde richting. Wanneer je een RTL-methode override, kopieer dan de parametertypes letterlijk, ook als je "weet" dat twee types hetzelfde zijn. Delphi Linux64 is simpelweg de eerste target waar dat inzicht ophoudt te kloppen
Welke records hebben Integer nodig in plaats van Longint, en welke moeten Longint houden?
Een record dat een bestandsformaat of een Windows-structuur spiegelt, moet types met vaste breedte gebruiken (Integer, Cardinal, Int32), terwijl een record dat een C-structuur spiegelt die met long gedeclareerd is, Longint / LongWord moet houden. Beide gevallen zien er in de bron identiek uit en vragen om tegenovergestelde antwoorden, want de Windows-LONG is altijd 32 bits en de C long is wat de platform-ABI zegt
Type
// Uniscribe GOFFSET: twee Windows-LONGs, samen altijd 8 bytes
TGoffset= Packed Record
du: Integer; // was Longint: het record groeide op Delphi Linux64 naar 16 bytes
dv: Integer;
End;
// EMF LOGCOLORSPACE: header gevuld met een vaste Move van 68 bytes uit de recorddata
TPLEMFLogColorSpace= Packed Record
Signature, Version, Size, ColorType, Intent: Cardinal;
Endpoints: Array[0..8] Of Integer; // was Longint: 36 bytes werd 72
Gamma: Array[0..2] Of Cardinal;
Filename: Array[0..259] Of WideChar;
End;
// PDFium FPDF_FILEACCESS: m_FileLen is een C unsigned long, dus LongWord klopt
FPDF_FILEACCESS= Record
m_FileLen: LongWord; // 4 bytes op Windows, 8 op LP64, gelijk aan de C-kant
m_GetBlock: TFPDF_GetBlock;
m_Param: Pointer;
End;
Het EMF-geval laat zien hoe stilletjes dit misgaat. De importer kopieert 68 bytes in TPLEMFLogColorSpace: 20 bytes header, 36 bytes CIE-eindpunten, 12 bytes gamma. Met Longint-eindpunten op LP64 zouden diezelfde 68 bytes in een 72-byte array voor eindpunten belanden, de gammawaarden zouden nooit worden weggeschreven en niets zou een foutmelding geven. Beide records zitten op Windows-only codepaden, en ze gebruiken desondanks types met vaste breedte, want een record dat bytes op schijf spiegelt hoort niet af te hangen van de toevallige bouwcompiler
De PDFium-binding is het tegenbeeld. Zijn m_FileLen spiegelt een C unsigned long, die op Windows 32 bits is en op LP64 64 bits, en LongWord volgt dat op beide exact. Het in een opruimronde herschrijven naar Cardinal breekt het zodra de PDFium-host een POSIX-loader krijgt. De PDFium-host is Windows-only, dus dit is een notitie voor later. De calling-convention-kant van C-bindingen heeft eigen valkuilen; die worden behandeld in het artikel over de DLL-, ActiveX- en dylib-bindingen van de PDF Library
Een goedkope bewaking voor elk record dat bytes spiegelt: assert één keer de grootte bij het opstarten, bijvoorbeeld Assert(SizeOf(TGoffset) = 8) in een initialization-sectie. Het kost niets en maakt van een stille layoutwijziging een directe foutmelding bij de eerste run op een nieuwe target
De conditional-defines goed zetten: DELPHI_LINUX, Android en Types
Voordat een van de typeproblemen zich kon tonen, moest Delphi op Linux eerst stoppen met doen alsof het Windows was. PDFlibDefines.inc definieerde voor Delphi Linux helemaal niets, dus de compiler viel rechtstreeks in de Windows/VCL-defaults en de eerste foutmelding luidde dat Vcl.Graphics niet gevonden werd. De defines leiden Delphi Linux naar dezelfde backend die FPC op niet-Windows-targets gebruikt: Cairo-rendering, fontconfig, OpenSSL en libcurl die op runtime worden geladen
{$IFDEF LINUX}
{$IFNDEF FPC}
{$IFNDEF ANDROID}
{$DEFINE DELPHI_LINUX}
{$DEFINE NOVCL}
{$DEFINE MD5_MAC}
{$DEFINE NOCRYPTAPI}
{$DEFINE USE_Cairo}
{$ENDIF}
{$ENDIF}
{$ENDIF}
Twee details in dat blok zijn makkelijk over het hoofd te zien. De ANDROID-uitsluiting staat er omdat Delphi LINUX ook voor Android-targets definieert, en deze backend dekt Android niet. En de nieuwe define is met opzet gescheiden van de bestaande Delphi macOS-tak (OS_MAC zonder FPC), die van Macapi-units afhangt; de Linux-define bouwt in plaats daarvan op het FPC non-Windows-pad
De andere terugkerende foutmelding was dat TPoint en TRect onbekend bleven. Op Windows bereiken die types de meeste units indirect, via de unit Windows. Op Delphi Linux staat er geen Windows in de uses-clausule, dus units zoals PDFlibFont en PDFlibPageTree zetten onder DELPHI_LINUX nu expliciet Types op. Compileert jouw code op Windows alleen omdat Windows System.Types meesleept, reken dan op dezelfde muur. Het algemene patroon van code die alleen toevallig op één compiler werkt, is het onderwerp van vijf Delphi-gewoontes die alleen toevallig werken totdat FPC ze ontmaskert
Waar de compiler ophoudt te helpen
De LP64-problemen hierboven zijn de problemen die een compiler kan vinden. Wat hij níét kan vinden — zoals een record met de verkeerde grootte dat nooit door een typecast gaat — komt pas boven water wanneer tests echt op de target draaien, en precies daarvoor dienen de size-assertions van hierboven
Als 1-bit output de reden was dat je bij de dithering-code uitkwam: de ondersteunde route voor gerenderde pagina's loopt anders, namelijk via GDI-halftone-downconversion buiten de renderer, beschreven in PDF-pagina's renderen naar 1-bit monochrome bitmaps
Het hier beschreven cross-compiler-fundament zit in de PDF Library for Delphi, die vandaag Delphi en C++Builder op Windows ondersteunt plus headless Free Pascal-builds op Linux en macOS; de productpagina zet de ondersteunde compilers en platforms volledig uiteen