Το HotXLS παρέχει ένα Object Pascal codebase σε κάθε release Delphi και C++Builder από το XE5 και μετά, και το build-All-Lib-TRIAL.cmd είναι το script που το αποδεικνύει: 43 build legs, με 12 Delphi versions σε Win32 και Win64 συν 10 C++Builder Win32 και 9 Win64 package builds. Από το v2.363 έως το v2.374 αυτό το script δεν έτρεξε ποτέ μέχρι τέλους, και το XE5 leg ήταν σπασμένο όλο αυτό το διάστημα
Τίποτε στην αποτυχία δεν ήταν subtle αφού την είδες. Πέντε διαφορετικές constructs που ο current compiler δέχεται χωρίς σχόλιο είναι hard errors στο RAD Studio XE5, το οποίο το build matrix ονομάζει 12.0. Το release v2.375.0 διόρθωσε και τις πέντε και το matrix πρασίνισε ξανά στο 43 από 43. Παρακάτω είναι κάθε rejection, γιατί ο παλιός compiler έχει εν μέρει δίκιο για τα δύο που απορρίπτει σε επίπεδο types και το πιο embarrassing μέρος: το probe script που γράφτηκε για τη διάγνωση ανέφερε false pass στην πρώτη εκτέλεσή του
Γιατί σάπισε το XE5 leg χωρίς να το παρατηρήσει κανείς
Το XE5 leg σάπισε επειδή το day-to-day development έτρεχε μόνο το 37.0 four-script set, και ένα green local build δεν λέει τίποτε για compiler που δεν invoked. Το full matrix είναι ξεχωριστό, αργό script που ο trial installer καλεί πριν το Inno Setup μαζέψει files, άρα ασκείται στο packaging time και όχι στο commit time. Δώδεκα releases χωρούσαν μέσα σε αυτό το κενό
Αξίζει να εξηγηθεί η arithmetic του leg επειδή εκεί βρίσκεται η ψευδαίσθηση κάλυψης. Το DELPHI_TRIAL_VERSIONS απαριθμεί τις 12.0 έως 37.0 και καθεμία από αυτές τις 12 versions χτίζεται δύο φορές, Win32 και Win64. Το CB_TRIAL_WIN32_VERSIONS παραθέτει 10 versions και το CB_TRIAL_WIN64_VERSIONS μόνο 9, επειδή το XE5 διαθέτει C++Builder package project αλλά δεν διανέμει το Win64 package startup object c0pkg64.o. Δώδεκα συν δώδεκα συν δέκα συν εννέα κάνει 43. Το να τρέξεις τέσσερις από αυτές και να αποκαλέσεις το codebase portable είναι category error, και είναι ακριβώς το error που το άφησε να συμβεί
Το HotXLS έχει πέσει στο ίδιο σχήμα και από την αντίθετη κατεύθυνση. Μια νέα unit που είναι reachable μέσω uses clause αλλά λείπει από το .cbproj file list κάνει compile κανονικά στο Delphi, επειδή το dcc τραβά implicit unlisted units στο package και στη χειρότερη περίπτωση βγάζει hint W1033. Το C++Builder εκδίδει .obj μόνο για units που ονομάζονται σε <DelphiCompile>, οπότε ο ίδιος code πεθαίνει στο ilink stage με unresolved external. Η μία toolchain κρύβει αυτό που πιάνει η άλλη. Αυτός είναι όλος ο λόγος να τρέχεις το matrix αντί να εμπιστεύεσαι representative compiler
Hard type casts που απορρίπτουν οι παλιοί Win32 compilers
Δύο από τα πέντε rejections είναι το ίδιο bug με διαφορετικό ρούχο: hard type cast πάνω σε floating-point expression και όχι σε variable. Στο Win32 οι παλιότεροι compilers αξιολογούν arithmetic μέσω x87 stack, οπότε addition που περιλαμβάνει Double κρατιέται σε 80-bit excess precision και ο static type της γίνεται το 10-byte Extended. Cast από 10 bytes σε 8-byte TDateTime δεν είναι νόμιμο typecast και ο compiler το λέει με E2089 Invalid typecast
Η εκνευριστική λεπτομέρεια είναι ότι η variable form είναι σωστή. Το TDateTime(Serial) κάνει compile σε κάθε version του matrix επειδή το Serial είναι ήδη 8 bytes και το cast διατηρεί το μέγεθος. Πρόσθεσε οτιδήποτε σε αυτό και η expression πλαταίνει από κάτω σου. Η διόρθωση δεν είναι wider cast ή conditional define, αλλά να σταματήσεις να κάνεις cast: implicit real-to-real assignment κάνει σωστά τη conversion σε κάθε compiler που υποστηρίζει το HotXLS και λέει τι σημαίνει πράγματι ο code
// Απορρίπτεται στο XE5 (Win32): κάθε addition αξιολογείται ως 10-byte
// Extended και το narrowing cast 10-to-8 σηκώνει E2089
if Dates1904 then
Value := TDateTime(Serial + XLSDate1904Offset)
else if Serial < 60 then
Value := TDateTime(Serial + 1)
else
Value := TDateTime(Serial); // αυτό γίνεται δεκτό: χωρίς addition
// Version-safe: άφησε το real-to-real assignment να κάνει τη conversion
if Dates1904 then
Value := Serial + XLSDate1904Offset
else if Serial < 60 then
Value := Serial + 1
else
Value := Serial;
// Ίδια κατηγορία rejection στον cell value packer: hard Double cast
// ενός integer. Κάνε divide αντί για cast — ο operator δίνει ήδη real
if (Scaled = intVal) and (Double(intVal) / 100 = AValue) then // E2089
;
if (Scaled = intVal) and (intVal / 100 = AValue) then // portable
;
Το branch Serial < 60 είναι η fiction του leap year 1900 και όχι off-by-one: το serial 60 είναι το ανύπαρκτο 1900-02-29 του Excel, άρα τα serials κάτω από αυτό χρειάζονται την extra day πριν το δει το DecodeDate. Η δουλειά φορητότητας δεν πρέπει ποτέ να αλλάζει σιωπηρά τέτοια λογική, και γι’ αυτό η safe edit αφαιρεί το cast και αφήνει την arithmetic ανέγγιχτη
Τι χαλά όταν το nil είναι procedural argument
Ένα γυμνό nil σε θέση procedural type αποτυγχάνει να γίνει bind κατά το overload resolution στους παλιότερους compilers. Το call site στο HotXLS είναι το ResolveIndexedColor, overloaded και με callback TXLSTryResolveSystemColor που οι περισσότεροι callers δεν χρειάζονται. Οι νεότεροι compilers λύνουν το nil απέναντι στο procedural parameter και επιλέγουν το σωστό overload. Το XE5 δεν το κάνει, και το diagnostic δείχνει στο overload set αντί στο argument, πράγμα που είναι ο τρόπος να χάσεις είκοσι λεπτά
Η portable απάντηση είναι να δώσεις type στο null callback. Variable σε επίπεδο unit του procedural type είναι zero-initialized από τη γλώσσα, άρα είναι ήδη nil χωρίς initializer και μεταφέρει τις type information που θέλει ο παλιός resolver. Όπου variable σε επίπεδο unit θα ήταν υπερβολή, typed local με assignment nil κάνει την ίδια δουλειά
var
// Ένα nil procedural literal δεν γίνεται bind στο overload resolution
// των παλιότερων compilers· typed, zero-initialized variable γίνεται
NilSystemColorResolver: TXLSTryResolveSystemColor;
// ...
FWorkbook.ResolveIndexedColor(AIndexedColor, xicsBiffIcv, ARole,
NilSystemColorResolver, Resolution);
// Η ίδια διόρθωση με typed local, στο XLSX workbook
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;
Σημείωσε ότι πρόκειται για πραγματική διαφορά σε επίπεδο γλώσσας και όχι για compiler bug που αξίζει workaround με defines. Η zero-initialized variable είναι σωστή σε κάθε version του matrix και κοστίζει μία γραμμή, άρα δεν υπάρχει conditional compilation εδώ. Χρησιμοποίησε {$IF CompilerVersion} μόνο όταν η platform διαφέρει πράγματι ανάμεσα σε releases, πράγμα που σε αυτό το batch συμβαίνει ακριβώς μία φορά
Protected VCL methods που μετακινούνται ανάμεσα σε releases
Το TPicture.LoadFromStream είναι public στη current VCL και protected στις παλιότερες versions που υποστηρίζει το HotXLS, οπότε direct call κάνει compile τώρα και αποτυγχάνει τότε. Το HotXLS το χρησιμοποιεί για να επαληθεύσει ότι ένα worksheet background image payload πράγματι κάνει decode, signature check που τρέχει πριν ο HTML exporter δεσμευτεί να ενσωματώσει τα bytes. Εδώ εφαρμόζεται η κλασική Pascal απάντηση: δήλωσε descendant στην ίδια unit μόνο για να διευρύνεις το visibility και κάνε cast μέσω αυτού στο call site
type
// Το TPicture.LoadFromStream είναι protected στις παλιότερες VCL versions
// που υποστηρίζει η library· descendant της ίδιας unit το εκθέτει
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);
Το accessor-class trick είναι ασφαλές εδώ επειδή ο descendant δεν προσθέτει fields και δεν γίνεται ποτέ instantiate· το cast αλλάζει μόνο όσα επιτρέπει να ονομάσεις ο compiler. Παρ’ όλα αυτά αξίζει comment στη declaration, επειδή reader που χτίζει μόνο σε current IDE διαφορετικά θα δει ένα pointless type. Το background image handling εμφανίζεται ξανά στο custom VCL grid rendering path, όπου το ίδιο decoded payload τροφοδοτεί το on-screen sheet
Ο τύπος token του GdiplusStartup άλλαξε δύο φορές
Η μοναδική rejection σε αυτό το batch που χρειάζεται πραγματικά conditional compilation είναι ο τύπος του var parameter του GdiplusStartup, ο οποίος άλλαξε ανάμεσα σε VCL generations με τρόπο που δεν αφήνει ένα spelling έγκυρο παντού. Το probing ανά version κλείδωσε την πραγματική συμπεριφορά: τα legs 12.0 έως 20.0 δέχονται μόνο Cardinal, τα 21.0 και 22.0 μόνο THandle ή ULONG_PTR και τα 23.0 και 37.0 δέχονται και τα δύο. Σε release names αυτό σημαίνει Cardinal από XE5 έως 10.3 Rio και THandle από 10.4 Sydney και μετά. Επειδή οι δύο accepting ranges δεν επικαλύπτονται από 12.0 έως 22.0, κανένα unconditional declaration δεν λειτουργεί: ο guard βασίζεται στο CompilerVersion >= 34, δηλαδή στο Sydney, και το call είναι πλήρως qualified ως Winapi.GDIPAPI.GdiplusStartup ώστε η unit resolution order να μην υποκαταστήσει άλλη declaration σε κάποια ενδιάμεση version
function TXLSPageImageExporter.EncodeTiff(Stream: TStream): Integer;
var
StartupInput: TGdiplusStartupInput;
// Ο τύπος var-parameter του GDIPAPI.GdiplusStartup ακολουθεί τη VCL
// generation: Cardinal έως Rio, THandle από 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;
Αυτή είναι η TIFF branch του page image exporter, άρα το blast radius του λάθους είναι ολόκληρη η raster export surface, μαζί με τα paths που περιγράφονται στο exporting cell range ως single image. Σημείωσε επίσης τι δεν ισχυρίζεται ο guard: ULONG_PTR και THandle έχουν το ίδιο width και στις δύο platforms, οπότε η επιλογή αφορά ποιο identifier ονομάζει η declaration και όχι 32-bit έναντι 64-bit correctness
Γιατί το πρώτο probe run δεν ανέφερε τίποτε
Το version probe δεν ανέφερε τίποτε στην πρώτη εκτέλεση επειδή assignments τύπου res=$(...) γίνονταν μέσα σε subshell, όπου δεν μεταδίδονται στον parent. Το dcc32 επιστρέφει 0 σε επιτυχία, άρα το exit code ήταν το σωστό σήμα για capture, και το script το αποθήκευε σε variable που έπαυε να υπάρχει μία γραμμή αργότερα. Κάθε leg επέστρεφε empty και το output έμοιαζε με probe που δεν είχε κάνει compile τίποτε, ακριβώς επειδή αυτό συνέβαινε
Η δεύτερη αποτυχία ήταν χειρότερη επειδή έδωσε λάθος απάντηση αντί για καμία. Το probe ταξινομούσε leg μετρώντας lines που ταίριαζαν στο Error, και το Delphi δεν βάζει αυτή τη λέξη μπροστά σε κάθε fatal. Το F1026 File not found είναι fatal και δεν ταιριάζει, οπότε probe που δεν μπορούσε να επιλύσει καθόλου unit βαθμολογούνταν ως clean pass. Το XE5 δεν διανέμει το Winapi.GDIPOPS.dcu, το πρώτο probe χτύπησε ακριβώς εκεί και πρασίνισε ψευδώς. Ο κανόνας που προέκυψε είναι στενός και αξίζει να ειπωθεί καθαρά: κρίνε compiler probe από το παραγόμενο artifact ή από τη summary line του ίδιου του compiler και ποτέ με grep output για keyword. Το grep stderr για Error είναι heuristic που αποτυγχάνει στη μία κατεύθυνση που δεν αντέχεις, αναφέροντας σιωπηρά επιτυχία
Τι κοστίζει πράγματι η υποστήριξη μιας δεκαετίας compilers
Η ειλικρινής αποτίμηση είναι ότι οι αλλαγές στον code εδώ είναι trivial και οι αλλαγές στη διαδικασία όχι. Τέσσερα από τα πέντε rejections διορθώθηκαν γράφοντας περισσότερο ordinary Pascal και όχι προσθέτοντας version machinery: αφαίρεσε cast, κάνε divide αντί για cast, δώσε type στο nil και δήλωσε accessor class. Μόνο το GdiplusStartup κέρδισε ένα {$IF}. Codebase που εκτείνεται από XE5 έως current release δεν γίνεται thicket conditional defines εκτός αν αφήσεις hard casts και newest-compiler idioms να συσσωρευτούν εξαρχής
Αυτό που πραγματικά κοστίζει είναι build time και discipline. Σαράντα τρία legs είναι αργό script, και ακριβώς γι’ αυτό μετακινήθηκε στο packaging time και τελικά στο ποτέ. Η υπερασπίσιμη μέση λύση είναι να κρατήσεις το γρήγορο four-script loop για iteration και να τρέχεις το full matrix σε schedule που δεν μπορεί να παραλειφθεί, επειδή το failure mode δεν είναι broken build που παρατηρείς, αλλά supported IDE που έπαψε σιωπηρά να υποστηρίζεται δώδεκα releases πριν
Αυτή η υποχρέωση είναι η άλλη πλευρά του να διανέμεις native component. Το HotXLS διαβάζει και γράφει XLS, XLSX και ODS μόνο μέσω Object Pascal, χωρίς εγκατάσταση Excel και χωρίς COM dependency, και αυτό είναι που κάνει εφικτό το Office-free workbook automation σε locked-down server. Η ίδια ιδιότητα σημαίνει ότι ο compiler είναι ολόκληρο το platform contract, άρα κάθε version στο matrix είναι promise που πρέπει να ξαναεπαληθευτεί και όχι να θεωρηθεί δεδομένη
Το cross-compiler build matrix και ο version-safe code που συζητούνται εδώ διατίθενται ως μέρος του HotXLS Delphi Spreadsheet Component, που υποστηρίζει Delphi και C++Builder από XE5 έως την τρέχουσα έκδοση με prebuilt library binaries για κάθε supported IDE