HotXLS dodává jediný codebase v Object Pascalu do každého releasu Delphi a C++Builderu od XE5 dál a build-All-Lib-TRIAL.cmd je skript, který to dokazuje: 43 build legů, pokrývajících 12 verzí Delphi na Win32 a Win64 plus 10 package buildů C++Builderu Win32 a 9 Win64. Od v2.363 do v2.374 nebyl tento skript nikdy dokončen a větev XE5 byla po celou dobu rozbitá
Jakmile se chyba uviděla, nebylo na ní nic subtilního. Pět odlišných konstrukcí, které aktuální kompilátor přijímá bez poznámky, jsou na RAD Studio XE5, které build matrix označuje 12.0, tvrdé chyby. Release v2.375.0 opravil všech pět a matrix znovu zezelenala na 43 z 43. Následuje každé odmítnutí, proč má starý kompilátor u dvou z nich z hlediska typů možná pravdu, a trapnější část: probe skript napsaný pro diagnostiku nepořádku při prvním běhu ohlásil falešný průchod
Proč větev XE5 zestárla, aniž si toho někdo všiml
Větev XE5 zestárla, protože každodenní vývoj spouštěl pouze čtyřskriptovou sadu 37.0 a zelený lokální build nic neříká o kompilátoru, který jste nevyvolali. Plná matrix je samostatný pomalý skript, který trial installer volá před tím, než Inno Setup sesbírá soubory, takže se vykonává při packagingu místo při commitu. Dvanáct release se do této mezery vejde
Aritmetiku větve stojí za to rozepsat, protože právě v ní žije iluze pokrytí. DELPHI_TRIAL_VERSIONS vypisuje 12.0 až 37.0 a každá z těchto 12 verzí se sestaví dvakrát, Win32 a Win64. CB_TRIAL_WIN32_VERSIONS uvádí 10 verzí a CB_TRIAL_WIN64_VERSIONS jen 9, protože XE5 má projekt package pro C++Builder, ale nedodává Win64 package startup object c0pkg64.o. Dvanáct plus dvanáct plus deset plus devět je 43. Spustit čtyři z nich a označit codebase za portable je kategoriální chyba a přesně tato chyba to umožnila
HotXLS narazil na stejný tvar problému i z opačné strany. Nová jednotka dosažitelná přes klauzuli uses, ale chybějící v seznamu souborů .cbproj, se pod Delphi zkompiluje bez potíží, protože dcc nepřihlášené jednotky do package implicitně přitáhne a nanejvýš vypíše hint W1033. C++Builder vytvoří .obj pouze pro jednotky uvedené v <DelphiCompile>, takže stejný kód zemře ve fázi ilink na unresolved external. Jeden toolchain skrývá to, co druhý zachytí. To je celý argument pro spouštění matrix místo důvěry v reprezentativní kompilátor
Tvrdé typecasty, které staré Win32 kompilátory odmítají
Dvě z pěti odmítnutí jsou stejná chyba v jiném oblečení: hard typecast aplikovaný na floating-point výraz místo na proměnnou. Na Win32 starší kompilátory vyhodnocují aritmetiku přes x87 stack, takže sčítání obsahující Double drží v 80bitové extra přesnosti a jeho statickým typem se stane 10bajtový Extended. Přetypování 10 bajtů na 8bajtový TDateTime není legální typecast a kompilátor to řekne jako E2089 Invalid typecast
Šílený detail je, že forma s proměnnou je v pořádku. TDateTime(Serial) se zkompiluje ve všech verzích matrix, protože Serial už má 8 bajtů a cast zachovává velikost. Něco k němu přičtěte a výraz se pod vámi rozšíří. Opravou není širší cast ani podmíněný define, ale přestat castovat: implicitní assignment real-to-real převede správně v každém kompilátoru podporovaném HotXLS a říká, co kód skutečně znamená
// Na XE5 (Win32) odmítnuto: každé sčítání se vyhodnotí jako 10bajtový
// Extended a narrowing cast z 10 na 8 vyvolá E2089
if Dates1904 then
Value := TDateTime(Serial + XLSDate1904Offset)
else if Serial < 60 then
Value := TDateTime(Serial + 1)
else
Value := TDateTime(Serial); // tento je přijat: bez sčítání
// Bezpečné napříč verzemi: nech real-to-real assignment provést převod
if Dates1904 then
Value := Serial + XLSDate1904Offset
else if Serial < 60 then
Value := Serial + 1
else
Value := Serial;
// Stejná třída odmítnutí v packeru hodnoty buňky: hard Double cast
// celého čísla. Místo toho děl - operátor už vrací real
if (Scaled = intVal) and (Double(intVal) / 100 = AValue) then // E2089
;
if (Scaled = intVal) and (intVal / 100 = AValue) then // portable
;
Větev Serial < 60 je fikce přestupného roku 1900, nikoli off-by-one: serial 60 je neexistující Excel datum 1900-02-29, takže serialy pod ním potřebují před DecodeDate další den. Práce na přenositelnosti nesmí potichu změnit tento druh logiky, a právě proto bezpečná úprava odstraní cast a aritmetiku ponechá
Co se rozbije, když je nil procedurálním argumentem
Holé nil předané tam, kde se očekává procedurální typ, se u starších kompilátorů během overload resolution nepřipojí. Call site v HotXLS je ResolveIndexedColor, které je overloaded a přijímá callback TXLSTryResolveSystemColor, jenž většina volajících nepotřebuje. Novější kompilátory vyřeší nil proti procedurálnímu parametru a vyberou správný overload. XE5 to nedokáže a diagnostika ukáže na overload set místo na argument, což je způsob, jak ztratit dvacet minut
Přenosná odpověď je dát null callbacku typ. Proměnná na úrovni unity procedurálního typu se inicializuje nulou podle jazyka, takže už je nil bez inicializátoru a nese typovou informaci, kterou starý resolver potřebuje. Tam, kde by unit-level proměnná byla přehnaná, udělá totéž typovaný local přiřazený na nil
var
// Procedurální literal nil se u starších kompilátorů nepřipojí při
// overload resolution; typovaná proměnná inicializovaná nulou ano
NilSystemColorResolver: TXLSTryResolveSystemColor;
// ...
FWorkbook.ResolveIndexedColor(AIndexedColor, xicsBiffIcv, ARole,
NilSystemColorResolver, Resolution);
// Stejná oprava s typovaným localem v XLSX workbooku
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;
Je to skutečný rozdíl na úrovni jazyka, nikoli compiler bug, který má smysl obcházet pomocí defines. Proměnná inicializovaná nulou je správná ve všech verzích matrix a stojí jeden řádek, takže zde není žádná conditional compilation. Po {$IF CompilerVersion} sáhněte jen tehdy, když se platforma mezi releasy skutečně liší, což se v této batch stane přesně jednou
Protected metody VCL se mezi releasy přesouvají
TPicture.LoadFromStream je v aktuální VCL public a ve starších verzích podporovaných HotXLS protected, takže přímé volání se nyní zkompiluje a tehdy selže. HotXLS ho používá k ověření, že payload obrázku pozadí worksheet se skutečně dekóduje, což je kontrola běžící předtím, než HTML exporter odsouhlasí vložení bajtů. Uplatní se klasická Pascalová odpověď: ve stejné unitě deklarujte potomka jen kvůli rozšíření visibility a v místě volání přes něj proveďte cast
type
// TPicture.LoadFromStream je ve starších podporovaných VCL verzích
// protected; potomek ve stejné unitě ho zpří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 třídou je zde bezpečný, protože potomek nepřidává žádná pole a nikdy se nevytváří; cast pouze mění, co vám kompilátor dovolí pojmenovat. Přesto stojí za komentář u deklarace, protože čtenář, který buildí jen na aktuálním IDE, jinak uvidí zdánlivě zbytečný typ. Práce s obrázkem pozadí se znovu objeví v cestě vykreslení vlastní VCL grid, kde stejný dekódovaný payload napájí sheet na obrazovce
Typ tokenu GdiplusStartup se změnil dvakrát
Jediné odmítnutí v batchi, které skutečně vyžaduje conditional compilation, je typ parametru var u GdiplusStartup, jenž se mezi generacemi VCL změnil tak, že žádný jednotný zápis není platný všude. Probing po verzích skutečné chování zafixoval: větve 12.0 až 20.0 přijímají pouze Cardinal, 21.0 a 22.0 pouze THandle nebo ULONG_PTR a 23.0 a 37.0 přijímají obojí. V názvech release je to Cardinal od XE5 do 10.3 Rio a THandle od 10.4 Sydney. Protože se akceptované rozsahy pro 12.0 až 22.0 nepřekrývají, žádná unconditional deklarace nefunguje: guard se řídí CompilerVersion >= 34, což je Sydney, a volání je plně kvalifikované jako Winapi.GDIPAPI.GdiplusStartup, takže pořadí resolvingu jednotek nemůže v některé verzi uprostřed rozsahu dosadit jinou deklaraci
function TXLSPageImageExporter.EncodeTiff(Stream: TStream): Integer;
var
StartupInput: TGdiplusStartupInput;
// Typ var-parametru GDIPAPI's GdiplusStartup sleduje generaci VCL
// podle kompilátoru: Cardinal do Rio, THandle od 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');
// ... encode ...
end;
Jde o TIFF větev exporteru obrázků stránky, takže blast radius špatné opravy je celá plocha rasterového exportu včetně cest popsaných v exportu rozsahu buněk jako jediného obrázku. Všimněte si také, co guard netvrdí: ULONG_PTR a THandle mají na obou platformách stejnou šířku, takže volba se týká toho, které jméno deklarace používá, nikoli správnosti 32bitového proti 64bitovému cíli
Proč první běh probe neohlásil nic
Version probe při prvním běhu nic neohlásil, protože přiřazení res=$(...) probíhala v subshellu, odkud se do parenta nepřenášejí. dcc32 při úspěchu končí nulou, takže exit code byl správný signál ke čtení, ale skript ho uložil do proměnné, která o řádek později přestala existovat. Každá větev se vrátila prázdná a výstup vypadal jako probe, který nic nezkompiloval, což také přesně byl
Druhé selhání bylo horší, protože dalo špatnou odpověď místo žádné. Probe klasifikoval větev počítáním řádků odpovídajících Error a Delphi každou fatální chybu tímto slovem neprefixuje. F1026 File not found je fatální a neodpovídá, takže probe, který nedokázal vyřešit vůbec žádnou unit, dostal skóre čistého průchodu. XE5 nedodává Winapi.GDIPOPS.dcu, první probe narazil přesně na to a falešně zezelenal. Pravidlo, které z toho vzniklo, je úzké a stojí za přímé uvedení: compiler probe posuzujte podle vytvořeného artefaktu nebo podle vlastního souhrnného řádku kompilátoru, nikdy grepem výstupu na keyword. Grepování stderr na Error je heuristika, která selhává právě směrem, který si nemůžete dovolit, a potichu hlásí úspěch
Co skutečně stojí podpora celé dekády kompilátorů
Poctivé účetnictví říká, že změny kódu zde jsou triviální a změny procesu nikoli. Čtyři z pěti odmítnutí se opravily napsáním obyčejnějšího Pascalu, nikoli přidáním version machinery: odstranit cast, dělit místo castování, dát nil typ a deklarovat accessor class. Jen GdiplusStartup si vysloužil {$IF}. Codebase sahající od XE5 po aktuální release se nestane houštinou conditional defines, pokud nenecháte hard casts a idiomy nejnovějšího kompilátoru hromadit od začátku
Co to opravdu stojí, je čas buildu a disciplína. Třiačtyřicet větví je pomalý skript, právě proto se posunul k packagingu a potom k nikdy. Obhajitelný střed je ponechat rychlý čtyřskriptový loop pro iteraci a plnou matrix spouštět podle plánu, který nelze přeskočit, protože failure mode není rozbitý build, kterého si všimnete, ale podporované IDE, které se potichu přestalo podporovat před dvanácti releasy
Tato povinnost je odvrácenou stranou toho, že vůbec distribuujete nativní komponentu. HotXLS čte a zapisuje XLS, XLSX a ODS pouze přes Object Pascal, bez instalace Excelu a bez COM závislosti, což umožňuje automatizaci workbooku bez Office na uzamčeném serveru. Stejná vlastnost znamená, že kompilátor je celý kontrakt platformy, takže každá verze v matrix je slib, který je třeba znovu ověřit, nikoli předpokládat
Cross-compiler build matrix a version-safe kód popsaný zde jsou součástí HotXLS Delphi Spreadsheet Component, která podporuje Delphi a C++Builder od XE5 po aktuální release s předbuilděnými library binaries pro každé podporované IDE