Teknisk artikel

HotXLS byggmatris över Delphi-kompilatorer sedan XE5

HotXLS levererar en Object Pascal-kodbas till varje Delphi- och C++Builder-version från XE5 och framåt, och build-All-Lib-TRIAL.cmd är skriptet som bevisar det: 43 byggsteg som täcker 12 Delphi-versioner på Win32 och Win64 samt 10 C++Builder Win32- och 9 Win64-paketbyggen. Från v2.363 till v2.374 kördes skriptet aldrig till slut, och XE5-steget var trasigt hela tiden

Ingenting med felet var subtilt när det väl syntes. Fem distinkta konstruktioner som den aktuella kompilatorn accepterar utan kommentar är hårda fel i RAD Studio XE5, som byggmatrisen märker 12.0. v2.375.0 rättade alla fem och matrisen blev grön igen med 43 av 43. Här följer varje avvisning, varför den äldre kompilatorn kan sägas ha rätt om de två som avvisar på typgrunder och den pinsammare delen: probskriptet som skrevs för att diagnostisera röran rapporterade ett falskt godkänt på sin första körning

Varför förföll XE5-steget utan att någon märkte det?

XE5-steget förföll eftersom den dagliga utvecklingen bara körde uppsättningen med fyra skript för 37.0, och en grön lokal byggning säger ingenting om en kompilator du inte anropade. Hela matrisen är ett separat, långsamt skript som provinstallationsprogrammet anropar innan Inno Setup samlar filer, så det körs vid paketering i stället för vid commit. Tolv versioner fick plats i den luckan

Stegaritmetiken är värd att skriva ut eftersom det är där täckningsillusionen bor. DELPHI_TRIAL_VERSIONS räknar upp 12.0 till 37.0 och var och en av de 12 versionerna byggs två gånger, Win32 och Win64. CB_TRIAL_WIN32_VERSIONS listar 10 versioner och CB_TRIAL_WIN64_VERSIONS bara 9, eftersom XE5 har ett C++Builder-paketprojekt men inte levererar startobjektet c0pkg64.o för Win64-paket. Tolv plus tolv plus tio plus nio är 43. Att köra fyra av dem och kalla kodbasen portabel är ett kategorifel, och det är det specifika felet som lät detta hända

HotXLS har träffats av samma problemform från motsatt håll. En ny enhet som nås genom en uses-sats men saknas i fillistan för .cbproj kompileras perfekt under Delphi, eftersom dcc implicit drar in enheter som inte finns listade i paketet och i värsta fall avger en W1033-hint. C++Builder avger en .obj endast för enheter som namnges i <DelphiCompile>, så samma kod dör i ilink-steget med en olöst extern symbol. Den ena verktygskedjan döljer vad den andra fångar. Det är hela argumentet för att köra matrisen i stället för att lita på en representativ kompilator

Hårda typkast som de gamla Win32-kompilatorerna avvisar

Två av de fem avvisningarna är samma fel i olika kläder: ett hårt typkast tillämpat på ett flyttalsuttryck i stället för på en variabel. På Win32 utvärderar de äldre kompilatorerna aritmetik genom x87-stacken, så en addition som involverar en Double hålls med 80-bitars extra precision och dess statiska typ blir den 10 byte stora Extended. Att kasta 10 byte ner till en 8 byte stor TDateTime är inte ett lagligt typkast, och kompilatorn säger det med E2089 Invalid typecast

Den irriterande detaljen är att variabelformen fungerar. TDateTime(Serial) kompileras i alla versioner i matrisen eftersom Serial redan är 8 byte och kastet bevarar storleken. Lägg till vad som helst och uttrycket breddas under dig. Fixen är inte ett bredare kast eller en villkorlig define, utan att sluta kasta: en implicit real-till-real-tilldelning omvandlar korrekt i varje kompilator som HotXLS stöder och säger vad koden faktiskt betyder

// Avvisas på XE5 (Win32): varje addition utvärderas som en 10-byte
// Extended, och kastet från 10 till 8 byte ger E2089
if Dates1904 then
  Value := TDateTime(Serial + XLSDate1904Offset)
else if Serial < 60 then
  Value := TDateTime(Serial + 1)
else
  Value := TDateTime(Serial);        // detta accepteras: ingen addition

// Versionssäkert: låt real-till-real-tilldelningen göra omvandlingen
if Dates1904 then
  Value := Serial + XLSDate1904Offset
else if Serial < 60 then
  Value := Serial + 1
else
  Value := Serial;

// Samma typ av avvisning i packaren för cellvärden: ett hårt Double-kast
// av ett heltal. Dividera i stället - operatorn ger redan ett reellt värde
if (Scaled = intVal) and (Double(intVal) / 100 = AValue) then   // E2089
  ;
if (Scaled = intVal) and (intVal / 100 = AValue) then           // portabelt
  ;

Grenen Serial < 60 är skottårsfiktionen från 1900, inte ett off-by-one-fel: serienummer 60 är Excels icke-existerande 1900-02-29, så serienummer under det behöver den extra dagen innan DecodeDate ser dem. Portabilitetsarbete bör aldrig tyst ändra sådan logik, vilket är exakt varför den säkra redigeringen tar bort kastet och lämnar aritmetiken orörd

Vad går sönder när nil är ett procedurargument?

Ett naket nil som skickas där en procedurtyp förväntas misslyckas med att bindas under överlagringsupplösningen i de äldre kompilatorerna. Anropsstället i HotXLS är ResolveIndexedColor, som är överlagrad och tar en callback av typen TXLSTryResolveSystemColor som de flesta anropare inte behöver. Nyare kompilatorer löser nil mot procedurparametern och väljer rätt överlagring. XE5 gör inte det, och diagnostiken pekar på överlagringsmängden i stället för på argumentet, vilket är hur man förlorar tjugo minuter

Det portabla svaret är att ge null-callbacken en typ. En variabel på enhetsnivå av procedurtypen nollinitieras av språket, så den är redan nil utan initialiserare och bär den typinformation som den gamla resolvern vill ha. Där en variabel på enhetsnivå skulle vara överdriven gör en typad lokal som tilldelas nil samma jobb

var
  // En procedurliteral med nil binds inte i de äldres
  // överlagringsupplösning; en typad, nollinitierad variabel gör det
  NilSystemColorResolver: TXLSTryResolveSystemColor;

// ...

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

// Samma fix med en typad lokal i XLSX-arbetsboken
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;

Lägg märke till att detta är en verklig skillnad på språknivå och inte ett kompilatorfel som är värt att kringgå med defines. Den nollinitierade variabeln är korrekt i alla versioner i matrisen och kostar en rad, så ingen villkorlig kompilering behövs här. Använd {$IF CompilerVersion} först när plattformen verkligen skiljer sig mellan versionerna, vilket händer exakt en gång i denna batch

Skyddade VCL-metoder flyttar sig mellan versioner

TPicture.LoadFromStream är public i aktuell VCL och protected i de äldre versioner som HotXLS stöder, så ett direkt anrop kompileras nu och misslyckas då. HotXLS använder det för att validera att en nyttolast för en bakgrundsbild i ett kalkylblad verkligen avkodas, en signaturkontroll som körs innan HTML-exportören åtar sig att bädda in bytena. Det klassiska Pascal-svaret gäller: deklarera en ättling i samma enhet enbart för att bredda synligheten och kasta genom den vid anropsstället

type
  // TPicture.LoadFromStream är protected i de äldre VCL-versioner
  // som biblioteket stöder; en ättling i samma enhet visar upp den
  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-klassknepet är säkert här eftersom ättlingen inte lägger till några fält och aldrig instansieras; kastet ändrar bara vad kompilatorn låter dig namnge. Det är ändå värt en kommentar vid deklarationen, eftersom en läsare som bara bygger på en aktuell IDE annars ser en meningslös typ. Hanteringen av bakgrundsbilder dyker upp igen i den anpassade VCL-rutnätsvägen för rendering, där samma avkodade nyttolast matar kalkylbladet på skärmen

GdiplusStartup-tokenstypen ändrades två gånger

Den enda avvisningen i batchen som verkligen kräver villkorlig kompilering är var-parametertypen till GdiplusStartup, som ändrades mellan VCL-generationerna på ett sätt som lämnar ingen gemensam stavning giltig överallt. Version-för-version-probning fastställde det faktiska beteendet: stegen 12.0 till 20.0 accepterar endast Cardinal, stegen 21.0 och 22.0 accepterar endast THandle eller ULONG_PTR, och 23.0 och 37.0 accepterar båda. Med versionsnamn betyder det Cardinal från XE5 till och med 10.3 Rio och THandle från 10.4 Sydney och framåt. Eftersom de två accepterande intervallen inte överlappar för 12.0 till 22.0 fungerar ingen ovillkorlig deklaration: vakten använder CompilerVersion >= 34, vilket är Sydney, och anropet är fullt kvalificerat som Winapi.GDIPAPI.GdiplusStartup så att enhetsupplösningens ordning inte kan ersätta deklarationen i någon mellanversion

function TXLSPageImageExporter.EncodeTiff(Stream: TStream): Integer;
var
  StartupInput: TGdiplusStartupInput;
  // GDIPAPI:s GdiplusStartup-var-parameter följer VCL-generationen:
  // Cardinal till och med Rio, THandle från Sydney och framåt
  {$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;

Detta är TIFF-grenen i exportören för sidbilder, så spridningsradien är hela den rasterexportyta som också omfattar vägarna som beskrivs i att exportera ett cellintervall som en enda bild. Lägg också märke till vad vakten inte påstår: ULONG_PTR och THandle har samma bredd på båda plattformarna, så valet gäller vilket namn deklarationen använder, inte korrekthet mellan 32 och 64 bitar

Varför rapporterade den första probkörningen ingenting?

Versionsproben rapporterade ingenting vid den första körningen eftersom tilldelningar av typen res=$(...) gjordes i en subshell, där de inte förs över till föräldern. dcc32 avslutar med 0 vid framgång, så avslutskoden var den rätta signalen att fånga, och skriptet fångade den i en variabel som upphörde att existera en rad senare. Varje steg kom tillbaka tomt och utdata såg ut som från en prob som inte hade kompilerat något, vilket exakt var vad det var

Det andra felet var värre, eftersom det gav ett felaktigt svar i stället för inget svar. Proben klassificerade ett steg genom att räkna rader som matchade Error, och Delphi prefixar inte varje fatalt fel med det ordet. F1026 File not found är fatalt och matchar inte, så en prob som inte alls kunde lösa en enhet fick ett rent godkänt. XE5 levererar inte Winapi.GDIPOPS.dcu, den första proben träffade exakt det och blev falskt grön. Regeln som kom ur det är smal och värd att säga tydligt: bedöm en kompilatorprob efter den producerade artefakten eller efter kompilatorns egen summeringsrad, aldrig genom att grep:a dess utdata efter ett nyckelord. Att grep:a stderr efter Error är en heuristik som misslyckas i den riktning du minst har råd med och tyst rapporterar framgång

Vad kostar det egentligen att stödja ett decennium av kompilatorer?

Den ärliga redovisningen är att kodändringarna här är triviala och processändringarna inte är det. Fyra av de fem avvisningarna rättades genom att skriva vanligare Pascal, inte genom att lägga till versionsmaskineri: ta bort ett kast, dividera i stället för att kasta, ge nil en typ och deklarera en accessorklass. Endast GdiplusStartup förtjänade en {$IF}. En kodbas som spänner från XE5 till aktuell version blir inte ett snår av villkorliga defines om du inte låter hårda kast och idiom från den nyaste kompilatorn samlas i första hand

Det som faktiskt kostar är byggtid och disciplin. Fyrtiotre steg är ett långsamt skript, vilket är exakt varför det drev mot paketeringstid och sedan aldrig kördes. Den försvarbara mitten är att behålla den snabba loopen med fyra skript för iteration och köra hela matrisen enligt ett schema som inte kan hoppas över, eftersom felläget inte är en trasig byggning som du märker utan en stödd IDE som tyst slutade vara stödd för tolv versioner sedan

Den skyldigheten är baksidan av att över huvud taget leverera en inbyggd komponent. HotXLS läser och skriver XLS, XLSX och ODS med enbart Object Pascal, utan Excel-installation och utan COM-beroende, vilket är det som gör arbetsboksautomation utan Office möjlig på en låst server. Samma egenskap betyder att kompilatorn är hela plattformskontraktet, så varje version i matrisen är ett löfte som måste verifieras på nytt i stället för att antas

Byggmatrisen över kompilatorer och den versionssäkra koden som diskuteras här levereras som en del av HotXLS Delphi Spreadsheet Component, som stöder Delphi och C++Builder från XE5 till aktuell version med förbyggda biblioteksbinärer för varje stödd IDE