HotXLS dodáva jednu codebase Object Pascalu každému release Delphi a C++Buildera od XE5 vyššie a build-All-Lib-TRIAL.cmd je skript, ktorý to dokazuje: 43 build legs pokrývajúcich 12 verzií Delphi na Win32 a Win64 plus 10 package buildov C++Builder Win32 a 9 Win64. Od v2.363 po v2.374 sa tento skript nikdy nespustil do konca a leg XE5 bol pokazený celý čas
Keď sa zlyhanie uvidelo, nič na ňom nebolo subtílne. Päť rôznych konštruktov, ktoré aktuálny kompilátor prijíma bez komentára, sú na RAD Studio XE5 tvrdé chyby, pričom build matrix ich označuje ako 12.0. Release v2.375.0 opravil všetkých päť a matica znovu zozelenala na 43 z 43. Nasleduje každé odmietnutie, dôvod, prečo má starý kompilátor pri dvoch odmietnutiach na typovej úrovni argumenty na svojej strane, a trápnejšia časť: probe script napísaný na diagnostiku neporiadku pri prvom spustení nahlásil falošný úspech
Prečo leg XE5 potichu zhnila bez toho, aby si to niekto všimol?
Leg XE5 zhnila, pretože každodenný vývoj spúšťal iba štvoricu scriptov 37.0 a zelený lokálny build nehovorí nič o kompilátore, ktorý ste nevyvolali. Plná matica je samostatný pomalý skript, ktorý trial installer volá predtým, než Inno Setup zhromaždí súbory, takže sa vykonáva pri packagingu, nie pri commite. Do tejto medzery sa zmestilo dvanásť release
Aritmetiku legov sa oplatí rozpísať, pretože práve v nej žije ilúzia coverage. DELPHI_TRIAL_VERSIONS vymenúva 12.0 až 37.0 a každá z týchto 12 verzií sa builduje dvakrát, Win32 a Win64. CB_TRIAL_WIN32_VERSIONS uvádza 10 verzií a CB_TRIAL_WIN64_VERSIONS iba 9, pretože XE5 má C++Builder package project, ale nedodáva Win64 package startup object c0pkg64.o. Dvanásť plus dvanásť plus desať plus deväť je 43. Spustiť štyri z nich a nazvať codebase prenositeľnou je category error a presne táto chyba to umožnila
HotXLS narazil na rovnaký tvar problému aj z opačnej strany. Nový unit, ktorý je dosiahnuteľný cez klauzulu uses, ale chýba v zozname súborov .cbproj, sa pod Delphi skompiluje dokonale, pretože dcc implicitne vtiahne neuvedené unity do package a nanajvýš vydá hint W1033. C++Builder vytvorí .obj iba pre unity uvedené v <DelphiCompile>, takže rovnaký kód zomrie vo fáze ilink na unresolved external. Jeden toolchain skrýva to, čo druhý zachytí. To je celý argument pre spustenie matice namiesto dôvery reprezentatívnemu kompilátoru
Hard type casty, ktoré staré Win32 kompilátory odmietajú
Dve z piatich odmietnutí sú tá istá chyba v dvoch prevlekoch: hard type cast aplikovaný na floating-point výraz namiesto premennej. Na Win32 staršie kompilátory vyhodnocujú aritmetiku cez x87 stack, takže sčítanie s Double sa nesie v 80-bitovej excess precision a jeho statický typ sa stane 10-bajtovým Extended. Pretypovanie 10 bajtov nadol na 8-bajtový TDateTime nie je legálny typecast a kompilátor to povie ako E2089 Invalid typecast
Najotravnejší detail je, že forma s premennou je v poriadku. TDateTime(Serial) sa skompiluje vo všetkých verziách matice, pretože Serial už má 8 bajtov a cast zachováva veľkosť. Niečo k nemu pripočítajte a výraz sa pod vami rozšíri. Opravou nie je širší cast ani conditional define, ale prestať castovať: implicitné real-to-real priradenie konvertuje správne na každom kompilátore, ktorý HotXLS podporuje, a hovorí to, čo kód skutočne znamená
// Odmietnuté na XE5 (Win32): každé sčítanie sa vyhodnotí ako 10-bajtový
// Extended a zúženie z 10 na 8 bajtov vyvolá E2089
if Dates1904 then
Value := TDateTime(Serial + XLSDate1904Offset)
else if Serial < 60 then
Value := TDateTime(Serial + 1)
else
Value := TDateTime(Serial); // toto je prijaté: bez sčítania
// Bezpečné naprieč verziami: nech konverziu vykoná real-to-real priradenie
if Dates1904 then
Value := Serial + XLSDate1904Offset
else if Serial < 60 then
Value := Serial + 1
else
Value := Serial;
// Rovnaká trieda odmietnutia v packeri hodnoty bunky: hard Double cast
// celého čísla. Namiesto toho del - operátor už vracia real
if (Scaled = intVal) and (Double(intVal) / 100 = AValue) then // E2089
;
if (Scaled = intVal) and (intVal / 100 = AValue) then // prenositeľné
;
Vetva Serial < 60 je fikcia priestupného roka 1900, nie off-by-one: serial 60 je neexistujúci Excel dátum 1900-02-29, takže serialy pod ním potrebujú extra deň skôr, než ich uvidí DecodeDate. Prenositeľnostná práca by nikdy nemala potichu meniť takúto logiku, práve preto bezpečná úprava odstráni cast a aritmetiku ponechá nedotknutú
Čo sa pokazí, keď je nil procedurálny argument?
Holé nil odovzdané tam, kde sa očakáva procedural type, sa na starších kompilátoroch počas overload resolution nenaviaže. Call site v HotXLS je ResolveIndexedColor, ktorý je overloaded a prijíma callback TXLSTryResolveSystemColor, ktorý väčšina volajúcich nepotrebuje. Novšie kompilátory vyriešia nil proti procedurálnemu parametru a vyberú správny overload. XE5 to neurobí a diagnostika ukáže na overload set, nie na argument, čo je spôsob, ako stratiť dvadsať minút
Prenositeľná odpoveď je dať null callbacku typ. Premenná na úrovni unitu s procedural type je jazykom inicializovaná na nulu, takže už je nil bez inicializátora a nesie typovú informáciu, ktorú starý resolver potrebuje. Tam, kde by unit-level premenná bola prehnaná, urobí rovnakú prácu typed local priradený na nil
var
// Procedurálny literál nil sa v overload resolution starších
// kompilátorov nenaviaže; typed premenná inicializovaná na nulu áno
NilSystemColorResolver: TXLSTryResolveSystemColor;
// ...
FWorkbook.ResolveIndexedColor(AIndexedColor, xicsBiffIcv, ARole,
NilSystemColorResolver, Resolution);
// Rovnaká oprava s typed local v XLSX workbooke
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;
Všimnite si, že ide o skutočný rozdiel na úrovni jazyka, nie o compiler bug, ktorý treba obísť defines. Premenná inicializovaná na nulu je správna vo všetkých verziách matice a stojí jeden riadok, takže tu vôbec nie je conditional compilation. Po {$IF CompilerVersion} siahnite iba vtedy, keď sa platforma medzi release skutočne líši, čo je v tejto batch presne raz
Protected VCL metódy sa medzi release presúvajú
TPicture.LoadFromStream je na aktuálnom VCL public a na starších verziách, ktoré HotXLS podporuje, protected, takže priame volanie sa teraz skompiluje a vtedy zlyhá. HotXLS ho používa na overenie, že payload obrázka pozadia worksheetu sa naozaj dekóduje, teda signature check, ktorý prebehne skôr, než sa HTML exporter zaviaže vložiť bajty. Platí klasická Pascalová odpoveď: deklarujte potomka v rovnakom unite čisto na rozšírenie viditeľnosti a na call site castujte cez neho
type
// TPicture.LoadFromStream je protected na starších VCL verziách,
// ktoré knižnica podporuje; potomok v rovnakom unite ho sprístupní
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);
Trik s accessor class je tu bezpečný, pretože potomok nepridáva polia a nikdy sa neinštanciuje; cast iba zmení to, čo vám kompilátor dovolí pomenovať. Aj tak sa oplatí komentár pri deklarácii, pretože čitateľ, ktorý builduje iba na aktuálnom IDE, inak uvidí zbytočný typ. Práca s obrázkom pozadia sa znovu objavuje v ceste renderovania vlastnej VCL grid, kde ten istý dekódovaný payload zásobuje sheet na obrazovke
Typ tokenu GdiplusStartup sa zmenil dvakrát
Jediné odmietnutie v batch, ktoré skutočne vyžaduje conditional compilation, je typ parametra var funkcie GdiplusStartup, ktorý sa medzi generáciami VCL zmenil spôsobom, pri ktorom neexistuje jedno platné pomenovanie všade. Probe po verziách pripol skutočné správanie: legy 12.0 až 20.0 prijímajú iba Cardinal, legy 21.0 a 22.0 iba THandle alebo ULONG_PTR a 23.0 aj 37.0 prijímajú obe. V názvoch release je to Cardinal od XE5 po 10.3 Rio a THandle od 10.4 Sydney ďalej. Keďže dve prijímajúce pásma sa pre 12.0 až 22.0 neprekrývajú, žiadna unconditional declaration nefunguje: guard sa opiera o CompilerVersion >= 34, čo je Sydney, a volanie je plne kvalifikované ako Winapi.GDIPAPI.GdiplusStartup, aby poradie resolvingu unitov nemohlo na nejakej verzii uprostred rozsahu dosadiť inú deklaráciu
function TXLSPageImageExporter.EncodeTiff(Stream: TStream): Integer;
var
StartupInput: TGdiplusStartupInput;
// Typ var-parametra GdiplusStartup v GDIPAPI sleduje generáciu VCL:
// Cardinal po Rio, THandle od Sydney ďalej
{$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;
Toto je TIFF vetva page image exportera, takže blast radius nesprávneho riešenia je celá plocha raster exportu vrátane ciest opísaných v článku o exporte rozsahu buniek ako jediného obrázka. Všimnite si tiež, čo guard netvrdí: ULONG_PTR a THandle majú na oboch platformách rovnakú šírku, takže voľba sa týka toho, ktoré identifier deklarácia pomenúva, nie správnosti pre 32 bitov verzus 64 bitov
Prečo prvý probe run nič nenahlásil?
Version probe pri prvom behu nič nenahlásil, pretože assignments res=$(...) sa vykonávali v subshelli, odkiaľ sa neprenášajú do parenta. dcc32 pri úspechu končí na 0, takže exit code bol správny signál na zachytenie a skript ho zachytával do premennej, ktorá o riadok neskôr prestala existovať. Každý leg sa vrátil prázdny a výstup vyzeral ako probe, ktorý nič neskompiloval, čo presne aj bol
Druhé zlyhanie bolo horšie, pretože vyrobilo nesprávnu odpoveď namiesto žiadnej. Probe klasifikoval leg počítaním riadkov zhodujúcich sa s Error a Delphi neprefiksuje týmto slovom každú fatálnu chybu. F1026 File not found je fatálna a nezhoduje sa, takže probe, ktorý vôbec nevedel vyriešiť unit, dostal skóre čistého úspechu. XE5 nedodáva Winapi.GDIPOPS.dcu, prvý probe narazil presne na to a potichu zozelenal. Pravidlo, ktoré z toho vzišlo, je úzke a stojí za priame vyslovenie: compiler probe posudzujte podľa vytvoreného artefaktu alebo podľa vlastného summary riadku kompilátora, nikdy nie grepom výstupu na keyword. Grepovať stderr na Error je heuristika, ktorá zlyhá práve v smere, ktorý si nemôžete dovoliť, a potichu nahlási úspech
Čo v skutočnosti stojí podpora desaťročia kompilátorov
Poctivé účtovníctvo je, že zmeny kódu tu sú triviálne a procesné zmeny nie. Štyri z piatich odmietnutí sa opravili napísaním obyčajnejšieho Pascalu, nie pridaním version machinery: odstráňte cast, namiesto castovania delte, dajte nil typ, deklarujte accessor class. Iba GdiplusStartup si vyslúžil {$IF}. Codebase od XE5 po aktuálny release sa nestane húštinou conditional defines, pokiaľ v prvom rade dovolíte, aby sa v ňom hromadili hard casty a idiomy najnovšieho kompilátora
Skutočná cena je build time a disciplína. Štyridsaťtri legov je pomalý skript, práve preto driftol k packagingu a potom k nikdy. Obhájiteľný stred je ponechať rýchlu štvoricu scriptov na iteráciu a plnú maticu spúšťať podľa harmonogramu, ktorý nemožno preskočiť, pretože failure mode nie je pokazený build, ktorý si všimnete, ale podporované IDE, ktoré potichu prestalo byť podporované pred dvanástimi release
Táto povinnosť je odvrátenou stranou dodávania natívneho komponentu vôbec. HotXLS číta a zapisuje XLS, XLSX a ODS iba cez Object Pascal, bez inštalácie Excelu a bez COM závislosti, čo umožňuje automatizáciu workbookov bez Office na uzamknutom serveri. Tá istá vlastnosť znamená, že kompilátor je celý platform contract, takže každá verzia v matici je prísľub, ktorý treba znovu overiť, nie predpokladať
Cross-compiler build matrix a version-safe kód opísaný tu sa dodávajú ako súčasť HotXLS Delphi Spreadsheet Component, ktorý podporuje Delphi a C++Builder od XE5 po aktuálny release s prebuilt library binaries pre každé podporované IDE