A HotXLS egyetlen Object Pascal kódbázist szállít minden Delphi és C++Builder kiadáshoz XE5-től felfelé, és a build-All-Lib-TRIAL.cmd az a script, amely ezt bizonyítja: 43 build-leg, 12 Delphi-verzió Win32 és Win64 alatt, továbbá 10 C++Builder Win32 és 9 Win64 package-build. A v2.363 és v2.374 között ezt a scriptet soha nem futtatták végig, az XE5 leg pedig végig hibás volt
Miután láttuk, a hiba semmilyen szempontból nem volt finom. Öt külön konstrukció, amelyet a jelenlegi compiler megjegyzés nélkül elfogad, kemény hiba RAD Studio XE5 alatt, amelyet a build matrix 12.0-ként jelöl. A v2.375.0 mind az ötöt javította, és a matrix ismét zöld lett 43-ból 43-nál. Az alábbiakban sorra jön mind az öt elutasítás, miért van az öreg compilernek vitathatatlanul igaza abból a kettőből, amelyeket típusok miatt utasít el, és a kínosabb rész: a diagnosztizálásra írt probe script első futásakor hamis zöldet jelentett
Miért rohadt el az XE5 leg úgy, hogy senki nem vette észre?
Az XE5 leg azért rohadt el, mert a mindennapi fejlesztés csak a 37.0-s négy-scriptes készletet futtatta, egy zöld lokális build pedig semmit nem mond egy olyan compilerről, amelyet nem hívtál meg. A teljes matrix külön, lassú script, amelyet a trial installer hív meg, mielőtt az Inno Setup összeszedi a fájlokat, ezért packaging közben fut, nem commitkor. Tizenkét kiadás fért be ebbe a résbe
A leg-számtant érdemes kibontani, mert itt él a lefedettségi illúzió. A DELPHI_TRIAL_VERSIONS a 12.0–37.0 tartományt sorolja fel, és mind a 12 verzió kétszer fordul, Win32 és Win64 alatt. A CB_TRIAL_WIN32_VERSIONS 10 verziót tartalmaz, a CB_TRIAL_WIN64_VERSIONS csak 9-et, mert az XE5-nek van C++Builder package-projektje, de nem szállít Win64 package startup objectet, a c0pkg64.o-t. Tizenkettő plusz tizenkettő plusz tíz plusz kilenc az 43. Négyet futtatni, majd portable-nak nevezni a kódbázist kategóriahiba, és pontosan ez engedte megtörténni a bajt
A HotXLS ugyanennek a problémának a fordított oldalába is belefutott. Egy olyan új unit, amelyet egy uses záradék elér, de hiányzik a .cbproj fájllistájából, Delphi alatt tökéletesen fordul, mert a dcc implicit módon behúzza a csomagba a fel nem sorolt unitokat, legfeljebb W1033 hintet ad. A C++Builder csak a <DelphiCompile>-ban név szerint szereplő unitokhoz készít .obj-t, ezért ugyanaz a kód az ilink fázisban unresolved externallel meghal. Az egyik toolchain elrejti azt, amit a másik elkap. Ez az egész érv a matrix futtatása mellett, a representative compilerben való bizalommal szemben
A régi Win32-compilerek által elutasított hard type castok
Az öt elutasításból kettő ugyanaz a bug, csak más ruhában: lebegőpontos kifejezésre, nem változóra alkalmazott hard type cast. Win32 alatt a régi compilerek az aritmetikát az x87 stacken végzik, ezért egy Double-t érintő összeadás 80 bites extra pontossággal fut, statikus típusa pedig a 10 bájtos Extended lesz. A 10 bájtról 8 bájtos TDateTime-re szűkítő cast nem legális typecast, a compiler pedig E2089 Invalid typecast hibát mond
A dühítő részlet, hogy a változós forma jó. A TDateTime(Serial) minden matrixverzión lefordul, mert a Serial már 8 bájtos, a cast pedig méretet tart. Adj hozzá bármit, és a kifejezés alattad kiszélesedik. A javítás nem egy szélesebb cast vagy feltételes define, hanem a cast elhagyása: az implicit real-to-real hozzárendelés minden támogatott compileren helyesen konvertál, és pontosan azt mondja, amit a kód jelent
// XE5 (Win32) alatt elutasítva: minden összeadás 10 bájtos
// Extendedként fut, a 10-ről 8 bájtra szűkítő cast E2089-et ad
if Dates1904 then
Value := TDateTime(Serial + XLSDate1904Offset)
else if Serial < 60 then
Value := TDateTime(Serial + 1)
else
Value := TDateTime(Serial); // ezt elfogadja: nincs összeadás
// Verzióbiztos: a real-to-real hozzárendelés végezze a konverziót
if Dates1904 then
Value := Serial + XLSDate1904Offset
else if Serial < 60 then
Value := Serial + 1
else
Value := Serial;
// Ugyanez az elutasítás a cellaérték-csomagolóban: hard Double cast
// egy integeren. Inkább osszunk - az operátor már realt ad
if (Scaled = intVal) and (Double(intVal) / 100 = AValue) then // E2089
;
if (Scaled = intVal) and (intVal / 100 = AValue) then // portable
;
A Serial < 60 ág az 1900-as szökőév-kitaláció, nem off-by-one: a 60-as serial az Excel nem létező 1900-02-29 napja, ezért a nála kisebb serialokhoz kell hozzáadni a plusz napot, mielőtt a DecodeDate látná. A hordozhatósági munka soha ne változtasson csendben ilyen logikán, ezért távolítja el a biztonságos szerkesztés a castot, az aritmetikát érintetlenül hagyva
Mi törik el, amikor a nil procedurális argumentum?
Egy csupasz nil ott, ahol procedurális típust vár a compiler, nem kötődik az overload resolution során a régi compilereken. A HotXLS hívási helye a ResolveIndexedColor, amely overloadolt, és egy TXLSTryResolveSystemColor callbacket kér, amire a legtöbb hívónak nincs szüksége. Az újabb compilerek a nil-t a procedurális paraméterhez oldják, és kiválasztják a helyes overloadot. Az XE5 nem, a diagnosztika pedig az overload-készletre mutat, nem az argumentumra, így könnyű húsz percet elveszíteni
A hordozható válasz, hogy típust adsz a null callbacknek. A procedurális típusú unit-szintű változót a nyelv nulláról inicializálja, így initializer nélkül is nil, miközben hordozza azt a típusinformációt, amelyre a régi resolvernek szüksége van. Ha a unit-szintű változó túlzás, egy nil-re állított típusos local ugyanazt a célt szolgálja
var
// A nil procedurális literál nem kötődik a régi compilerek
// overload resolutionjában; a típusos, nulláról inicializált változó igen
NilSystemColorResolver: TXLSTryResolveSystemColor;
// ...
FWorkbook.ResolveIndexedColor(AIndexedColor, xicsBiffIcv, ARole,
NilSystemColorResolver, Resolution);
// Ugyanez a javítás típusos local változóval az XLSX workbookban
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;
Ez valódi nyelvi különbség, nem olyan compilerbug, amelyet define-okkal kellene megkerülni. A nulláról inicializált változó minden matrixverzión helyes, egyetlen sor az ára, ezért itt nincs feltételes fordítás. A {$IF CompilerVersion} csak akkor kerüljön elő, amikor a platform tényleg eltér kiadásonként, ebben a batchben pontosan egyszer
A protected VCL-metódusok kiadásról kiadásra mozognak
A TPicture.LoadFromStream a jelenlegi VCL-ben public, a HotXLS által támogatott régi verziókban viszont protected, ezért a közvetlen hívás most lefordul, akkor pedig elbukik. A HotXLS arra használja, hogy ellenőrizze, valóban dekódolódik-e a worksheet-háttérkép payloadja, még mielőtt a HTML-exporter commitálná a byte-ok beágyazását. A klasszikus Pascal-válasz érvényes: deklarálj ugyanabban a unitban egy leszármazottat pusztán a láthatóság kibővítéséhez, majd azon keresztül castolj a hívási helyen
type
// A TPicture.LoadFromStream protected a régebbi VCL-verziókon, amelyeket
// a library támogat; az azonos unitbeli leszármazott láthatóvá teszi
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);
Az accessor-class trükk itt biztonságos, mert a leszármazott nem ad mezőt, és soha nem példányosítjuk; a cast csak azt változtatja meg, mit enged a compiler megnevezni. A deklaráció mellé mégis érdemes kommentet írni, mert aki csak jelenlegi IDE-n buildel, különben értelmetlen típusnak látja. A háttérképfeldolgozás a custom VCL grid renderelési útvonalán ismét előjön, ahol ugyanaz a dekódolt payload táplálja a képernyőn megjelenő sheetet
A GdiplusStartup token típusa kétszer is megváltozott
Az egyetlen olyan elutasítás a batchben, amely tényleg feltételes fordítást kér, a GdiplusStartup var paraméterének típusa, amely VCL-generációk között úgy változott, hogy nincs egyetlen mindenhol érvényes írásmód. A verziónkénti probe rögzítette a tényleges viselkedést: a 12.0–20.0 legek csak Cardinal-t fogadnak el, a 21.0 és 22.0 legek csak THandle-t vagy ULONG_PTR-t, a 23.0 és 37.0 pedig mindkettőt. Kiadásnevekkel ez Cardinal az XE5-től 10.3 Rioig, THandle pedig 10.4 Sydney-től. Mivel a 12.0–22.0 tartomány két elfogadási halmaza nem fedi egymást, feltétel nélküli deklaráció nem működik: a guard a Sydneyt jelentő CompilerVersion >= 34-re épül, a hívás pedig teljesen minősített Winapi.GDIPAPI.GdiplusStartup, így az unitfeloldási sorrend nem cserélhet be más deklarációt a köztes verziók egyikén
function TXLSPageImageExporter.EncodeTiff(Stream: TStream): Integer;
var
StartupInput: TGdiplusStartupInput;
// A GDIPAPI GdiplusStartup var-paraméterének típusa követi a VCL
// generációját: Rioig Cardinal, Sydneytől THandle
{$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;
Ez a page image exporter TIFF-ága, ezért a rossz döntés hatóköre az egész raszterexport-felület, beleértve a cellatartomány egyetlen képként való exportját is. A guard viszont nem állítja, hogy az ULONG_PTR és a THandle platformonként eltérő szélességű lenne: mindkettő ugyanakkora, a választás arról szól, melyik azonosítót nevezi meg a deklaráció, nem a 32 vagy 64 bit helyességéről
Miért nem jelentett semmit az első probe-futtatás?
A verzióprobe első futása azért nem jelentett semmit, mert a res=$(...) hozzárendelések subshellben történtek, ahonnan nem propagálódtak a szülőbe. A dcc32 siker esetén 0-val tér vissza, ezért a kilépési kód volt a helyes jel, a script pedig egy olyan változóba mentette, amely egy sorral később már nem létezett. Minden leg üresen jött vissza, a kimenet pedig úgy nézett ki, mint egy probe, amely semmit sem fordított le, ami pontosan így is volt
A második hiba rosszabb volt, mert nulla válasz helyett rossz választ adott. A probe úgy osztályozta a leget, hogy megszámolta az Error szót tartalmazó sorokat, Delphi viszont nem minden fatal üzenetet ezzel a szóval prefixál. Az F1026 File not found fatal, de nem illeszkedik, ezért egy olyan probe, amely egyáltalán nem tudott unitot feloldani, tiszta passnak számított. Az XE5 nem szállít Winapi.GDIPOPS.dcu-t, az első probe pontosan ebbe ütközött, mégis hamis zöldet adott. A belőle születő szabály szűk, és érdemes világosan kimondani: egy compiler-probe-ot a létrejött artifact vagy a compiler saját összegző sora alapján ítélj meg, soha ne a kimenet egy kulcsszóra való greppelésével. Az stderr Error szóra greppelése olyan heurisztika, amely abban az irányban hibázik, amelyet a legkevésbé engedhetsz meg magadnak, és csendben sikert jelent
Valójában mennyibe kerül egy évtizednyi compiler támogatása?
Az őszinte elszámolás szerint az itteni kódváltozások triviálisak, a folyamatváltozások viszont nem. Az öt elutasításból négyet hétköznapibb Pascal megírásával javítottunk, nem verziómachinériával: cast elhagyása, cast helyetti osztás, a nil típusának megadása, accessor class deklarálása. Csak a GdiplusStartup érdemelt {$IF}-et. Egy XE5-től a jelenlegi kiadásig terjedő kódbázis csak akkor válik feltételes define-ok sűrűjévé, ha hagyod, hogy a hard castok és a legújabb compiler-idiomák először felhalmozódjanak
A valódi ár buildidő és fegyelem. A 43 leg lassú script, éppen ezért csúszott packaging-időre, majd a soha-futtatott állapotba. A védhető középút az, hogy az iterációhoz megtartod a gyors négy-scriptes kört, a teljes matrixot pedig olyan ütemezéssel futtatod, amelyet nem lehet kihagyni, mert a hibamód nem egy észrevehető broken build, hanem egy támogatott IDE, amely csendben tizenkét kiadással korábban abbahagyta a támogatottságot
Ez a kötelezettség a natív komponens szállításának másik oldala. A HotXLS csak Object Pascalon keresztül olvas és ír XLS, XLSX és ODS fájlokat, telepített Excel és COM-függőség nélkül, ez teszi lehetővé a Office-mentes workbook-automatizálást lezárt szerveren. Ugyanez a tulajdonság azt jelenti, hogy a compiler az egész platform-szerződés, ezért a matrix minden verziója újraellenőrizendő ígéret, nem feltételezés
Az itt tárgyalt cross-compiler build matrix és verzióbiztos kód a HotXLS Delphi Spreadsheet Component része, amely XE5-től a jelenlegi kiadásig támogatja a Delphit és a C++Buildert, minden támogatott IDE-hez előre buildelt library binárisokkal