HotXLS leverer én Object Pascal-kodebase til hver Delphi- og C++Builder-release fra XE5 og frem, og build-All-Lib-TRIAL.cmd er scriptet, der beviser det: 43 build-ben, der dækker 12 Delphi-versioner på Win32 og Win64 plus 10 C++Builder Win32- og 9 Win64-package-builds. Fra v2.363 til v2.374 blev scriptet aldrig kørt til ende, og XE5-benet var ødelagt hele tiden
Intet ved fejlen var subtilt, da den først blev set. Fem forskellige konstruktioner, som den aktuelle compiler accepterer uden en kommentar, er hårde fejl på RAD Studio XE5, som buildmatricen mærker 12.0. v2.375.0 rettede alle fem, og matricen blev grøn igen med 43 af 43. Det følgende er hver afvisning, hvorfor den gamle compiler med rimelighed har ret i de to, den afviser på typegrundlag, og den mere pinlige del: Probe-scriptet, der blev skrevet for at diagnosticere rodet, rapporterede en falsk succes ved første kørsel
Hvorfor rådnede XE5-benet, uden at nogen lagde mærke til det?
XE5-benet rådnede, fordi den daglige udvikling kun kørte sættet på fire scripts til 37.0, og et grønt lokalt build siger intet om en compiler, du ikke kaldte. Den fulde matrix er et separat, langsomt script, som trial-installeren kalder, før Inno Setup samler filer, så den bliver kørt ved packaging i stedet for ved commit. Tolv releases passer ind i det hul
Ben-aritmetikken er værd at skrive ud, fordi det er dér, dækningsillusionen bor. DELPHI_TRIAL_VERSIONS opregner 12.0 til 37.0, og hver af de 12 versioner bygger to gange, Win32 og Win64. CB_TRIAL_WIN32_VERSIONS lister 10 versioner, og CB_TRIAL_WIN64_VERSIONS kun 9, fordi XE5 har et C++Builder-packageprojekt, men ikke leverer Win64-package-startobjektet c0pkg64.o. Tolv plus tolv plus ti plus ni er 43. At køre fire af dem og kalde kodebasen portabel er en kategorifejl, og det er den konkrete fejl, der lod dette ske
HotXLS er blevet ramt af samme problemform fra den modsatte retning. En ny unit, der nås gennem en uses-klausul, men mangler i .cbproj-filens liste, kompilerer perfekt under Delphi, fordi dcc implicit trækker unoterede units ind i pakken og højst udsender et W1033-hint. C++Builder udsender kun en .obj for units, der er navngivet i <DelphiCompile>, så den samme kode dør i ilink-fasen med en unresolved external. Den ene toolchain skjuler det, den anden fanger. Det er hele argumentet for at køre matricen i stedet for at stole på en repræsentativ compiler
Hårde typecasts, som de gamle Win32-compilere afviser
To af de fem afvisninger er den samme bug i forskelligt tøj: Et hårdt typecast anvendes på et floating-point-udtryk i stedet for på en variabel. På Win32 evaluerer de ældre compilere aritmetik gennem x87-stacken, så en addition, der involverer en Double, holdes med 80-bit excess precision, og dens statiske type bliver den 10 bytes lange Extended. At caste 10 bytes ned til en 8 bytes lang TDateTime er ikke et lovligt typecast, og compileren siger det med E2089 Invalid typecast
Den irriterende detalje er, at variabelformen er fin. TDateTime(Serial) kompilerer i alle versioner i matricen, fordi Serial allerede er 8 bytes, og castet bevarer størrelsen. Tilføj hvad som helst til det, og udtrykket udvider sig under dig. Rettelsen er ikke et bredere cast eller et conditional define, men at holde op med at caste: En implicit real-to-real-assignment konverterer korrekt på alle compilere, HotXLS understøtter, og den siger, hvad koden faktisk betyder
// Afvises på XE5 (Win32): hver addition evalueres som en 10 bytes lang
// Extended, og narrowing fra 10 til 8 bytes giver E2089
if Dates1904 then
Value := TDateTime(Serial + XLSDate1904Offset)
else if Serial < 60 then
Value := TDateTime(Serial + 1)
else
Value := TDateTime(Serial); // denne accepteres: ingen addition
// Versionssikker: lad real-to-real-assignmenten udføre konverteringen
if Dates1904 then
Value := Serial + XLSDate1904Offset
else if Serial < 60 then
Value := Serial + 1
else
Value := Serial;
// Samme type af afvisning i cell-value-packeren: et hårdt Double-cast
// af et heltal. Divider i stedet - operatoren giver allerede en real
if (Scaled = intVal) and (Double(intVal) / 100 = AValue) then // E2089
;
if (Scaled = intVal) and (intVal / 100 = AValue) then // portabel
;
Grenen Serial < 60 er 1900-skudårsfiktionen og ikke en off-by-one: Serial 60 er Excels ikke-eksisterende 1900-02-29, så serials under den skal have den ekstra dag, før DecodeDate ser dem. Portabilitetsarbejde må aldrig lydløst ændre den slags logik, hvilket netop er grunden til, at den sikre redigering fjerner castet og lader aritmetikken være urørt
Hvad går i stykker, når nil er et procedureargument?
En bar nil, der sendes, hvor en procedural type forventes, binder ikke under overload resolution på de ældre compilere. Kaldestedet i HotXLS er ResolveIndexedColor, som er overloaded og tager en callback af typen TXLSTryResolveSystemColor, som de fleste kaldere ikke har brug for. Nyere compilere opløser nil mod procedureparameteren og vælger den rigtige overload. XE5 gør ikke, og diagnostikken peger på overload-sættet i stedet for argumentet, hvilket er sådan, man mister 20 minutter
Det portable svar er at give null-callbacken en type. En variabel på unitniveau af proceduretypen nulinitialiseres af sproget, så den er allerede nil uden en initializer, og den bærer den typeinformation, den gamle resolver ønsker. Hvor en variabel på unitniveau ville være overkill, gør en typed local, der tildeles nil, det samme
var
// En nil-procedureliteral binder ikke i de ældre compileres
// overload resolution; en typet, nulinitialiseret variabel gør
NilSystemColorResolver: TXLSTryResolveSystemColor;
// ...
FWorkbook.ResolveIndexedColor(AIndexedColor, xicsBiffIcv, ARole,
NilSystemColorResolver, Resolution);
// Den samme rettelse med en typed local i XLSX-workbooken
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;
Bemærk, at dette er en reel forskel på sprogniveau og ikke en compilerbug, der er værd at omgå med defines. Den nulinitialiserede variabel er korrekt på hver version i matricen og koster én linje, så der er slet ingen conditional compilation her. Brug kun {$IF CompilerVersion}, når platformen reelt adskiller sig mellem releases, hvilket sker præcis én gang i denne batch
Beskyttede VCL-metoder flytter sig mellem releases
TPicture.LoadFromStream er public i den aktuelle VCL og protected i de ældre versioner, HotXLS understøtter, så et direkte kald kompilerer nu og fejler dér. HotXLS bruger det til at validere, at en worksheet-background-image-payload faktisk afkodes, en signaturkontrol, der kører, før HTML-exporteren binder sig til at indlejre bytesene. Det klassiske Pascal-svar gælder: Deklarér en descendant i den samme unit udelukkende for at udvide synligheden, og cast gennem den på kaldestedet
type
// TPicture.LoadFromStream er protected på de ældre VCL-versioner,
// biblioteket understøtter; en descendant i samme unit eksponerer den
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);
Accessor-class-tricket er sikkert her, fordi descendanten ikke tilføjer felter og aldrig instantieres; castet ændrer kun, hvad compileren lader dig navngive. Det er stadig værd at have en kommentar ved deklarationen, fordi en læser, der kun bygger på en aktuel IDE, ellers vil se en meningsløs type. Håndtering af background images dukker igen op i den brugerdefinerede VCL-grid-rendering, hvor den samme afkodede payload føder arket på skærmen
GdiplusStartup-token-typen ændrede sig to gange
Den eneste afvisning i batchen, der reelt kræver conditional compilation, er var-parameterens type i GdiplusStartup, som ændrede sig på tværs af VCL-generationer på en måde, der ikke efterlader én stavemåde, der er gyldig overalt. Probe pr. version fastslog den faktiske adfærd: Benene 12.0 til 20.0 accepterer kun Cardinal, benene 21.0 og 22.0 accepterer kun THandle eller ULONG_PTR, og 23.0 og 37.0 accepterer begge. I release-navne betyder det Cardinal fra XE5 til og med 10.3 Rio og THandle fra 10.4 Sydney og frem. Fordi de to accepterende intervaller ikke overlapper for 12.0 til 22.0, virker ingen ubetinget deklaration: Guarden bruger CompilerVersion >= 34, som er Sydney, og kaldet er fuldt kvalificeret som Winapi.GDIPAPI.GdiplusStartup, så unit-opløsningsrækkefølgen ikke kan substituere en anden deklaration i en version midt i intervallet
function TXLSPageImageExporter.EncodeTiff(Stream: TStream): Integer;
var
StartupInput: TGdiplusStartupInput;
// GDIPAPI's GdiplusStartup-var-parameter følger VCL-generationen:
// Cardinal til og med Rio, THandle fra Sydney og frem
{$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');
// ... encode ...
end;
Dette er TIFF-grenen i sidebilledeksportøren, så blast radius ved at få det forkert er hele rastereksportfladen, inklusive de stier, der er beskrevet i eksport af et celleområde som ét billede. Bemærk også, hvad guarden ikke påstår: ULONG_PTR og THandle har samme bredde på begge platforme, så valget handler om, hvilken identifikator deklarationen navngiver, og ikke om 32-bit- kontra 64-bit-korrekthed
Hvorfor rapporterede den første probe-kørsel ingenting?
Versionsproben rapporterede ingenting ved første kørsel, fordi assignmenter som res=$(...) blev foretaget inde i en subshell, hvor de ikke føres videre til parent. dcc32 afslutter med 0 ved succes, så exitkoden var det rigtige signal at opsamle, og scriptet opsamlede den i en variabel, der ophørte med at eksistere en linje senere. Hvert ben kom tilbage tomt, og outputtet lignede en probe, der ikke havde kompileret noget, hvilket præcis var, hvad den var
Den anden fejl var værre, fordi den producerede et forkert svar og ikke intet svar. Proben klassificerede et ben ved at tælle linjer, der matchede Error, og Delphi sætter ikke det ord foran enhver fatal fejl. F1026 File not found er fatal og matcher ikke, så en probe, der slet ikke kunne opløse en unit, blev scoret som en ren succes. XE5 leverer ikke Winapi.GDIPOPS.dcu, den første probe ramte præcis det, og den blev falsk grøn. Reglen, der kom ud af det, er snæver og værd at sige klart: Bedøm en compilerprobe på det producerede artifact eller på compilerens egen summary-linje, aldrig ved at greppe dens output efter et keyword. At greppe stderr efter Error er en heuristik, der fejler i den retning, du mindst har råd til, og rapporterer lydløst succes
Hvad koster det reelt at understøtte et årti af compilere?
Den ærlige opgørelse er, at kodeændringerne her er trivielle, mens procesændringerne ikke er. Fire af de fem afvisninger blev rettet ved at skrive mere almindelig Pascal og ikke ved at tilføje versionsmaskineri: fjern et cast, divider i stedet for at caste, giv nil en type og deklarér en accessor-class. Kun GdiplusStartup fortjente en {$IF}. En kodebase, der spænder fra XE5 til den aktuelle release, bliver ikke et krat af conditional defines, medmindre du lader hårde casts og idiomer fra den nyeste compiler samle sig i første omgang
Det, det faktisk koster, er buildtid og disciplin. 43 ben er et langsomt script, hvilket netop er grunden til, at det gled til packagingtid og derefter til aldrig. Den forsvarlige mellemvej er at beholde den hurtige løkke på fire scripts til iteration og køre den fulde matrix efter en tidsplan, der ikke kan springes over, fordi fejltilstanden ikke er et ødelagt build, du opdager, men en understøttet IDE, der lydløst holdt op med at være understøttet for tolv releases siden
Den forpligtelse er bagsiden af overhovedet at levere en native komponent. HotXLS læser og skriver XLS, XLSX og ODS kun gennem Object Pascal uden Excel-installation og uden COM-afhængighed, hvilket gør Office-fri workbook-automation mulig på en låst server. Den samme egenskab betyder, at compileren er hele platformskontrakten, så hver version i matricen er et løfte, der skal verificeres igen og ikke antages
Cross-compiler-buildmatricen og den versionssikre kode, der beskrives her, leveres som en del af HotXLS Delphi Spreadsheet Component, som understøtter Delphi og C++Builder fra XE5 til den aktuelle release med præbyggede biblioteksbinærer til hver understøttet IDE