Articol tehnic

Matricea de build cross-compiler Delphi: HotXLS din XE5

HotXLS livrează un singur codebase Object Pascal pentru fiecare versiune Delphi și C++Builder de la XE5 înainte, iar build-All-Lib-TRIAL.cmd este scriptul care dovedește asta: 43 de build legs, acoperind 12 versiuni Delphi pe Win32 și Win64 plus 10 package builds C++Builder Win32 și 9 Win64. Din v2.363 până în v2.374, scriptul nu a fost rulat niciodată până la capăt, iar leg-ul XE5 a fost stricat tot timpul

Nimic din failure nu a fost subtil după ce a fost văzut. Cinci constructe distincte pe care compiler-ul curent le acceptă fără comentarii sunt hard errors pe RAD Studio XE5, pe care build matrix-ul îl numește 12.0. Versiunea v2.375.0 le-a reparat pe toate cinci și matricea a redevenit green la 43 din 43. Urmează fiecare rejection, motivul pentru care vechiul compiler are probabil dreptate în cele două cazuri pe motive de type, plus partea mai jenantă: scriptul de probe scris pentru diagnosticarea haosului a raportat un false pass la prima rulare

De ce a putrezit leg-ul XE5 fără să observe nimeni?

Leg-ul XE5 a putrezit deoarece development-ul de zi cu zi rula doar setul de patru script-uri pentru 37.0, iar un build local green nu spune nimic despre un compiler pe care nu l-ai invocat. Full matrix este un script separat și lent, pe care trial installer-ul îl apelează înainte ca Inno Setup să colecteze fișierele, așa că este exercitat la packaging, nu la commit. Douăsprezece release-uri au încăput în acel gol

Aritmetica leg-urilor merită explicată, pentru că aici trăiește iluzia de coverage. DELPHI_TRIAL_VERSIONS enumeră 12.0 până la 37.0, iar fiecare dintre cele 12 versiuni se construiește de două ori, Win32 și Win64. CB_TRIAL_WIN32_VERSIONS enumeră 10 versiuni, iar CB_TRIAL_WIN64_VERSIONS doar 9, deoarece XE5 are un package project C++Builder, dar nu livrează startup object-ul package Win64 c0pkg64.o. Doisprezece plus doisprezece plus zece plus nouă înseamnă 43. Să rulezi patru dintre ele și să numești codebase-ul portable este o eroare de categorie și este exact eroarea care a permis să se întâmple asta

HotXLS a fost lovit de aceeași formă de problemă și din direcția opusă. Un unit nou accesibil printr-o clauză uses, dar absent din lista de fișiere .cbproj, compilează perfect sub Delphi, deoarece dcc aduce implicit unit-urile nelistate în package și, în cel mai rău caz, emite un hint W1033. C++Builder emite un .obj doar pentru unit-urile numite în <DelphiCompile>, așa că același cod moare la etapa ilink cu un unresolved external. Un toolchain ascunde ce prinde celălalt. Acesta este întregul argument pentru a rula matricea, nu pentru a avea încredere într-un compiler reprezentativ

Hard type casts respinse de compilerele Win32 vechi

Două dintre cele cinci respingeri sunt același bug în haine diferite: un hard type cast aplicat unei expresii floating-point, nu unei variabile. Pe Win32, compilerele vechi evaluează aritmetica prin stack-ul x87, așa că o adunare care implică un Double este păstrată la excess precision de 80 de biți, iar tipul static devine Extended de 10 bytes. Cast-ul de la 10 bytes la un TDateTime de 8 bytes nu este un typecast legal, iar compiler-ul spune asta cu E2089 Invalid typecast

Detaliul exasperant este că forma cu variabilă este bună. TDateTime(Serial) compilează în fiecare versiune din matrix, deoarece Serial are deja 8 bytes, iar cast-ul își păstrează dimensiunea. Adaugă orice la el și expresia se lărgește dedesubtul tău. Fix-ul nu este un cast mai lat sau un conditional define, ci să nu mai faci cast: o implicit real-to-real assignment convertește corect în fiecare compiler suportat de HotXLS și spune ce înseamnă de fapt codul

// Respins pe XE5 (Win32): fiecare adunare este evaluată ca un
// Extended de 10 bytes, iar narrowing cast-ul 10-la-8 produce E2089
if Dates1904 then
  Value := TDateTime(Serial + XLSDate1904Offset)
else if Serial < 60 then
  Value := TDateTime(Serial + 1)
else
  Value := TDateTime(Serial);        // acesta este acceptat: fără adunare

// Compatibil cu versiunile: lasă assignment-ul real-to-real să facă conversia
if Dates1904 then
  Value := Serial + XLSDate1904Offset
else if Serial < 60 then
  Value := Serial + 1
else
  Value := Serial;

// Aceeași clasă de rejection în cell value packer: un Double hard cast
// al unui integer. Împarte în schimb - operatorul produce deja un real
if (Scaled = intVal) and (Double(intVal) / 100 = AValue) then   // E2089
  ;
if (Scaled = intVal) and (intVal / 100 = AValue) then           // portable
  ;

Ramura Serial < 60 este ficțiunea anului bisect 1900, nu un off-by-one: serialul 60 este 1900-02-29 inexistent al Excel-ului, așa că serialele sub el au nevoie de ziua suplimentară înainte ca DecodeDate să le vadă. Lucrul de portabilitate nu trebuie să schimbe în tăcere astfel de logică, tocmai de aceea editarea sigură elimină cast-ul și păstrează aritmetica neatinsă

Ce se rupe când nil este argument procedural?

Un nil simplu transmis acolo unde se așteaptă un procedural type nu reușește să se lege în timpul overload resolution pe compilerele vechi. Call site-ul din HotXLS este ResolveIndexedColor, care este overloaded și primește un callback TXLSTryResolveSystemColor de care majoritatea caller-ilor nu au nevoie. Compilerele mai noi rezolvă nil față de parameter-ul procedural și aleg overload-ul corect. XE5 nu o face, iar diagnosticul indică overload set-ul, nu argumentul, și așa pierzi douăzeci de minute

Răspunsul portable este să dai null callback-ului un type. O variabilă la nivel de unit a tipului procedural este zero-initialized de limbaj, deci este deja nil fără initializer și poartă informația de tip de care are nevoie resolver-ul vechi. Unde o variabilă la nivel de unit ar fi prea mult, un local typed atribuit cu nil face același lucru

var
  // Un procedural literal nil nu se leagă în overload resolution-ul
  // compiler-elor vechi; o variabilă typed și zero-initialized se leagă
  NilSystemColorResolver: TXLSTryResolveSystemColor;

// ...

FWorkbook.ResolveIndexedColor(AIndexedColor, xicsBiffIcv, ARole,
  NilSystemColorResolver, Resolution);

// Același fix cu un local typed, în workbook-ul XLSX
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;

Observă că aceasta este o diferență reală la nivel de limbaj, nu un compiler bug pe care merită să îl ocolești cu defines. Variabila zero-initialized este corectă în fiecare versiune din matrix și costă o linie, așa că nu există conditional compilation aici. Apelează la {$IF CompilerVersion} doar când platforma diferă cu adevărat între release-uri, ceea ce se întâmplă exact o dată în acest batch

Metodele VCL protected se mută între release-uri

TPicture.LoadFromStream este public în VCL curent, dar protected în versiunile vechi suportate de HotXLS, așa că un apel direct compilează acum și eșuează atunci. HotXLS îl folosește pentru a valida că un worksheet background image payload se decodează cu adevărat, un check de semnătură care rulează înainte ca HTML exporter-ul să facă commit la embedding-ul bytes-ilor. Răspunsul clasic Pascal se aplică: declară un descendant în aceeași unitate, exclusiv pentru a lărgi visibility, și fă cast prin el la call site

type
  // TPicture.LoadFromStream este protected în versiunile VCL vechi suportate
  // de bibliotecă; un descendant din aceeași unitate îl expune
  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);

Trucul cu accessor class este sigur aici deoarece descendantul nu adaugă fields și nu este instanțiat niciodată; cast-ul schimbă doar ceea ce te lasă compiler-ul să numești. Tot merită un comment la declaration, deoarece un reader care construiește doar pe un IDE curent va vedea altfel un type inutil. Tratarea imaginilor de background apare din nou în ruta de custom VCL grid rendering, unde același payload decodat alimentează sheet-ul de pe ecran

Tipul token-ului GdiplusStartup s-a schimbat de două ori

Singura rejection din batch care chiar cere conditional compilation este tipul parameter-ului var al lui GdiplusStartup, care s-a schimbat între generațiile VCL într-un mod care nu lasă o singură spelling validă peste tot. Probing-ul pe fiecare versiune a fixat comportamentul real: leg-urile 12.0 până la 20.0 acceptă doar Cardinal, leg-urile 21.0 și 22.0 acceptă doar THandle sau ULONG_PTR, iar 23.0 și 37.0 le acceptă pe ambele. În nume de release, asta înseamnă Cardinal din XE5 până la 10.3 Rio și THandle de la 10.4 Sydney încolo. Deoarece intervalele de acceptare nu se suprapun pentru 12.0 până la 22.0, nicio declaration necondiționată nu funcționează: guard-ul se bazează pe CompilerVersion >= 34, adică Sydney, iar apelul este fully qualified ca Winapi.GDIPAPI.GdiplusStartup astfel încât unit resolution order să nu poată substitui o altă declarație pe vreo versiune din mijlocul intervalului

function TXLSPageImageExporter.EncodeTiff(Stream: TStream): Integer;
var
  StartupInput: TGdiplusStartupInput;
  // Tipul var-parameter-ului GdiplusStartup din GDIPAPI urmează generația VCL:
  // Cardinal până la Rio, THandle de la Sydney încolo
  {$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;

Aceasta este ramura TIFF a page image exporter-ului, așa că blast radius-ul unei greșeli este întreaga suprafață de raster export, inclusiv rutele descrise în exportul unui cell range ca o singură imagine. Observă și ce nu pretinde guard-ul: ULONG_PTR și THandle au aceeași dimensiune pe ambele platforme, așa că alegerea ține de identificatorul numit în declarație, nu de corectitudinea 32-bit față de 64-bit

De ce nu a raportat prima rulare a probei nimic?

Version probe nu a raportat nimic la prima rulare deoarece assignment-urile de forma res=$(...) erau făcute într-un subshell, unde nu se propagă la parent. dcc32 iese cu 0 la succes, deci exit code-ul era semnalul corect de capturat, iar scriptul îl captura într-o variabilă care înceta să existe cu o linie mai târziu. Fiecare leg revenea gol, iar output-ul arăta ca un probe care nu compilase nimic, exact ceea ce era

A doua problemă era mai gravă, pentru că producea un răspuns greșit în loc de niciun răspuns. Proba clasifica un leg numărând liniile care se potriveau cu Error, iar Delphi nu prefixează fiecare fatal cu acel cuvânt. F1026 File not found este fatal și nu se potrivește, așa că o probă care nu putea rezolva deloc un unit era notată ca pass curat. XE5 nu livrează Winapi.GDIPOPS.dcu, prima probă a dat exact de asta și totuși a devenit falsely green. Regula rezultată este îngustă și merită spusă direct: judecă o compiler probe după artifact-ul produs sau după summary line a compiler-ului, niciodată după grep-ul unui keyword din output. Grep-uirea stderr după Error este o heuristics care eșuează în direcția pe care nu ți-o permiți, raportând succes în tăcere

Cât costă cu adevărat suportarea unui deceniu de compilere

Socoteala onestă este că schimbările de cod de aici sunt triviale, iar schimbările de proces nu sunt. Patru dintre cele cinci respingeri au fost reparate scriind Pascal mai obișnuit, nu adăugând version machinery: elimină un cast, împarte în loc să faci cast, dă-i lui nil un type, declară un accessor class. Doar GdiplusStartup a meritat un {$IF}. Un codebase care se întinde de la XE5 la release-ul curent nu devine un hățiș de conditional defines decât dacă lași hard casts și idiomuri ale compiler-ului nou să se acumuleze de la început

Costul real este build time și disciplină. Patruzeci și trei de legs înseamnă un script lent, tocmai de aceea a migrat la packaging time și apoi la never. Zona de mijloc defensabilă este să păstrezi bucla rapidă de patru script-uri pentru iterație și să rulezi full matrix după un schedule care nu poate fi sărit, pentru că failure mode-ul nu este un build rupt pe care îl observi, ci un IDE suportat care a încetat tăcut să fie suportat cu douăsprezece release-uri în urmă

Această obligație este reversul livrării unui component nativ. HotXLS citește și scrie XLS, XLSX și ODS doar prin Object Pascal, fără instalare Excel și fără dependență COM, ceea ce face posibilă automatizarea workbook-urilor fără Office pe un server blocat. Aceeași proprietate înseamnă că compiler-ul este întregul contract de platformă, așa că fiecare versiune din matrix este o promisiune care trebuie reverificată, nu presupusă

Cross-compiler build matrix și codul version-safe discutat aici sunt livrate în HotXLS Delphi Spreadsheet Component, care suportă Delphi și C++Builder de la XE5 până la release-ul curent, cu library binaries prebuilt pentru fiecare IDE suportat