Műszaki cikk

Delphi cross-compiler build matrix: HotXLS XE5-től

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