HotXLS isporučuje jednu Object Pascal codebase za svako Delphi i C++Builder izdanje od XE5 nadalje, a build-All-Lib-TRIAL.cmd je skripta koja to dokazuje: 43 build leg-a, sa 12 Delphi verzija na Win32 i Win64, plus 10 C++Builder Win32 i 9 Win64 package build-ova. Od v2.363 do v2.374 ta skripta nikada nije izvršena do kraja, a XE5 leg je sve vreme bio pokvaren
Ništa u neuspehu nije bilo suptilno čim je viđeno. Pet različitih konstrukcija koje aktuelni kompajler prihvata bez komentara jesu hard errors na RAD Studio XE5, koji build matrix označava kao 12.0. Izdanje v2.375.0 popravilo je svih pet, pa je matrix ponovo postao zelen sa 43 od 43. Sledi svako odbijanje, razlog zbog kojeg je stari kompajler za dve greške na nivou tipova verovatno u pravu i još neprijatniji deo: probe skripta napisana da dijagnostikuje problem prijavila je lažni prolaz pri prvom pokretanju
Zašto je XE5 leg propadao a da niko nije primetio?
XE5 leg je propao zato što se svakodnevni razvoj izvršavao samo kroz set od četiri skripte za 37.0, a zeleni lokalni build ne govori ništa o kompajleru koji niste pokrenuli. Ceo matrix je zasebna, spora skripta koju trial installer poziva pre nego što Inno Setup sakupi fajlove, pa se izvršava u trenutku pakovanja, a ne pri commit-u. Dvanaest izdanja uklopilo se u tu prazninu
Vredi ispisati aritmetiku leg-ova jer se upravo tu krije iluzija pokrivenosti. DELPHI_TRIAL_VERSIONS nabraja 12.0 do 37.0 i svaka od tih 12 verzija gradi se dvaput, za Win32 i Win64. CB_TRIAL_WIN32_VERSIONS navodi 10 verzija, a CB_TRIAL_WIN64_VERSIONS samo 9, jer XE5 ima C++Builder package projekat, ali ne isporučuje Win64 package startup objekat c0pkg64.o. Dvanaest plus dvanaest plus deset plus devet jeste 43. Pokrenuti četiri od njih i nazvati codebase prenosivim jeste kategorijalna greška, a upravo je ta greška dozvolila da se ovo desi
HotXLS je već bio pogođen istim oblikom problema iz suprotnog smera. Nova jedinica koja je dohvatljiva kroz uses klauzulu, ali nedostaje na spisku fajlova .cbproj, kompajlira se savršeno pod Delphi-jem, jer dcc implicitno uvlači nenavedene jedinice u package i u najgorem slučaju emituje W1033 hint. C++Builder emituje .obj samo za jedinice imenovane u <DelphiCompile>, pa isti kod umire u ilink fazi sa unresolved external. Jedan toolchain skriva ono što drugi uhvati. To je ceo argument za pokretanje matrix-a umesto poverenja reprezentativnom kompajleru
Hard type cast-ovi koje stari Win32 kompajleri odbijaju
Dva od pet odbijanja jesu isti bag u različitoj odeći: hard type cast primenjen na izraz sa pokretnim zarezom umesto na promenljivu. Na Win32 stariji kompajleri računaju aritmetiku preko x87 stack-a, pa se sabiranje koje uključuje Double izvršava sa 80-bitnom excess precision i njegov statički tip postaje 10-bajtni Extended. Cast sa 10 bajtova na 8-bajtni TDateTime nije legalan typecast, a kompajler to kaže kao E2089 Invalid typecast
Izluđujući detalj jeste da je oblik sa promenljivom ispravan. TDateTime(Serial) kompajlira se u svakoj verziji matrix-a, jer je Serial već širok 8 bajtova, a cast čuva veličinu. Dodajte bilo šta i izraz se ispod vas proširi. Popravka nije širi cast ni conditional define, već prestanak cast-ovanja: implicitna real-to-real dodela ispravno konvertuje na svakom kompajleru koji HotXLS podržava i govori šta kod stvarno znači
// Odbijeno na XE5 (Win32): svako sabiranje se računa kao 10-bajtni
// Extended, a narrowing cast sa 10 na 8 bajtova podiže E2089
if Dates1904 then
Value := TDateTime(Serial + XLSDate1904Offset)
else if Serial < 60 then
Value := TDateTime(Serial + 1)
else
Value := TDateTime(Serial); // ovo je prihvaćeno: nema sabiranja
// Bezbedno po verzijama: pusti real-to-real dodelu da obavi konverziju
if Dates1904 then
Value := Serial + XLSDate1904Offset
else if Serial < 60 then
Value := Serial + 1
else
Value := Serial;
// Ista klasa odbijanja u pakeru vrednosti ćelije: hard Double cast
// celog broja. Deli umesto toga - operator već daje realan rezultat
if (Scaled = intVal) and (Double(intVal) / 100 = AValue) then // E2089
;
if (Scaled = intVal) and (intVal / 100 = AValue) then // prenosivo
;
Grana Serial < 60 jeste fikcija prestupne godine 1900, a ne off-by-one: serijski broj 60 je nepostojeći Excel datum 1900-02-29, pa serijali ispod njega zahtevaju dodatni dan pre nego što ih DecodeDate vidi. Rad na prenosivosti nikada ne treba tiho da promeni takvu logiku, zbog čega bezbedna izmena ovde uklanja cast, a aritmetiku ostavlja netaknutom
Šta se lomi kada je nil proceduralni argument?
Goli nil prosleđen tamo gde se očekuje proceduralni tip ne uspeva da se veže tokom overload resolution-a na starijim kompajlerima. Call site u HotXLS-u je ResolveIndexedColor, koji je overload-ovan i prima callback TXLSTryResolveSystemColor koji većini pozivalaca nije potreban. Noviji kompajleri razrešavaju nil prema proceduralnom parametru i biraju pravi overload. XE5 to ne radi, a dijagnostika pokazuje na skup overload-a, a ne na argument, što je način da izgubite dvadeset minuta
Prenosiv odgovor jeste da null callback-u date tip. Promenljivu proceduralnog tipa na nivou jedinice jezik inicijalizuje nulom, pa je već nil bez inicijalizatora i nosi informaciju o tipu koju stari resolver želi. Tamo gde bi promenljiva na nivou jedinice bila previše, typed local kome dodelite nil radi isto
var
// Nil proceduralni literal ne može da se veže u overload resolution-u
// starijih kompajlera; typed, nula-inicijalizovana promenljiva može
NilSystemColorResolver: TXLSTryResolveSystemColor;
// ...
FWorkbook.ResolveIndexedColor(AIndexedColor, xicsBiffIcv, ARole,
NilSystemColorResolver, Resolution);
// Ista popravka sa typed local promenljivom u XLSX workbook-u
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;
Obratite pažnju da je ovo stvarna razlika na nivou jezika, a ne bag kompajlera koji vredi zaobići define-ima. Nula-inicijalizovana promenljiva ispravna je na svakoj verziji matrix-a i košta jednu liniju, pa ovde nema conditional compilation-a. Za {$IF CompilerVersion} posegnite samo kada se platforma zaista razlikuje između izdanja, što se u ovom batch-u dešava tačno jednom
Zaštićene VCL metode pomeraju se između izdanja
TPicture.LoadFromStream je public u aktuelnom VCL-u, a protected u starijim verzijama koje HotXLS podržava, pa direktan poziv sada kompajlira, a tada pada. HotXLS ga koristi da potvrdi da se payload pozadinske slike worksheet-a zaista dekodira, što je provera potpisa koja se izvršava pre nego što HTML exporter potvrdi upis bajtova. Primena klasičnog Pascal odgovora ovde je prava: deklarišite naslednika u istoj jedinici samo da proširite vidljivost i cast-ujte kroz njega na mestu poziva
type
// TPicture.LoadFromStream je protected u starijim VCL verzijama
// koje biblioteka podržava; naslednik iz iste jedinice ga izlaže
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 sa accessor klasom bezbedan je ovde zato što naslednik ne dodaje polja i nikada se ne instancira; cast samo menja ono što kompajler dozvoljava da imenujete. Ipak vredi ostaviti komentar uz deklaraciju, jer bi čitalac koji gradi samo na aktuelnom IDE-u inače video beskoristan tip. Obrada pozadinske slike ponovo se pojavljuje u putanji renderovanja prilagođene VCL grid tabele, gde isti dekodirani payload hrani sheet na ekranu
Tip GdiplusStartup tokena promenio se dvaput
Jedino odbijanje u batch-u koje zaista zahteva conditional compilation jeste tip var parametra za GdiplusStartup, koji se kroz generacije VCL-a promenio na način zbog kojeg nijedan jedinstveni spelling nije validan svuda. Probing po verzijama precizirao je stvarno ponašanje: leg-ovi 12.0 do 20.0 prihvataju samo Cardinal, leg-ovi 21.0 i 22.0 samo THandle ili ULONG_PTR, a 23.0 i 37.0 prihvataju oba. U imenima izdanja, to je Cardinal od XE5 do 10.3 Rio, a THandle od 10.4 Sydney nadalje. Pošto se opsezi koji prihvataju dva oblika ne preklapaju za 12.0 do 22.0, nijedna bezuslovna deklaracija ne radi: guard se vezuje za CompilerVersion >= 34, što je Sydney, a poziv je u potpunosti kvalifikovan kao Winapi.GDIPAPI.GdiplusStartup da redosled razrešavanja jedinica ne bi na nekoj verziji u sredini opsega zamenio deklaraciju
function TXLSPageImageExporter.EncodeTiff(Stream: TStream): Integer;
var
StartupInput: TGdiplusStartupInput;
// GDIPAPI GdiplusStartup var-parameter prati generaciju VCL-a:
// Cardinal do Rio-a, THandle od Sydney-a nadalje
{$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;
Ovo je TIFF grana exporter-a slika stranice, pa je blast radius pogrešne implementacije cela površina raster izvoza, uključujući putanje opisane u tekstu o izvozu opsega ćelija kao jedne slike. Obratite pažnju i na ono što guard ne tvrdi: ULONG_PTR i THandle iste su širine na obe platforme, pa je izbor pitanje imena koje deklaracija navodi, a ne tačnosti 32-bitnog naspram 64-bitnog koda
Zašto je prvo probe pokretanje prijavilo ništa?
Version probe nije pri prvom pokretanju prijavio ništa zato što su dodele oblika res=$(...) izvršavane unutar subshell-a, gde se ne prenose roditelju. dcc32 vraća 0 pri uspehu, pa je exit code bio pravi signal za hvatanje, a skripta ga je hvatala u promenljivu koja je prestajala da postoji jedan red kasnije. Svaki leg vraćao se prazan, a izlaz je izgledao kao probe koji ništa nije kompajlirao, što je upravo i bio slučaj
Drugi neuspeh bio je gori, jer je proizveo pogrešan odgovor umesto nikakvog odgovora. Probe je klasifikovao leg brojanjem linija koje se podudaraju sa Error, a Delphi ne prefiksuje svaku fatalnu grešku tom rečju. F1026 File not found jeste fatalan i ne podudara se, pa je probe koji uopšte nije mogao da razreši jedinicu ocenjen kao čist prolaz. XE5 ne isporučuje Winapi.GDIPOPS.dcu, prvi probe je upravo to pogodio, a rezultat je lažno pozelenio. Pravilo koje je iz toga proizašlo usko je i vredi ga reći jasno: compiler probe procenjujte prema proizvedenom artefaktu ili prema sopstvenoj summary liniji kompajlera, nikada grepanjem izlaza po keyword-u. Grepovanje stderr-a za Error heuristika je koja pada u smeru koji ne možete sebi priuštiti, nečujno prijavljujući uspeh
Koliko zaista košta podrška deceniji kompajlera
Pošten obračun kaže da su izmene koda ovde trivijalne, a izmene procesa nisu. Četiri od pet odbijanja popravljena su pisanjem običnijeg Pascal-a, a ne dodavanjem version machinery: uklonite cast, delite umesto cast-ovanja, dajte nil-u tip, deklarišite accessor klasu. Samo je GdiplusStartup zaslužio jedan {$IF}. Codebase koji obuhvata XE5 do aktuelnog izdanja ne postaje šuma conditional define-a osim ako prvo dopustite da se hard cast-ovi i idiomi najnovijeg kompajlera nagomilaju
Prava cena su vreme izgradnje i disciplina. Četrdeset tri leg-a čine sporu skriptu, što je upravo razlog da je skliznula u vreme pakovanja, a zatim u nikad. Odbranjiva sredina jeste zadržati brzu petlju od četiri skripte za iteraciju, a puni matrix pokretati po rasporedu koji se ne može preskočiti, jer način otkaza nije pokvaren build koji primetite, već podržani IDE koji je tiho prestao da bude podržan pre dvanaest izdanja
Ta obaveza je druga strana isporuke native komponente uopšte. HotXLS čita i upisuje XLS, XLSX i ODS samo kroz Object Pascal, bez instaliranog Excel-a i bez COM zavisnosti, što omogućava automatizaciju radnih sveski bez Office-a na zaključanom serveru. Ista osobina znači da je kompajler ceo ugovor platforme, pa je svaka verzija u matrix-u obećanje koje mora ponovo da se proveri, a ne da se podrazumeva
Cross-compiler build matrix i version-safe kod obrađeni ovde isporučuju se kao deo HotXLS Delphi Spreadsheet komponente, koja podržava Delphi i C++Builder od XE5 do aktuelnog izdanja, uz prebuilt library binare za svaki podržani IDE