HotXLS distribuisce un solo codebase Object Pascal a ogni release Delphi e C++Builder dalla XE5 in avanti, e build-All-Lib-TRIAL.cmd è lo script che lo dimostra: 43 build leg, che coprono 12 versioni Delphi su Win32 e Win64 più 10 build di package C++Builder Win32 e 9 Win64. Dalla v2.363 alla v2.374 lo script non è mai stato eseguito fino alla fine e il leg XE5 è rimasto rotto per tutto il tempo
Una volta visto, nulla del guasto era sottile. Cinque costrutti distinti che il compilatore attuale accetta senza commenti sono errori hard su RAD Studio XE5, che la build matrix etichetta 12.0. La release v2.375.0 ha corretto tutti e cinque e la matrice è tornata verde a 43 su 43. Seguono i cinque rifiuti, il motivo per cui il vecchio compilatore ha ragione almeno sui due rifiuti di tipo e la parte più imbarazzante: lo script di probe scritto per diagnosticare il disastro ha riportato un falso pass al primo tentativo
Perché il leg XE5 è marcito senza che nessuno se ne accorgesse?
Il leg XE5 è marcito perché lo sviluppo quotidiano eseguiva solo il set di quattro script della 37.0, e una build locale verde non dice nulla su un compilatore che non avete invocato. La matrice completa è uno script separato e lento che il trial installer chiama prima che Inno Setup raccolga i file, quindi viene eseguita al packaging invece che al commit. Dodici release entrano in quel vuoto
Vale la pena esplicitare l'aritmetica dei leg, perché è qui che vive l'illusione della copertura. DELPHI_TRIAL_VERSIONS elenca da 12.0 a 37.0 e ognuna di queste 12 versioni viene compilata due volte, Win32 e Win64. CB_TRIAL_WIN32_VERSIONS elenca 10 versioni e CB_TRIAL_WIN64_VERSIONS solo 9, perché XE5 ha un progetto package C++Builder ma non distribuisce l'oggetto di startup package Win64 c0pkg64.o. Dodici più dodici più dieci più nove fa 43. Eseguire quattro di questi leg e chiamare portabile il codebase è un errore di categoria, ed è l'errore specifico che ha permesso tutto questo
HotXLS è stato colpito dalla stessa forma di problema nella direzione opposta. Una nuova unità raggiungibile tramite una clausola uses ma assente dall'elenco file del .cbproj compila perfettamente sotto Delphi, perché dcc include implicitamente le unità non elencate e al massimo emette un hint W1033. C++Builder emette un .obj solo per le unità nominate in <DelphiCompile>, quindi lo stesso codice muore allo stage ilink con un unresolved external. Una toolchain nasconde ciò che l'altra intercetta. Questo è l'intero argomento per eseguire la matrice invece di fidarsi di un compilatore rappresentativo
Hard type cast rifiutati dai vecchi compilatori Win32
Due dei cinque rifiuti sono lo stesso bug con vestiti diversi: un hard type cast applicato a un'espressione floating-point invece che a una variabile. Su Win32 i compilatori più vecchi valutano l'aritmetica attraverso lo stack x87, quindi un'addizione che coinvolge un Double viene mantenuta con precisione extra a 80 bit e il suo tipo statico diventa l'Extended da 10 byte. Ridurre con un cast 10 byte a un TDateTime da 8 byte non è un typecast legale e il compilatore lo segnala con E2089 Invalid typecast
Il dettaglio esasperante è che la forma con variabile è corretta. TDateTime(Serial) compila in ogni versione della matrice, perché Serial è già largo 8 byte e il cast preserva la dimensione. Aggiungete qualcosa e l'espressione si allarga sotto di voi. La correzione non è un cast più largo né un define condizionale: è smettere di fare il cast. Un'assegnazione real-to-real implicita converte correttamente su ogni compilatore supportato da HotXLS e dice ciò che il codice significa davvero
// Rifiutato su XE5 (Win32): ogni addizione viene valutata come Extended
// da 10 byte e il cast da 10 a 8 byte genera E2089
if Dates1904 then
Value := TDateTime(Serial + XLSDate1904Offset)
else if Serial < 60 then
Value := TDateTime(Serial + 1)
else
Value := TDateTime(Serial); // questo è accettato: nessuna addizione
// Compatibile tra le versioni: lasciare che l'assegnazione real-to-real esegua la conversione
if Dates1904 then
Value := Serial + XLSDate1904Offset
else if Serial < 60 then
Value := Serial + 1
else
Value := Serial;
// Stessa classe di rifiuto nel packer del valore della cella: hard cast Double
// di un intero. Dividere invece: l'operatore restituisce già un real
if (Scaled = intVal) and (Double(intVal) / 100 = AValue) then // E2089
;
if (Scaled = intVal) and (intVal / 100 = AValue) then // portabile
;
Il ramo Serial < 60 è la finzione dell'anno bisestile 1900, non un off-by-one: il serial 60 è il 1900-02-29 inesistente di Excel, quindi i serial inferiori richiedono un giorno aggiuntivo prima che DecodeDate li veda. Il lavoro di portabilità non deve mai cambiare silenziosamente questo tipo di logica, ed è proprio per questo che l'edit sicuro rimuove il cast e lascia intatta l'aritmetica
Cosa si rompe quando nil è un argomento procedurale?
Un nil nudo passato dove è atteso un tipo procedurale non riesce a fare il binding durante l'overload resolution sui compilatori più vecchi. Il punto di chiamata in HotXLS è ResolveIndexedColor, overloaded e con un callback TXLSTryResolveSystemColor che la maggior parte dei caller non richiede. I compilatori più recenti risolvono nil rispetto al parametro procedurale e selezionano l'overload corretto. XE5 non lo fa, e la diagnostica punta all'insieme degli overload invece che all'argomento, motivo per cui si perdono venti minuti
La risposta portabile è dare un tipo al callback nullo. Una variabile a livello di unità del tipo procedurale viene inizializzata a zero dal linguaggio, quindi è già nil senza initializer e porta con sé le informazioni di tipo richieste dal vecchio resolver. Quando una variabile a livello di unità sarebbe eccessiva, una variabile locale tipizzata assegnata a nil svolge lo stesso lavoro
var
// Un literal procedurale nil non si associa nell'overload resolution dei
// compilatori vecchi; una variabile tipizzata e inizializzata a zero sì
NilSystemColorResolver: TXLSTryResolveSystemColor;
// ...
FWorkbook.ResolveIndexedColor(AIndexedColor, xicsBiffIcv, ARole,
NilSystemColorResolver, Resolution);
// La stessa correzione con una variabile locale tipizzata, nel workbook 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;
È una differenza reale a livello di linguaggio, non un compiler bug da aggirare con i define. La variabile inizializzata a zero è corretta in ogni versione della matrice e costa una riga, quindi qui non c'è alcuna compilazione condizionale. Ricorrete a {$IF CompilerVersion} solo quando la piattaforma differisce davvero tra release, come accade esattamente una volta in questo batch
I metodi VCL protected cambiano tra le release
TPicture.LoadFromStream è public nella VCL attuale e protected nelle versioni vecchie supportate da HotXLS, quindi una chiamata diretta compila ora e fallisce allora. HotXLS lo usa per verificare che un payload di immagine di sfondo del worksheet venga davvero decodificato, un controllo di integrità eseguito prima che l'exporter HTML si impegni a incorporare i byte. Vale la risposta classica Pascal: dichiarare nella stessa unità un descendant solo per ampliare la visibilità e fare il cast attraverso di esso nel punto di chiamata
type
// TPicture.LoadFromStream è protected nelle vecchie VCL supportate dalla
// libreria; un descendant nella stessa unità lo rende visibile
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);
Il trucco della accessor class è sicuro perché il descendant non aggiunge campi e non viene mai istanziato; il cast cambia solo ciò che il compilatore permette di nominare. Vale comunque la pena lasciare un commento alla dichiarazione, perché un lettore che compila solo su un IDE attuale vedrebbe altrimenti un tipo inutile. La gestione delle immagini di sfondo ricompare nel percorso di rendering della griglia VCL personalizzata, dove lo stesso payload decodificato alimenta il foglio a schermo
Il tipo del token GdiplusStartup è cambiato due volte
L'unico rifiuto del batch che richiede davvero compilazione condizionale è il tipo del parametro var di GdiplusStartup, cambiato tra le generazioni VCL in un modo che non lascia una singola grafia valida ovunque. Il probing versione per versione ha fissato il comportamento reale: i leg da 12.0 a 20.0 accettano solo Cardinal, quelli 21.0 e 22.0 accettano solo THandle o ULONG_PTR, e 23.0 e 37.0 accettano entrambi. Nei nomi delle release, significa Cardinal da XE5 a 10.3 Rio e THandle da 10.4 Sydney in poi. Poiché gli intervalli accettati non si sovrappongono tra 12.0 e 22.0, nessuna dichiarazione incondizionata funziona: la guardia usa CompilerVersion >= 34, che corrisponde a Sydney, e la chiamata è qualificata completamente come Winapi.GDIPAPI.GdiplusStartup così l'ordine di risoluzione delle unità non può sostituire una dichiarazione diversa in qualche versione intermedia
function TXLSPageImageExporter.EncodeTiff(Stream: TStream): Integer;
var
StartupInput: TGdiplusStartupInput;
// Il tipo del parametro var di GdiplusStartup in GDIPAPI segue la generazione
// VCL: Cardinal fino a Rio, THandle da Sydney in poi
{$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');
// ... codifica ...
end;
Questo è il ramo TIFF dell'exporter di immagini pagina, quindi il blast radius di un errore comprende l'intera superficie di export raster, inclusi i percorsi descritti in esportare un cell range come immagine singola. Notate anche cosa non afferma la guardia: ULONG_PTR e THandle hanno la stessa dimensione su entrambe le piattaforme, quindi la scelta riguarda l'identificatore nominato dalla dichiarazione, non la correttezza a 32 o 64 bit
Perché il primo probe non ha riportato nulla?
Il version probe non ha riportato nulla al primo run perché le assegnazioni res=$(...) venivano effettuate dentro una subshell, dove non si propagano al parent. dcc32 esce con 0 in caso di successo, quindi il codice di uscita era il segnale giusto da catturare e lo script lo catturava in una variabile che cessava di esistere una riga dopo. Ogni leg tornava vuoto e l'output sembrava quello di una probe che non avesse compilato nulla, esattamente ciò che era successo
Il secondo fallimento era peggiore, perché produceva una risposta errata invece di nessuna risposta. La probe classificava un leg contando le righe che corrispondevano a Error, ma Delphi non antepone quella parola a ogni fatal. F1026 File not found è fatale e non corrisponde, quindi una probe che non riusciva a risolvere nemmeno un'unità veniva valutata come pass pulito. XE5 non distribuisce Winapi.GDIPOPS.dcu, la prima probe ha colpito esattamente questo caso e ha dato un falso verde. La regola che ne è uscita è stretta ma va detta: giudicate una compiler probe dall'artefatto prodotto o dalla riga di riepilogo del compilatore, mai cercando una parola nell'output. Cercare Error nello stderr è un'euristica che fallisce nella direzione che non potete permettervi, segnalando silenziosamente successo
Quanto costa davvero supportare un decennio di compilatori
Il conto onesto è che le modifiche al codice qui sono banali, mentre i cambiamenti di processo non lo sono. Quattro dei cinque rifiuti sono stati corretti scrivendo Pascal più ordinario, non aggiungendo meccanismi di versione: eliminare un cast, dividere invece di fare il cast, dare un tipo a nil, dichiarare una accessor class. Solo GdiplusStartup si è guadagnato un {$IF}. Un codebase che va da XE5 alla release attuale non diventa un groviglio di define condizionali, a meno che non si lascino accumulare hard cast e idiomi dell'ultimo compilatore fin dall'inizio
Il costo reale è tempo di build e disciplina. Quarantatré leg sono uno script lento, proprio per questo è scivolato al packaging e poi al mai. Il compromesso difendibile è mantenere il loop veloce di quattro script per l'iterazione ed eseguire la matrice completa secondo una schedulazione che non possa essere saltata, perché il failure mode non è una build rotta che notate: è un IDE supportato che ha smesso silenziosamente di essere supportato dodici release fa
Questo obbligo è il rovescio della medaglia di distribuire un componente nativo. HotXLS legge e scrive XLS, XLSX e ODS solo tramite Object Pascal, senza installazione di Excel né dipendenza COM, ed è ciò che rende possibile l'automazione di workbook senza Office su un server bloccato. La stessa proprietà significa che il compilatore è l'intero contratto di piattaforma, quindi ogni versione della matrice è una promessa da riverificare, non da presumere
La matrice di build cross-compiler e il codice version-safe trattati qui sono inclusi nel componente Spreadsheet Delphi HotXLS, che supporta Delphi e C++Builder da XE5 alla release attuale con binari di libreria precompilati per ogni IDE supportato