Техническа статия

Delphi cross-compiler build matrix: HotXLS от XE5

HotXLS доставя един Object Pascal codebase за всяка Delphi и C++Builder release от 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 никога не е бил пускан до completion и XE5 leg-ът е бил счупен през цялото време

Нищо в failure-а не беше subtle, щом беше видян. Пет distinct constructs, които current compiler приема без comment, са hard errors в RAD Studio XE5, което build matrix-ът label-ва като 12.0. Release-ът v2.375.0 поправи и петте и matrix-ът отново светна green на 43 от 43. Следва всяко rejection, защо старият compiler вероятно е прав за двете, които отхвърля по type grounds, и по-неудобната част: probe script-ът, написан да диагностицира mess-а, докладва false pass при първото си изпълнение

Защо XE5 leg-ът е прогнил, без някой да забележи?

XE5 leg-ът е прогнил, защото ежедневната development работа е изпълнявала само 37.0 four-script set, а green local build не казва нищо за compiler, който не сте извикали. Full matrix е отделен slow script, който trial installer-ът извиква, преди Inno Setup да събере files, така че се упражнява при packaging time, а не при commit time. Дванадесет releases се побират в тази празнина

Leg arithmetic-ата си струва да бъде изписана, защото точно там живее coverage illusion-ът. DELPHI_TRIAL_VERSIONS изброява 12.0 до 37.0 и всяка от тези 12 versions се build-ва два пъти, 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 и точно тази грешка е позволила това да се случи

HotXLS е бил ударен от същата форма на проблем от обратната посока. Нов unit, който е reachable през uses clause, но липсва във file list-а на .cbproj, се компилира перфектно под Delphi, защото dcc implicit-но издърпва unlisted units в package и в най-лошия случай emit-ва W1033 hint. C++Builder emit-ва .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 не е legal typecast и compiler-ът казва това с E2089 Invalid typecast

Дразнещият detail е, че variable form е наред. TDateTime(Serial) се компилира във всяка version от matrix-а, защото Serial вече е 8 bytes и cast-ът е size-preserving. Добавете каквото и да е към него и expression-ът се разширява отдолу. Fix-ът не е по-широк cast или conditional define, а да спрете да cast-вате: implicit real-to-real assignment преобразува правилно във всеки compiler, който HotXLS поддържа, и казва какво code-ът реално означава

// Отхвърлено на XE5 (Win32): всяко addition се изчислява като 10-byte
// Extended, а narrowing cast от 10 към 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 вместо това - 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 е несъществуващият Excel 1900-02-29, така че serials под него се нуждаят от extra day, преди DecodeDate да ги види. Portability work никога не трябва тихо да променя такава logic, затова safe edit-ът маха cast-а и оставя arithmetic-ата непроменена

Какво се чупи, когато nil е procedural argument?

Bare nil, подаден там, където се очаква procedural type, fail-ва при overload resolution на старите compilers. Call site-ът в HotXLS е ResolveIndexedColor, който е overloaded и приема callback TXLSTryResolveSystemColor, от който повечето caller-и не се нуждаят. По-новите compilers разрешават nil спрямо procedural parameter-а и избират правилния overload. XE5 не го прави, а diagnostic-ът сочи overload set-а, а не argument-а, което е начин да загубите 20 минути

Portable answer-ът е да дадете type на null callback-а. Unit-level variable от procedural type-а се zero-initialize-ва от language-а, така че вече е nil без initializer и носи type information-а, който старият resolver иска. Там, където unit-level variable би бил overkill, typed local, assigned с nil, върши същата работа

var
  // Nil procedural literal не се bind-ва при older compiler
  // overload resolution; typed, zero-initialized variable се bind-ва
  NilSystemColorResolver: TXLSTryResolveSystemColor;

// ...

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

// Същият fix с 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;

Имайте предвид, че това е реална language-level difference, а не compiler bug, който да заобикаляте с defines. Zero-initialized variable е правилен във всяка version от matrix-а и струва един ред, така че тук изобщо няма conditional compilation. Посегнете към {$IF CompilerVersion} само когато platform наистина се различава между releases, а това в този batch се случва точно веднъж

Protected VCL methods се местят между releases

TPicture.LoadFromStream е public в current VCL и protected в older versions, които HotXLS поддържа, така че direct call се компилира сега и fail-ва тогава. HotXLS го използва, за да валидира, че worksheet background image payload действително се decode-ва, signature check, който се изпълнява, преди HTML exporter-ът да commit-не bytes-те за embedding. Класическият Pascal answer се прилага: декларирайте descendant в същия unit само за да разширите visibility и cast-нете през него на call site-а

type
  // TPicture.LoadFromStream е protected в older VCL versions, които
  // library-то поддържа; same-unit descendant го expose-ва
  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-ът е safe тук, защото descendant добавя no fields и никога не се instantiate-ва; cast-ът само променя какво compiler-ът ще ви позволи да name-нете. Все пак си струва comment на declaration-а, защото reader, който винаги build-ва върху current IDE, иначе ще види pointless type. Background image handling се появява отново в custom VCL grid rendering path-а, където същият decoded payload захранва on-screen sheet-а

Типът на GdiplusStartup token се е променил два пъти

Единственото rejection в batch-а, което действително изисква conditional compilation, е type-ът на var parameter-а на GdiplusStartup, който се е променил между VCL generations по начин, оставящ една-единствена spelling невалидна навсякъде. Version-by-version probing фиксира реалното поведение: legs 12.0 до 20.0 приемат само Cardinal, legs 21.0 и 22.0 приемат само THandle или ULONG_PTR, а 23.0 и 37.0 приемат и двете. С имената на releases това е Cardinal от XE5 до 10.3 Rio и THandle от 10.4 Sydney нататък. Тъй като accepting ranges не се припокриват при 12.0 до 22.0, никоя unconditional declaration не работи: guard-ът се включва при CompilerVersion >= 34, което е Sydney, а call-ът е fully qualified като Winapi.GDIPAPI.GdiplusStartup, така че unit resolution order да не подмени declaration-а с друга при някоя version по средата на range-а

function TXLSPageImageExporter.EncodeTiff(Stream: TStream): Integer;
var
  StartupInput: TGdiplusStartupInput;
  // GDIPAPI GdiplusStartup var-parameter type следва 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, включително path-овете, описани в export на cell range като една image. Имайте предвид и какво guard-ът не твърди: ULONG_PTR и THandle са с еднаква width на двете platforms, така че изборът е за това кое identifier носи declaration-ът, а не за 32-bit срещу 64-bit correctness

Защо първият probe run не докладва нищо?

Version probe-ът не е докладвал нищо при първото изпълнение, защото assignments от вида res=$(...) са се правили вътре в subshell, където не се propagate-ват към parent. dcc32 излиза с 0 при success, така че exit code е бил правилният signal за capture, а script-ът го е записвал в variable, която престава да съществува един ред по-късно. Всеки leg се връщал празен и output-ът изглеждал като probe, който не е compile-нал нищо, което точно е бил

Вторият failure е бил по-лош, защото е дал грешен answer, а не никакъв. Probe-ът е класифицирал leg чрез броене на lines, съвпадащи с Error, а Delphi не prefix-ва всяка fatal с тази дума. F1026 File not found е fatal и не match-ва, така че probe, който изобщо не е могъл да resolve-не unit, е бил оценен като clean pass. XE5 не доставя Winapi.GDIPOPS.dcu, първият probe е ударил точно това и е светнал falsely green. Изводът е тесен и си струва да бъде казан ясно: преценявайте compiler probe по произведения artifact или по собствената summary line на compiler-а, никога чрез grep на output за keyword. Grep-ването на stderr за Error е heuristic, която fail-ва в посоката, която най-малко можете да си позволите, като тихо докладва success

Каква е реалната цена за поддръжка на десетилетие compilers?

Честната сметка е, че code changes тук са trivial, а process changes не са. Четири от петте rejections са поправени чрез по-обикновен Pascal, а не с version machinery: махнете cast, divide вместо cast, дайте type на nil, declare-нете accessor class. Само GdiplusStartup е заслужил {$IF}. Codebase, който обхваща XE5 до current release, не се превръща в thicket от conditional defines, освен ако не позволите hard casts и newest-compiler idioms да се натрупат на първо място

Истинската цена е build time и discipline. 43 legs е slow script, точно затова е drift-нал към packaging time и после към never. Защитимият middle ground е да пазите fast four-script loop за iteration и да пускате full matrix по schedule, който не може да бъде пропуснат, защото failure mode-ът не е broken build, който забелязвате, а supported IDE, който тихо е спрял да бъде supported преди 12 releases

Това задължение е обратната страна на shipping native component изобщо. HotXLS чете и записва XLS, XLSX и ODS само чрез Object Pascal, без Excel install и без COM dependency, което прави Office-free workbook automation възможна на locked-down server. Същото свойство означава, че compiler-ът е целият platform contract, така че всяка version в matrix-а е promise, който трябва да бъде re-verified, а не приет на доверие

Cross-compiler build matrix-ът и version-safe code-ът, обсъдени тук, са част от HotXLS Delphi Spreadsheet Component, който поддържа Delphi и C++Builder от XE5 до current release с prebuilt library binaries за всеки supported IDE