HotXLS livre une seule base de code Object Pascal à chaque version de Delphi et C++Builder depuis XE5, et build-All-Lib-TRIAL.cmd est le script qui le prouve : 43 configurations de build, couvrant 12 versions de Delphi sous Win32 et Win64, ainsi que 10 builds de packages C++Builder Win32 et 9 Win64. De la version 2.363 à la version 2.374, ce script n’a jamais été exécuté jusqu’au bout et la branche XE5 était cassée pendant toute cette période
Rien dans l’échec n’était subtil une fois qu’il a été observé. Cinq constructions distinctes que le compilateur actuel accepte sans commentaire sont des erreurs franches dans RAD Studio XE5, que la matrice désigne par 12.0. La version 2.375.0 a corrigé les cinq et la matrice est redevenue verte à 43 sur 43. Voici chaque rejet, la raison pour laquelle l’ancien compilateur a sans doute raison pour les deux rejets de typage et la partie plus embarrassante : le script de sonde écrit pour diagnostiquer le problème a signalé une réussite mensongère lors de son premier passage
Pourquoi la branche XE5 a-t-elle dépéri sans que personne ne le remarque ?
La branche XE5 a dépéri parce que le développement quotidien n’exécutait que l’ensemble de quatre scripts de la version 37.0, et un build local vert ne dit rien d’un compilateur que vous n’avez pas invoqué. La matrice complète est un script distinct et lent, appelé par l’installateur d’essai avant qu’Inno Setup ne collecte les fichiers ; elle est donc exécutée au moment du packaging plutôt qu’au moment du commit. Douze versions tiennent dans cet intervalle
Il vaut la peine de détailler l’arithmétique des branches, car c’est là que vit l’illusion de couverture. DELPHI_TRIAL_VERSIONS énumère les versions 12.0 à 37.0 et chacune de ces 12 versions est compilée deux fois, sous Win32 et Win64. CB_TRIAL_WIN32_VERSIONS en liste 10, tandis que CB_TRIAL_WIN64_VERSIONS n’en liste que 9, car XE5 possède un projet de package C++Builder mais ne fournit pas l’objet de démarrage de package Win64 c0pkg64.o. Douze plus douze plus dix plus neuf font 43. En exécuter quatre et qualifier la base de code de portable est une erreur de catégorie, et c’est précisément l’erreur qui a permis à cette situation de se produire
HotXLS a été pris par la même forme de problème dans l’autre sens. Une nouvelle unité atteignable par une clause uses mais absente de la liste de fichiers .cbproj se compile parfaitement sous Delphi, car dcc ajoute implicitement les unités non listées au package et émet au pire un hint W1033. C++Builder n’émet un .obj que pour les unités nommées dans <DelphiCompile> ; le même code meurt donc à l’étape ilink avec un externe non résolu. Une toolchain dissimule ce que l’autre détecte. C’est tout l’argument en faveur de l’exécution de la matrice plutôt que de la confiance accordée à un compilateur représentatif
Les casts de type forcés que les anciens compilateurs Win32 rejettent
Deux des cinq rejets sont le même bug sous deux apparences : un cast de type forcé appliqué à une expression flottante plutôt qu’à une variable. Sous Win32, les anciens compilateurs évaluent l’arithmétique dans la pile x87 ; une addition impliquant un Double est donc conservée avec une précision excédentaire de 80 bits et son type statique devient l’Extended de 10 octets. Réduire par cast 10 octets vers un TDateTime de 8 octets n’est pas un cast légal et le compilateur l’indique par E2089 Invalid typecast
Le détail exaspérant est que la forme avec variable est correcte. TDateTime(Serial) se compile dans toutes les versions de la matrice, car Serial fait déjà 8 octets et le cast conserve la taille. Ajoutez quelque chose et l’expression s’élargit sous vos pieds. Le correctif n’est ni un cast plus large ni une définition conditionnelle : il faut cesser de caster. Une affectation implicite réel-vers-réel convertit correctement sur tous les compilateurs supportés par HotXLS et exprime ce que le code veut réellement dire
// Rejeté sur XE5 (Win32) : chaque addition est évaluée comme un
// Extended de 10 octets, et le cast de 10 vers 8 octets lève E2089
if Dates1904 then
Value := TDateTime(Serial + XLSDate1904Offset)
else if Serial < 60 then
Value := TDateTime(Serial + 1)
else
Value := TDateTime(Serial); // celui-ci est accepté : aucune addition
// Compatible avec les versions : laisser l’affectation réel-vers-réel effectuer la conversion
if Dates1904 then
Value := Serial + XLSDate1904Offset
else if Serial < 60 then
Value := Serial + 1
else
Value := Serial;
// Même catégorie de rejet dans le packer de valeurs de cellule : un cast Double
// forcé d’un entier. Diviser suffit : l’opérateur produit déjà un réel
if (Scaled = intVal) and (Double(intVal) / 100 = AValue) then // E2089
;
if (Scaled = intVal) and (intVal / 100 = AValue) then // portable
;
La branche Serial < 60 correspond à la fiction de l’année bissextile 1900, pas à un décalage d’une unité : le numéro de série 60 est le 29/02/1900 inexistant d’Excel, si bien que les numéros inférieurs ont besoin d’un jour supplémentaire avant que DecodeDate ne les voie. Un travail de portabilité ne doit jamais modifier silencieusement ce genre de logique, raison exacte pour laquelle le correctif sûr retire le cast et laisse l’arithmétique intacte
Que se passe-t-il lorsque nil est un argument procédural ?
Un nil nu passé là où un type procédural est attendu ne se lie pas lors de la résolution de surcharge des anciens compilateurs. Le point d’appel dans HotXLS est ResolveIndexedColor, surchargé et prenant un callback TXLSTryResolveSystemColor dont la plupart des appelants n’ont pas besoin. Les compilateurs récents résolvent nil vers le paramètre procédural et choisissent la bonne surcharge. XE5 ne le fait pas, et le diagnostic pointe vers l’ensemble des surcharges plutôt que vers l’argument, ce qui explique les vingt minutes perdues
La réponse portable consiste à donner un type au callback nul. Une variable de niveau unité du type procédural est initialisée à zéro par le langage ; elle vaut donc déjà nil sans initialiseur et transporte l’information de type dont l’ancien résolveur a besoin. Lorsqu’une variable de niveau unité serait excessive, une variable locale typée à laquelle on affecte nil fait le même travail
var
// Un littéral procédural nil ne se lie pas lors de la résolution de surcharge
// des anciens compilateurs ; une variable typée et initialisée à zéro le permet
NilSystemColorResolver: TXLSTryResolveSystemColor;
// ...
FWorkbook.ResolveIndexedColor(AIndexedColor, xicsBiffIcv, ARole,
NilSystemColorResolver, Resolution);
// Le même correctif avec une variable locale typée, dans le classeur XLSX
function TXLSXWorkbook.ResolveIndexedColor(AIndex: Int64;
ASpace: TXLSIndexedColorSpace;
out AResolution: TXLSIndexedColorResolution): Boolean;
var
NoResolver: TXLSTryResolveSystemColor;
begin
NoResolver := nil;
Result := ResolveIndexedColor(AIndex, ASpace, xicrGeneral, NoResolver,
AResolution);
end;
Notez qu’il s’agit d’une véritable différence au niveau du langage, pas d’un bug du compilateur à contourner avec des defines. La variable initialisée à zéro est correcte dans toutes les versions de la matrice et ne coûte qu’une ligne, si bien qu’il n’y a aucune compilation conditionnelle ici. Ne recourez à {$IF CompilerVersion} que lorsque la plateforme diffère réellement selon les versions, ce qui n’arrive qu’une fois dans ce lot
Les méthodes VCL protected changent entre les versions
TPicture.LoadFromStream est public dans la VCL actuelle et protected dans les anciennes versions prises en charge par HotXLS ; un appel direct se compile donc maintenant et échoue alors. HotXLS l’utilise pour vérifier qu’une charge utile d’image d’arrière-plan de feuille se décode réellement, contrôle qui s’exécute avant que l’exporteur HTML ne s’engage à incorporer les octets. La réponse Pascal classique s’applique : déclarer dans la même unité un descendant dont le seul but est d’élargir la visibilité, puis caster vers lui au point d’appel
type
// TPicture.LoadFromStream est protected dans les anciennes versions de VCL
// prises en charge par la bibliothèque ; un descendant de la même unité l’expose
TXlsxPictureAccess = class(TPicture);
// ...
Stream.WriteBuffer(AData[1], Length(AData));
Stream.Position := 0;
TXlsxPictureAccess(Picture).LoadFromStream(Stream);
Result := (Picture.Graphic <> nil) and not Picture.Graphic.Empty and
(Picture.Graphic.Width > 0) and (Picture.Graphic.Height > 0);
L’astuce de la classe d’accès est sûre ici parce que le descendant n’ajoute aucun champ et n’est jamais instancié ; le cast modifie seulement ce que le compilateur vous autorise à nommer. Il vaut tout de même la peine de laisser un commentaire à la déclaration, car un lecteur qui ne construit que sur un IDE actuel verrait sinon un type inutile. La gestion des images d’arrière-plan réapparaît dans le chemin de rendu de grille VCL personnalisé, où la même charge décodée alimente la feuille affichée à l’écran
Le type du token GdiplusStartup a changé deux fois
Le seul rejet du lot qui exige réellement une compilation conditionnelle est le type du paramètre var de GdiplusStartup, qui a changé entre les générations de VCL d’une manière ne laissant aucune écriture unique valide partout. Les sondes version par version ont fixé le comportement réel : les branches 12.0 à 20.0 n’acceptent que Cardinal, celles de 21.0 et 22.0 n’acceptent que THandle ou ULONG_PTR, et 23.0 ainsi que 37.0 acceptent les deux. En noms de versions, cela donne Cardinal de XE5 à 10.3 Rio, puis THandle à partir de 10.4 Sydney. Comme les deux intervalles acceptés ne se recouvrent pas de 12.0 à 22.0, aucune déclaration inconditionnelle ne fonctionne : la garde se fonde sur CompilerVersion >= 34, qui correspond à Sydney, et l’appel est entièrement qualifié en Winapi.GDIPAPI.GdiplusStartup afin que l’ordre de résolution des unités ne substitue pas une autre déclaration dans une version intermédiaire
function TXLSPageImageExporter.EncodeTiff(Stream: TStream): Integer;
var
StartupInput: TGdiplusStartupInput;
// Le type du paramètre var de GdiplusStartup dans GDIPAPI suit la génération VCL :
// Cardinal jusqu’à Rio, THandle à partir de Sydney
{$IF CompilerVersion >= 34}
StartupToken: THandle;
{$ELSE}
StartupToken: Cardinal;
{$IFEND}
TiffEncoder: TGUID;
begin
FillChar(StartupInput, SizeOf(StartupInput), 0);
StartupInput.GdiplusVersion := 1;
CheckStatus(Winapi.GDIPAPI.GdiplusStartup(StartupToken, @StartupInput,
nil), 'startup');
if GetEncoderClsid('image/tiff', TiffEncoder) < 0 then
raise EInvalidGraphic.Create('GDI+ TIFF encoder is unavailable');
// ... encoder ...
end;
C’est la branche TIFF de l’exporteur d’images de page ; l’ampleur d’une erreur ici couvre donc toute la surface d’export raster, y compris les chemins décrits dans l’export d’une plage de cellules comme image unique. Notez aussi ce que la garde ne prétend pas : ULONG_PTR et THandle ont la même largeur sur les deux plateformes, si bien que le choix concerne le nom de l’identifiant dans la déclaration, pas la correction 32 bits par rapport à 64 bits
Pourquoi la première exécution de la sonde n’a-t-elle rien signalé ?
La sonde de version n’a rien signalé lors de son premier passage parce que les affectations res=$(...) étaient exécutées dans un sous-shell, où elles ne se propagent pas au parent. dcc32 quitte avec 0 en cas de réussite, et le code de sortie était donc le bon signal à capturer ; le script le capturait dans une variable qui cessait d’exister une ligne plus tard. Chaque branche revenait vide et la sortie ressemblait à celle d’une sonde qui n’avait rien compilé, ce qui était exactement le cas
Le second échec était pire, car il produisait une mauvaise réponse plutôt qu’aucune réponse. La sonde classait une branche en comptant les lignes correspondant à Error, et Delphi ne préfixe pas chaque erreur fatale par ce mot. F1026 File not found est fatal et ne correspond pas ; une sonde qui ne pouvait résoudre aucune unité était donc notée comme une réussite propre. XE5 ne fournit pas Winapi.GDIPOPS.dcu, la première sonde a rencontré précisément ce cas et est passée faussement au vert. La règle qui en est sortie est étroite et mérite d’être dite clairement : jugez une sonde de compilateur sur l’artefact produit ou sur la ligne de synthèse du compilateur, jamais en recherchant un mot-clé dans sa sortie. Rechercher Error dans stderr est une heuristique qui échoue dans la direction que vous pouvez le moins vous permettre, en signalant silencieusement une réussite
Que coûte réellement la prise en charge d’une décennie de compilateurs ?
Le bilan honnête est que les changements de code décrits ici sont triviaux, mais que les changements de processus ne le sont pas. Quatre des cinq rejets ont été corrigés en écrivant du Pascal plus ordinaire, pas en ajoutant une machinerie de versions : retirer un cast, diviser au lieu de caster, donner un type à nil, déclarer une classe d’accès. Seul GdiplusStartup a mérité un {$IF}. Une base de code qui s’étend de XE5 à la version actuelle ne devient pas un fourré de defines conditionnels, sauf si vous laissez les casts forcés et les idiomes du compilateur le plus récent s’accumuler dès le départ
Le vrai coût est celui du temps de build et de la discipline. Quarante-trois branches constituent un script lent, raison précise pour laquelle il a glissé vers le temps de packaging puis vers jamais. Le juste milieu défendable est de conserver la boucle rapide de quatre scripts pour l’itération et d’exécuter la matrice complète selon un calendrier impossible à sauter, car le mode de défaillance n’est pas un build cassé que vous remarquez : c’est un IDE pris en charge qui a cessé silencieusement de l’être depuis douze versions
Cette obligation est l’envers de la livraison d’un composant natif. HotXLS lit et écrit XLS, XLSX et ODS en Object Pascal uniquement, sans installation d’Excel ni dépendance COM, ce qui rend possible l’automatisation de classeurs sans Office sur un serveur verrouillé. La même propriété signifie que le compilateur est l’intégralité du contrat de plateforme ; chaque version de la matrice est donc une promesse à revérifier, pas une hypothèse
La matrice de compilation inter-compilateurs et le code compatible avec les versions décrits ici sont livrés dans le composant tableur HotXLS pour Delphi, qui prend en charge Delphi et C++Builder de XE5 à la version actuelle avec des binaires de bibliothèque précompilés pour chaque IDE supporté