Article technique

HotXLS sous Free Pascal : Unicode, slots COM et zlib

HotXLS se construit sous Free Pascal et Lazarus sur Windows, et le portage a tourné autour de quatre décisions qui n'ont rien à voir avec la syntaxe Object Pascal : garder le cœur en mode DELPHIUNICODE, déclarer les interfaces de stockage structuré OLE comme des interfaces CORBA avec comptage de références géré à la main, remplacer les fichiers objets AES Win32 par une implémentation Pascal, et corriger une boucle inflate qui pouvait accepter un ZIP tronqué comme complet

Quiconque a porté une bibliothèque Delphi mature connaît la forme de ce travail. Le compilateur accepte presque tout à la première passe. Ce qui suit est une longue traîne de différences de comportement qui compilent proprement et produisent des résultats faux, et un moteur de tableur y est exceptionnellement exposé parce qu'il touche encodage de texte, stockage structuré COM, compression et cryptographie dans un seul chemin de code

Pourquoi le cœur tient-il à DELPHIUNICODE plutôt qu'au simple DELPHI ?

Parce que le moteur de formules dépend de String et Char porteurs de la sémantique UTF-16, et que l'alternative ANSI perd des caractères avant même que quoi que ce soit n'atteigne le fichier. Il est tentant de construire le cœur en mode FPC DELPHI, puisque c'est le commutateur de compatibilité vers lequel la plupart des portages se tournent, et le code compile. Puis un classeur avec des noms de feuilles chinois ou des étiquettes cyrilliques fait l'aller-retour par le chemin de calcul et les caractères ont disparu au moment où le rédacteur les voit, sans aucune erreur nulle part

Le mode n'est pas uniforme dans la bibliothèque, et c'est délibéré plutôt que négligé. Le décodeur d'octets PNG et les surcharges LCL ont réellement besoin de signatures ANSI, car ils manipulent des octets et ce que le widgetset leur remet. Ces unités activent un commutateur séparé LX_FPC_ANSI. Deux modes dans une bibliothèque sonne comme une puanteur jusqu'à ce qu'on remarque que l'alternative est un décodeur d'octets qui traite son entrée comme du texte

Il y a un détail compagne qui attrape les gens plus tard. DELPHIUNICODE ne fait pas de TFormatSettings.DecimalSeparator un WideChar dans le runtime FPC. Une entrée portant un séparateur décimal Unicode doit être normalisée vers un séparateur ASCII à l'intérieur de la chaîne Unicode d'abord, et toute entrée dont le séparateur ne correspond pas à celui attendu doit être rejetée plutôt que tronquée en silence au caractère que l'analyseur n'a pas reconnu

program ExportReport;
{$MODE DELPHI}
uses
  Interfaces,          // doit venir en premier : initialise le widgetset LCL
  SysUtils, lxHandle;  // et la couche de conversion UTF-8

var
  Book: TXLSWorkbook;
begin
  Book := TXLSWorkbook.Create(nil);
  try
    Book.LoadFromFile('input.xls');
    Book.Sheets[0].AsString[1, 1] := 'Quarterly summary';
    Book.SaveToFile('output.xls');
  finally
    Book.Free;
  end;
end.

L'unité Interfaces n'est pas optionnelle et elle doit venir en premier. C'est elle qui initialise le widgetset LCL et la couche de conversion UTF-8, et HotXLS compte sur les deux dès que polices, chemins de fichiers ou texte traversent la frontière RTL/LCL. Un programme console qui la saute compilera et se comportera mal sur tout chemin non ASCII. C'est aussi la raison pour laquelle une compilation réussie prouve si peu ici : le portage ne démontrait son fonctionnement qu'une fois de vrais documents avec de vrais noms de polices et de vrais chemins ayant fait un aller-retour complet

Une VMT de classe n'est pas une vtable COM

Free Pascal ne vous laissera pas remettre une VMT de classe à Windows comme vtable d'interface COM, même quand la déclaration ressemble à celle que Delphi accepte. Les dispositions diffèrent de façons qui produisent un appel vers le mauvais slot, ce qui se manifeste par un plantage quelque part sans rapport avec le site d'appel. Le stockage structuré compte ici parce que le format binaire classique de classeur est un fichier composé OLE, et le lire ou l'écrire signifie implémenter ILockBytes dans lequel l'API de stockage Windows rappellera

L'arrangement qui fonctionne est une interface CORBA avec les slots COM déclarés explicitement et AddRef et Release gérés à la main. Cela veut dire renoncer au comptage de références automatique pour ces types et prendre la responsabilité du cycle de vie, ce qui est un marché honnête pour une poignée d'interfaces qui vivent dans une seule unité. Le piège précis dans ce travail est QueryInterface : il doit renvoyer un pointeur d'interface, pas le pointeur d'objet. Les deux compilent. L'un remet à Windows une adresse dont le premier mot machine n'est pas une vtable

Schéma comparant la VMT de classe Free Pascal à la vtable d'interface COM que HotXLS doit présenter à l'API de stockage structuré Windows : des ordres de slots différents pour la même déclaration Pascal, plus le piège QueryInterface où renvoyer le pointeur d'objet au lieu d'un pointeur d'interface envoie un appel ILockBytes vers un slot de classe et plante loin du site d'appel
Free Pascal refuse de servir une VMT de classe comme vtable COM, donc HotXLS déclare des interfaces CORBA avec des slots COM explicites et AddRef et Release gérés à la main, et QueryInterface renvoie le pointeur d'interface que Windows peut déréférencer

Les déclarations spécifiques à FPC vivent dans lxOleInterfaces.inc, à côté de lxAESBackend.inc et lxZlibBackend.inc dans le répertoire de sources FPC, si bien que les choix spécifiques au compilateur se tiennent en un seul endroit au lieu d'être éparpillés dans le moteur. Le format lui-même et la façon dont la bibliothèque le navigue sont décrits dans la lecture des fichiers composés OLE2 en Pascal

Un autre détail de type appartient à la même famille. LargeInt doit se résoudre en Int64 dans la branche FPC, et la classification par le compilateur de Comp diffère assez entre les deux chaînes d'outils pour que la résolution de surcharge puisse choisir une candidate différente. Testez le comportement aux grands offsets avec un flux de fichier plutôt qu'un flux HGLOBAL : le flux de mémoire globale Windows reboucle tout seul sur les seeks au-delà de 4 GiB, si bien qu'un test qui y passe ne prouve rien de votre propre arithmétique

Ce que cache une implémentation AES auto-cohérente

Les fichiers objets AES Win32 que la construction Delphi link sont en OMF, et l'éditeur de liens Free Pascal ne peut pas les consommer, donc la branche FPC utilise une implémentation AES en Pascal à la place. Delphi continue de linker les fichiers objets qu'il a toujours linkés, ce qui garde le binaire publié inchangé pour les clients existants

L'exigence de vérification est la partie qui vaut d'être emportée dans tout projet. Chiffrer des données puis les déchiffrer avec la même implémentation ne prouve absolument rien : un algorithme symétrique avec un key schedule faux, un ordre de blocs faux ou un chaînage faux est parfaitement auto-cohérent et fera l'aller-retour de sa propre sortie à chaque fois. Seuls les vecteurs à réponse connue l'attrapent, en contrôlant l'expansion de clé, l'ordre des blocs et le chaînage CBC contre des valeurs publiées. Livrez une implémentation fausse mais auto-cohérente et le symptôme apparaîtra la première fois qu'un client ouvre le fichier dans Excel

La compression avait un défaut d'un autre caractère. Un backend inflate en Pascal peut encore avoir de la sortie en attente après avoir consommé toute son entrée compressée, donc l'appelant doit continuer à appeler jusqu'à ce que le flux signale sa fin. Traiter une entrée épuisée comme une fin de flux tronque le dernier bloc. Pire, cela transforme une archive endommagée en archive silencieusement acceptée, ce qui est exactement le mode d'échec que le durcissement décrit dans la validation de l'enregistrement ZIP end-of-central-directory existe pour prévenir. La règle est que pas de progression plus pas terminé est une erreur de troncature, jamais un EOF

Deux pièges du système de construction qui coûtent de vraies heures

Les chemins de recherche LCL doivent précéder les chemins de jokers de paquets FPC, sinon l'unité Menus de Free Vision fait de l'ombre à l'unité LCL du même nom et vous obtenez une non-concordance de checksum PPU qui ne dit rien de l'une ni de l'autre. Une installation Lazarus déplacée après installation peut aussi laisser des chemins périmés dans fpc.cfg, si bien que les points d'entrée de construction spécifient explicitement les chemins d'unités et de binaires plutôt que d'hériter ce que l'environnement offre

Le second piège n'a rien à voir avec Pascal. Un fichier batch .cmd écrit avec des fins de ligne LF fonctionne jusqu'à ce que le fichier dépasse la taille du tampon de lecture de l'interpréteur, moment où call :label échoue en affirmant que l'étiquette batch n'existe pas, et l'échec apparaît sur le programme qui se trouve être passé la frontière. Tout outil qui réécrit un script batch doit réécrire du CRLF. Et lazbuild --build-all nettoie le répertoire de sortie des unités de paquet avant de compiler, si bien qu'un fichier d'options garé dans ce répertoire est supprimé avant de pouvoir être lu : gardez-le dehors, et rappelez-vous que le chemin @ se résout relativement au répertoire du paquet parce que lazbuild invoque le compilateur depuis là

Carte des deux couches de dangers derrière une compilation Free Pascal de HotXLS qui passe : le mode DELPHIUNICODE qui garde String et Char en UTF-16, l'échappatoire LX_FPC_ANSI pour le décodeur d'octets PNG et les surcharges LCL, et les pièges de construction allant de l'ombre du Menus de Free Vision, aux chemins périmés de fpc.cfg, aux fichiers batch LF seulement et au nettoyage de sortie de lazbuild
Une première compilation prouve peu : la carte des modes décide quels caractères survivent jusqu'au rédacteur, tandis que les pièges du système de construction font surface comme des non-concordances de checksum, des étiquettes manquantes fantômes et des fichiers d'options supprimés avant lecture
// Export de grille Lazarus : TGridToXLS est livré dans le paquet Lazarus, donc le
// même code d'export de DB-grid fonctionne dans une application LCL
var
  Exporter: TGridToXLS;
begin
  Exporter := TGridToXLS.Create(nil);
  try
    Exporter.DBGrid := GridOrders;
    Exporter.WorksheetName := 'Orders';
    Exporter.ExportHeader := True;
    Exporter.SetColumnsWidth := True;
    Exporter.ExportDBGrid;
    Exporter.SaveAs('orders.xls');
  finally
    Exporter.Free;
  end;
end;

Ce que vaut un avertissement du compilateur

Free Pascal signale les variables locales non initialisées que Delphi ne signale pas, et lancer la construction FPC a transformé cette différence en deux vrais défauts dans l'unité de calcul. Une fonction lisait une variable de compte jamais affectée avant usage, et une autre utilisait deux coordonnées dans une branche avant que le code qui les calcule ne tourne dans une branche différente. Sous Delphi, les deux se comportaient selon ce que la pile avait là par hasard, ce qui est la définition d'un bug qui se reproduit sur une machine et pas sur une autre

La conclusion pratique est que le second compilateur mérite de rester dans la boucle même pour un produit qui livre d'abord sur le premier. Balayer périodiquement les classes d'avertissements FPC est une passe d'analyse statique bon marché sur une base de code Delphi, et elle trouve une catégorie de défaut qu'aucune suite de tests n'atteint de façon fiable. La discipline plus large de matrice de versions dans laquelle cela s'inscrit est décrite dans la matrice de construction entre compilateurs

Le support Free Pascal et Lazarus pour Windows est livré avec le composant tableur Delphi HotXLS comme paquet Lazarus à côté des paquets Delphi et C++Builder, construit depuis le même arbre de sources plutôt qu'un fork. C'est là le but de l'exercice : un moteur, quatre chaînes d'outils, et les décisions spécifiques au compilateur isolées dans des fichiers include qu'on peut lire en une séance