Tehnički članak

Delphi cross-compiler build matrix: HotXLS od XE5

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