Article technique

Pièges Delphi Linux64 LP64 : Longint, pointeurs et WIN64

Delphi Linux64 suit le modèle de données LP64 : Longint et LongWord y font 8 octets, alors que Delphi sous Windows (Win32 et Win64) comme Free Pascal sur toutes ses cibles les maintiennent à 4. Porter PDFlibPas, la PDF Library pour Delphi, vers dcclinux64 se heurte à cette différence de trois façons distinctes, plus un piège lié à la largeur des pointeurs : un test {$IFDEF WIN64} censé choisir un entier de la taille d'un pointeur choisit en silence la mauvaise largeur sur toutes les cibles 64 bits non Windows

Aucun de ces pièges n'a rien d'exotique. Chacun est une habitude que Windows rend inoffensive par accident : écrire Integer là où la RTL dit Longint, refléter une structure C avec des champs Longint, ou demander « est-ce du Win64 ? » quand la vraie question est « est-ce qu'un pointeur fait 8 octets ? ». Du code peut porter ces habitudes pendant des années sans conséquence, car sous Win32, Win64 et FPC les deux orthographes désignent la même chose. Sur Delphi Linux64, elles cessent de signifier la même chose, et le compilateur est le premier à le remarquer

Tout ce qui suit concerne ce que le compilateur attrape. PDFlibrary.pas compile avec dcc32, dcc64 et dcclinux64 à partir d'une seule arborescence source, et ce sont précisément ces problèmes qui séparent les habitudes Windows-only de cette troisième cible

Qu'est-ce qui change quand Delphi cible Linux64 ?

Les types entiers qui changent de largeur sur Delphi Linux64 sont exactement Longint et LongWord ; Integer et Cardinal restent à 4 octets sur toutes les plateformes Delphi, et NativeInt suit le pointeur. Windows est LLP64, où seuls les pointeurs grossissent en 64 bits. Les cibles POSIX 64 bits de Delphi (Linux64, et les compilateurs macOS et iOS 64 bits) sont LP64, où le long C grossit aussi, et Embarcadero a fait suivre au Longint Pascal

TypeDelphi Win32Delphi Win64Delphi Linux64Free Pascal
Integer / Cardinal4444
Longint / LongWord4484 sur toutes les cibles
NativeInt / Pointer488taille du pointeur de la cible
C long (ABI de la plateforme)448suit l'ABI de l'OS

La colonne Free Pascal est le piège pour une bibliothèque multi-compilateurs. PDFlibPas livre déjà un build Linux via FPC, et FPC maintient Longint à 32 bits sous Linux x86-64. « Ça marche sous Linux » était donc vrai sans rien nous apprendre sur Delphi Linux64. Le backend non-Windows de FPC s'est révélé être la bonne fondation pour le portage Delphi, mais les tailles de types en dessous diffèrent

Matrice des largeurs de Integer, Longint, NativeInt et du C long sur les cibles Delphi Win32, Win64, Linux64 et Free Pascal dans la PDFlibPas PDF library
seuls Longint, LongWord et le C long s'élargissent sur Delphi Linux64, tandis que FPC les garde à 4 octets sur toutes les cibles

Pourquoi un test WIN64 tronque-t-il les pointeurs sous Linux ?

La réponse courte : WIN64 signifie « Windows 64 bits », pas « 64 bits », et traiter les deux comme synonymes choisit la mauvaise largeur d'entier. PDFlibDither, un ditherer Floyd-Steinberg à diffusion d'erreur qui réduit un bitmap 24 bits à quelques niveaux par canal, parcourt trois lignes de termes d'erreur en arithmétique de pointeurs brute, et l'entier de la taille d'un pointeur derrière cette arithmétique doit suivre la largeur du CPU, pas celle de l'OS. Le conditionnel naïf se trompe :

// Naïf : largeur choisie par l'OS, donc toute cible 64 bits non Windows reçoit Integer
{$IFDEF WIN64}
Type
  OSPtr= IntPtr;
{$ELSE}
Type
  OSPtr= Integer;
{$ENDIF}

// Le balayage serpentin avance les pointeurs des lignes d'erreur un terme à la fois
Inc(OSPtr(ErrorR), SizeOf(TErrorTerm));  // FPC x86-64 : ne réécrit que les 32 bits bas
Dec(OSPtr(ErrorG), SizeOf(TErrorTerm));  // dcclinux64 : E2064, left side cannot be assigned to

// Correct : un type de la taille d'un pointeur partout
Type
  OSPtr= TPLIntPtr;  // PDFlibTypes : NativeInt hors Windows, IntPtr / Integer sous Win64 / Win32

Avec ce conditionnel, OSPtr se résout en un Integer de 4 octets sur FPC pour Linux x86-64, macOS x64 et ARM64, et Inc(OSPtr(ErrorR), ...) réinterprète les 4 premiers octets d'un pointeur de 8 octets comme un entier et les incrémente. Sur un CPU little-endian, c'est la moitié basse de l'adresse. La moitié haute n'est jamais touchée, voilà pourquoi l'erreur se cache si bien : le résultat est correct jusqu'à ce que les 32 bits bas bouclent, moment où le pointeur revient silencieusement en arrière de 4 GiB. FPC compile le typecast de variable sans broncher. Delphi refuse un typecast de variable vers un type de taille différente à gauche d'une affectation, donc dcclinux64 s'arrête sur E2064 au premier Inc

C'est l'asymétrie utile ici : un compilateur qui refuse le typecast détecte une erreur de largeur de pointeur qu'un autre compilateur accepte en silence, avant même que le code n'ait tourné une seule ligne sur la nouvelle cible. PDFlibPas utilise TPLIntPtr de PDFlibTypes pour cela, qui teste OS_WINDOWS en premier et ne distingue ensuite Win64 de Win32, si bien que les cibles non Windows reçoivent toujours NativeInt

La règle retenue : ne déduisez jamais la largeur d'un pointeur d'un define d'OS. Si vous avez besoin d'un entier de la taille d'un pointeur, utilisez NativeInt / NativeUInt (Delphi et FPC les ont tous les deux) ou un alias de projet construit dessus, et sautez complètement le conditionnel. Si vous devez vraiment brancher sur la largeur dans du code Delphi, CPU64BITS est le define qui répond à la question que vous posez réellement

Flux de décision pour les types de largeur pointeur dans le code Delphi de PDFlibPas : un define WIN64 choisit un Integer de 4 octets sur les cibles 64 bits non Windows jusqu'à ce que NativeInt le garantisse
le define WIN64 interroge l'OS, donc les types de largeur pointeur doivent venir de NativeInt ou d'un test CPU64BITS

Overrides de TStream : copiez la signature de la RTL, caractère pour caractère

Un descendant de TStream sur Delphi Linux64 doit déclarer et implémenter Read, Write et SetSize avec des paramètres Longint, exactement comme la RTL les déclare, parce que Integer n'est plus le même type. PDFlibPas a son propre TPDFlibWideFileStream pour les noms de fichiers Unicode, et sa déclaration de classe utilisait bien Longint. La section implementation utilisait 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;

// Avant : accepté pendant des années sous Win32, Win64 et FPC, où Integer = Longint
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Integer): Longint;

// Après : l'en-tête d'implémentation répète la déclaration à l'identique
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Longint): Longint;
Begin
  Result:= FileRead(FHandle, Buffer, Count);
  If Result= - 1 Then
    Result:= 0;
End;

Sur tous les compilateurs que la bibliothèque avait rencontrés jusque-là, Integer et Longint étaient le même type de 4 octets, donc un en-tête incohérent était indiscernable d'un en-tête cohérent. Sur Delphi Linux64, l'implémentation ne correspond plus à sa propre déclaration et l'unité cesse de compiler. Celle-là fait plus mal que sa taille ne le suggère, car PDFlibExtra est une unité de fondation dont dépend la plus grande partie de la bibliothèque : un seul en-tête d'implémentation incohérent empêche la plupart des unités de compiler

Le même raisonnement vaut dans l'autre sens pour du code neuf. Quand vous overridez une méthode de la RTL, copiez ses types de paramètres littéralement, même quand vous « savez » que deux types sont identiques. Delphi Linux64 est simplement la première cible où ce savoir cesse d'être vrai

Quels enregistrements exigent Integer au lieu de Longint, et lesquels doivent le garder ?

Un enregistrement qui reflète un format de fichier ou une structure Windows doit utiliser des types à largeur fixe (Integer, Cardinal, Int32), tandis qu'un enregistrement qui reflète une structure C déclarée avec long doit garder Longint / LongWord. Les deux cas se ressemblent à s'y méprendre dans le source et appellent des réponses opposées, car le LONG Windows fait toujours 32 bits et le long C fait ce que dit l'ABI de la plateforme

Matrice de décision pour les champs d'enregistrements Delphi sur LP64 : Integer à largeur fixe pour les octets sur disque, Longint conservé pour les liaisons au C long dans PDFlibPas
les enregistrements qui reflètent des octets prennent des largeurs fixes, et seules les liaisons à un C long gardent Longint, adossées à une assertion de taille d'une ligne
Type
  // Uniscribe GOFFSET : deux LONG Windows, toujours 8 octets au total
  TGoffset= Packed Record
    du: Integer;  // était Longint : l'enregistrement passait à 16 octets sur Delphi Linux64
    dv: Integer;
  End;

  // EMF LOGCOLORSPACE : en-tête rempli par un Move fixe de 68 octets depuis les données de l'enregistrement
  TPLEMFLogColorSpace= Packed Record
    Signature, Version, Size, ColorType, Intent: Cardinal;
    Endpoints: Array[0..8] Of Integer;  // était Longint : 36 octets devenaient 72
    Gamma: Array[0..2] Of Cardinal;
    Filename: Array[0..259] Of WideChar;
  End;

  // PDFium FPDF_FILEACCESS : m_FileLen est un unsigned long C, donc LongWord est juste
  FPDF_FILEACCESS= Record
    m_FileLen: LongWord;  // 4 octets sous Windows, 8 sous LP64, comme le côté C
    m_GetBlock: TFPDF_GetBlock;
    m_Param: Pointer;
  End;

Le cas EMF montre à quel point cet échec est silencieux. L'importeur copie 68 octets dans TPLEMFLogColorSpace : 20 octets d'en-tête, 36 octets de points d'extrémité CIE, 12 octets de gamma. Avec des points d'extrémité Longint sous LP64, ces mêmes 68 octets atterriraient dans un tableau de 72 octets, les valeurs de gamma ne seraient jamais écrites, et rien ne lèverait d'erreur. Les deux enregistrements vivent sur des chemins de code Windows-only et utilisent de toute façon des types à largeur fixe, car un enregistrement qui reflète des octets sur disque ne doit pas dépendre du compilateur qui le compile par hasard

La liaison PDFium est le contre-exemple. Son m_FileLen reflète un unsigned long C, 32 bits sous Windows et 64 bits sous LP64, et LongWord suit cela exactement des deux côtés. Le réécrire en Cardinal lors d'une passe de nettoyage le casserait le jour où l'hôte PDFium gagnerait un chargeur POSIX. L'hôte PDFium est Windows-only, donc c'est une note pour l'avenir. Le côté conventions d'appel des liaisons C a ses propres pièges, traités dans l'article sur les liaisons DLL, ActiveX et dylib de la PDF Library

Une garde bon marché pour tout enregistrement qui reflète des octets : assertez sa taille une fois au démarrage, par exemple Assert(SizeOf(TGoffset) = 8) dans une section initialization. Cela ne coûte rien et transforme un changement de layout silencieux en échec immédiat au premier run d'une nouvelle cible

Bien régler les defines conditionnels : DELPHI_LINUX, Android et Types

Avant que le moindre problème de type puisse surgir, Delphi sous Linux devait arrêter de se faire passer pour Windows. PDFlibDefines.inc ne définissait rien du tout pour Delphi Linux, donc le compilateur tombait directement dans les défauts Windows/VCL, et la première erreur était l'introuvable Vcl.Graphics. Les defines aiguillent Delphi Linux vers le même backend que celui que FPC utilise sur les cibles non Windows : rendu Cairo, fontconfig, OpenSSL et libcurl chargés à l'exécution

{$IFDEF LINUX}
{$IFNDEF FPC}
{$IFNDEF ANDROID}
{$DEFINE DELPHI_LINUX}
{$DEFINE NOVCL}
{$DEFINE MD5_MAC}
{$DEFINE NOCRYPTAPI}
{$DEFINE USE_Cairo}
{$ENDIF}
{$ENDIF}
{$ENDIF}

Deux détails de ce bloc sont faciles à rater. L'exclusion ANDROID existe parce que Delphi définit LINUX pour les cibles Android aussi, et ce backend ne couvre pas Android. Et le nouveau define est volontairement séparé de la branche macOS Delphi existante (OS_MAC sans FPC), qui dépend des unités Macapi ; le define Linux s'appuie plutôt sur le chemin non-Windows de FPC

L'autre erreur récurrente était TPoint et TRect qui passaient non déclarés. Sous Windows, ces types arrivent dans la plupart des unités indirectement, via l'unité Windows. Sous Delphi Linux, il n'y a pas d'unité Windows dans la clause uses, donc des unités comme PDFlibFont et PDFlibPageTree listent désormais Types explicitement sous DELPHI_LINUX. Si votre propre code compile sous Windows uniquement parce que Windows traîne System.Types avec lui, attendez-vous au même mur. Le schéma général du code qui ne marche sur un compilateur que par coïncidence fait l'objet de cinq habitudes Delphi qui ne marchent que par accident tant que FPC ne les expose pas

Là où le compilateur cesse d'aider

Les problèmes LP64 ci-dessus sont ceux qu'un compilateur peut trouver. Ceux qu'il ne trouve pas, comme un enregistrement de la mauvaise taille mais qui ne passe jamais par un typecast, ne se manifestent que quand les tests tournent réellement sur la cible, et c'est exactement à cela que servent les assertions de taille décrites plus haut

Si c'est la sortie 1 bit qui vous a mené au code de dithering au départ, la route supportée pour les pages rendues est une autre : la down-conversion GDI halftone en dehors du renderer, décrite dans le rendu de pages PDF en bitmaps monochrome 1 bit

Le travail multi-compilateurs décrit ici vit dans la PDF Library pour Delphi, qui cible aujourd'hui Delphi et C++Builder sous Windows plus des builds Free Pascal headless sous Linux et macOS ; la page produit liste en détail les compilateurs et plateformes supportés