Article technique

Free Pascal Win32 : décoration des symboles C dans HotPDF

Free Pascal sur Win32 ajoute automatiquement un underscore de tête à chaque import cdecl; external, tandis que public name exporte la chaîne que vous avez écrite, caractère pour caractère. HotPDF doit satisfaire les deux conventions dans le même arbre de sources, parce que la construction Delphi livre déjà des déclarations d'import qui écrivent l'underscore à la main. Se tromper sur cette asymétrie produit des erreurs d'édition de liens qui nomment un symbole que personne n'a écrit

Étendre une bibliothèque Delphi à Free Pascal est habituellement présenté comme un problème de portabilité, et sur Win64 c'en est surtout un. Win32 est différent. L'ABI Windows x86 32 bits transporte trente ans de conventions accumulées sur la façon dont les symboles C s'épelent, sur qui nettoie la pile, et sur quels helpers privés au compilateur une unité de translation est en droit de supposer, et chacun de ces points est un endroit où deux compilateurs Pascal d'accord sur le langage peuvent encore se disputer sur le fichier objet

Pourquoi le même symbole se résout-il sur Win64 et échoue-t-il sur Win32 ?

Parce que le préfixe underscore est une convention 32 bits que Free Pascal applique aux imports mais pas aux exports. Déclarez function deflate(...): Integer; cdecl; external; et FPC cherche _deflate dans le fichier objet sur Win32, et deflate sur Win64. C'est un comportement correct, conforme à ce qu'émet un compilateur C. Le piège est de l'autre côté du pont : une routine marquée public name 'deflate' exporte précisément deflate sur les deux cibles, sans aucun préfixe ajouté

Ajoutez maintenant le détail historique qui rend la chose concrète. La construction Delphi déclare déjà certains de ces points d'entrée avec l'underscore écrit dans le nom, parce que c'est ce que contiennent ses propres fichiers objets. Donnez la même déclaration à FPC sur Win32 et le compilateur zèleusement la préfixe à nouveau, si bien que l'éditeur de links part en chasse de __deflate, un symbole que rien n'exporte. La correction intuitive, ajouter un underscore partout, casse les imports qui étaient déjà correctement épelés

Ce qui fonctionne, c'est une paire de constantes de préfixe plutôt qu'une seule. HPDFFPCZLib et HPDFFPCCodecStubs utilisent un préfixe pour les imports C simples et un autre pour les imports qui portent déjà un préfixe côté Delphi, et sur Win64 les deux constantes sont vides si bien que les noms de liens existants survivent intacts. Deux constantes au lieu d'une, c'est toute la correction, et ce n'est évident qu'une fois qu'on a séparé la règle d'import de la règle d'export

Les mêmes déclarations de symboles C résolues par Free Pascal et Delphi sur Win64 et Win32 : les imports cdecl gagnent un underscore seulement sur la cible 32 bits, une déclaration Delphi déjà épelée avec underscore devient __deflate et échoue à l'édition de liens, tandis que les exports public name restent littéraux sur les deux architectures
Une seule constante de préfixe ne peut pas servir les deux règles : les imports cdecl simples et les imports portant déjà l'underscore Delphi se décorent différemment sous FPC sur Win32, donc HotPDF en garde deux et laisse les deux vides sur Win64
// Deux préfixes, pas un : les imports C simples et les imports qui portent
// déjà un préfixe Delphi écrit à la main se décorent différemment sous FPC/Win32
const
{$IF DEFINED(FPC) and DEFINED(CPU32)}
  CPrefix     = '_';   // FPC ajoute lui-même celui-ci pour cdecl external
  DelphiCName = '';    // déjà épelé avec l'underscore dans la source
{$ELSE}
  CPrefix     = '';
  DelphiCName = '';
{$IFEND}

// Côté export : 'public name' est littéral sur chaque cible
procedure hpdf_codec_free(P: Pointer); cdecl;
  public name 'hpdf_codec_free';

WIN32 vous dit l'architecture, pas l'ABI

C'est l'erreur de compilation conditionnelle avec la plus longue traîne de débogage, et elle mérite d'être énoncée clairement : WIN32 et WIN64 décrivent l'architecture cible et ne disent rien de l'existence des helpers d'exécution privés au compilateur. Free Pascal définit les deux symboles sur les cibles Windows correspondantes, exactement comme Delphi. Une garde écrite {$IFDEF WIN32} autour de code qui appelle un helper du runtime Delphi compile donc sous FPC et échoue à l'édition de liens

Concrètement, trois familles de code tombent dans ce piège. Les trampolines Delphi pour entiers 64 bits atteints via les helpers System.@_ll, les routines de support assembleur Win32 de MSVC, et les slots d'import qui vont avec, tout cela existe pour servir des objets C précompilés que la construction Delphi link. Free Pascal ne link pas ces objets, donc il n'a besoin d'aucune de ces machineries, et chaque référence à elles doit disparaître. La subtilité est que la déclaration et l'implémentation doivent être exclues ensemble. N'en exclure qu'une et le compilateur rapporte quelque chose d'inutile sur un identifiant qu'il n'arrive à rattacher à rien

La règle qui s'en déduit est courte. Gardez sur le compilateur quand la question porte sur l'ABI ou le support d'exécution, gardez sur l'architecture quand la question porte sur la largeur des pointeurs ou le nombre de registres, et ne laissez jamais l'un tenir lieu de l'autre

Garder déclarations et implémentations ensemble

Un bloc conditionnel dans la section interface est facile à rater sans s'en apercevoir, et le message d'erreur qui en résulte pointe n'importe où sauf vers la cause. Ajouter une déclaration de méthode à l'interface d'une classe, et l'endroit naturel est à côté des méthodes liées, ce qui va très bien jusqu'au moment où ces voisines se trouvent à l'intérieur d'un bloc {$IFDEF} existant. Les directives conditionnelles ne sont pas indentées, donc un bloc ouvert quarante lignes plus haut est pour ainsi dire invisible pendant qu'on lit les déclarations environnantes

Ce qui se passe ensuite, c'est une compilation qui réussit sur une chaîne d'outils et produit une cascade sur l'autre. Si la garde environnante est un contrôle de version Delphi que Free Pascal ne satisfait pas, la déclaration disparaît pour FPC pendant que l'implémentation inconditionnelle reste, et le compilateur produit une longue liste de plaintes sur des identifiants de méthodes qu'il attendait et n'a pas trouvés. Aucun des messages ne mentionne le bloc conditionnel qui en est cause

Deux habitudes préviennent toute cette classe d'échecs. Avant d'insérer dans une section interface, cherchez vers le haut la conditionnelle ouverte la plus proche plutôt que de faire confiance au groupement visuel. Et traitez une suite de tests Delphi verte comme une preuve concernant Delphi seulement : la construction FPC de la bibliothèque est une barrière séparée, et la seule façon de savoir qu'elle passe est d'exécuter build-Win32-Lib-FPC.cmd et build-Win64-Lib-FPC.cmd dans le cadre du même changement

Ce qui casse dans le code arithmétique 32 bits

Une restriction du langage apparaît précisément dans le code le moins disposé à changer : Free Pascal 32 bits refuse un UInt64 comme variable de contrôle de boucle for. Dans les unités de courbes elliptiques qui portent X25519 et X448, les boucles qui parcourent les tableaux de limbs ont été écrites avec des compteurs 64 bits simplement parce que tout le reste du fichier est en 64 bits

La correction doit être chirurgicale, car en arithmétique de corps la largeur d'une variable fait partie de l'argument de correction. Les indices de boucle deviennent Integer, puisqu'un tableau de limbs a une poignée d'éléments et qu'aucun index n'approche jamais la plage 32 bits. Tout ce qui participe à l'arithmétique, les limbs eux-mêmes, la propagation de retenue et les masques, reste UInt64, car réduire l'un d'eux change silencieusement le résultat modulo le premier du corps

// Le FPC 32 bits rejette une variable de boucle UInt64. Réduisez seulement l'indice ;
// limbs, masques et retenues gardent leur largeur sinon les calculs du corps changent
var
  I: Integer;                 // était UInt64
  Carry, Mask: UInt64;
begin
  Carry := 0;
  for I := 0 to High(Limbs) do
  begin
    Limbs[I] := Limbs[I] + Carry;
    Carry := Limbs[I] shr 51;
    Limbs[I] := Limbs[I] and Mask;
  end;
end;

La vérification d'un tel changement ne peut pas être un test aller-retour. Chiffrer et déchiffrer avec la même implémentation cassée concorde parfaitement avec elle-même, c'est pourquoi les vecteurs à réponse connue sont non négociables ici : exécutez les vecteurs de test publiés de X25519 et X448 et comparez les octets de sortie exacts. C'est le seul contrôle qui distingue une implémentation correcte d'une implémentation fausse mais auto-cohérente, et il s'applique pareillement aux primitives symétriques discutées dans les frontières de codecs deflate et AES sous Free Pascal

Les deux points de rupture d'une construction Win32 Free Pascal de HotPDF : une garde {$IFDEF WIN32} autour de helpers du runtime Delphi qui compile mais échoue à l'édition de liens si déclaration et implémentation ne sont pas exclues ensemble, et la variable de boucle UInt64 des parcours de limbs X25519 et X448 réduite à Integer tandis que limbs, retenues et masques gardent leur largeur
Gardez sur le compilateur quand la question est l'ABI ou le support d'exécution et sur l'architecture quand c'est la largeur des pointeurs, puis prouvez les changements arithmétiques contre des vecteurs à réponse connue publiés plutôt que des tests aller-retour

Ce que vaut une construction Win32 Free Pascal

Le bénéfice pratique est qu'une application Lazarus ciblant Windows 32 bits obtient le même moteur de documents que son homologue Delphi, sans contrat binaire séparé à maintenir. Cela compte le plus pour les déploiements dont on parle rarement : contrôleurs industriels, terminaux de point de vente et logiciels métiers de longue durée où le runtime 32 bits n'est pas un choix hérité mais une contrainte matérielle

L'histoire Win64 est venue d'abord et est décrite dans le support Free Pascal et Lazarus sur Win64. Win32 n'en est pas une rediffusion. Win64 a une seule convention d'appel, aucune décoration de noms et aucun helper d'entiers privé à Delphi à contourner, si bien que presque tout cet article est spécifique à la cible 32 bits. Les unités arithmétiques qui ont requis le changement de variable de boucle sont celles décrites dans l'arithmétique de Montgomery sur les courbes NIST, où la discipline de largeur est expliquée plus en profondeur

La leçon générale est que le travail de portabilité entre compilateurs n'est pas d'abord une affaire de fonctionnalités du langage. Les deux compilateurs acceptent ici le même Object Pascal. Ce qui diffère, c'est le fichier objet : comment les symboles s'épelent, quelles routines de support le runtime est supposé fournir, et quels objets précompilés sont dans l'édition de liens. HotPDF livre les paquets Free Pascal et Lazarus à côté de ceux pour Delphi et C++Builder dans le composant PDF Delphi HotPDF, si bien que le même arbre de sources alimente chaque chaîne d'outils au lieu de se diviser par compilateur