Technisch artikel

Delphi Linux64 LP64-valkuilen: Longint, pointers en WIN64

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

TypeDelphi Win32Delphi Win64Delphi Linux64Free Pascal
Integer / Cardinal4444
Longint / LongWord4484 op elke target
NativeInt / Pointer488pointergrootte van de target
C long (platform-ABI)448volgt 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

Matrix van de breedte van Integer, Longint, NativeInt en C long op de Delphi-targets Win32, Win64, Linux64 en Free Pascal in de PDF-bibliotheek PDFlibPas
alleen Longint, LongWord en de C long worden breder op Delphi Linux64, terwijl FPC ze op elke target op 4 bytes houdt

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

Beslissingsstroom voor types op pointerbreedte in PDFlibPas-Delphi-code: een WIN64-define kiest een 4-byte Integer op niet-Windows 64-bit targets totdat NativeInt het voorkomt
de WIN64-define vraagt naar het besturingssysteem, dus types op pointerbreedte moeten van NativeInt of een CPU64BITS-test komen

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

Beslissingsmatrix voor recordvelden in Delphi op LP64: Integer met vaste breedte voor bytes op schijf, Longint behouden voor C-long-bindingen in PDFlibPas
records die bytes spiegelen krijgen vaste breedtes en alleen bindingen van een C long houden Longint, met een éénregelige size-assertion als vangnet
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