Teknisk artikkel

Delphi krysskompilator-matrise: HotXLS siden XE5

HotXLS leverer én Object Pascal-kodebase til hver Delphi- og C++Builder-release fra XE5 og fremover, og build-All-Lib-TRIAL.cmd er skriptet som beviser det: 43 byggben, som dekker 12 Delphi-versjoner på Win32 og Win64 pluss 10 C++Builder Win32- og 9 Win64-package-bygg. Fra v2.363 til v2.374 ble skriptet aldri kjørt ferdig, og XE5-benet var ødelagt hele tiden

Ingenting ved feilen var subtilt etter at den ble sett. Fem forskjellige konstruksjoner som den nåværende kompilatoren godtar uten kommentar, er harde feil på RAD Studio XE5, som byggmatrisen merker 12.0. v2.375.0-releasen rettet alle fem, og matrisen ble grønn igjen med 43 av 43. Det som følger er hver avvisning, hvorfor den gamle kompilatoren med god grunn har rett om de to som avvises på typegrunnlag, og den mer pinlige delen: Probe-skriptet som ble skrevet for å diagnostisere rotet, rapporterte en falsk bestått på første kjøring

Hvorfor råtnet XE5-benet uten at noen merket det?

XE5-benet råtnet fordi den daglige utviklingen bare kjørte fire-skriptsettet for 37.0, og et grønt lokalt bygg sier ingenting om en kompilator du ikke har startet. Den fulle matrisen er et separat og langsomt skript som prøveinstallasjonsprogrammet kaller før Inno Setup samler filer, så det utøves ved pakking i stedet for ved commit. Tolv releaser fikk plass i det gapet

Ben-aritmetikken er verdt å skrive ut, fordi det er her dekningsillusjonen bor. DELPHI_TRIAL_VERSIONS lister 12.0 til 37.0, og hver av de 12 versjonene bygger to ganger, Win32 og Win64. CB_TRIAL_WIN32_VERSIONS lister 10 versjoner, og CB_TRIAL_WIN64_VERSIONS bare 9, fordi XE5 har et C++Builder-package-prosjekt, men ikke leverer Win64-package-startobjektet c0pkg64.o. Tolv pluss tolv pluss ti pluss ni er 43. Å kjøre fire av dem og kalle kodebasen portabel er en kategorifeil, og det er den konkrete feilen som lot dette skje

HotXLS har blitt rammet av samme problemform fra motsatt retning. En ny enhet som nås gjennom en uses-klausul, men mangler fra .cbproj-fillisten, kompileres perfekt under Delphi, fordi dcc implisitt trekker ulistede enheter inn i pakken og på det verste skriver et W1033-hint. C++Builder skriver bare en .obj for enheter som er navngitt i <DelphiCompile>, så den samme koden dør på ilink-stadiet med en unresolved external. Én toolchain skjuler det den andre fanger. Det er hele argumentet for å kjøre matrisen i stedet for å stole på en representativ kompilator

Harde typecasts som de gamle Win32-kompilatorene avviser

To av de fem avvisningene er samme feil i forskjellige klær: En hard typecast er lagt på et flyttallsuttrykk i stedet for på en variabel. På Win32 evaluerer de eldre kompilatorene aritmetikk gjennom x87-stacken, så en addisjon som involverer en Double, holdes med 80-bits overskudspresisjon og får den statiske typen 10-byte Extended. Å caste 10 byte ned til en 8-byte TDateTime er ikke en lovlig typecast, og kompilatoren sier det med E2089 Invalid typecast

Den irriterende detaljen er at variabelformen er fin. TDateTime(Serial) kompileres på alle versjoner i matrisen, fordi Serial allerede er 8 byte og casten bevarer størrelsen. Legg hva som helst til den, så utvides uttrykket under deg. Løsningen er ikke en bredere cast eller en conditional define, men å slutte å caste: En implisitt real-til-real-tildeling konverterer riktig på alle kompilatorer HotXLS støtter, og den sier hva koden faktisk betyr

// Avvist på XE5 (Win32): hver addisjon evalueres som en 10-byte
// Extended, og 10-til-8-narrowing-casten utløser E2089
if Dates1904 then
  Value := TDateTime(Serial + XLSDate1904Offset)
else if Serial < 60 then
  Value := TDateTime(Serial + 1)
else
  Value := TDateTime(Serial);        // denne godtas: ingen addisjon

// Versjonssikker: la real-til-real-tildelingen gjøre konverteringen
if Dates1904 then
  Value := Serial + XLSDate1904Offset
else if Serial < 60 then
  Value := Serial + 1
else
  Value := Serial;

// Samme type av avvisning i celleverdi-pakkeren: en hard Double-cast
// av et heltall. Del i stedet - operatoren gir allerede en reell verdi
if (Scaled = intVal) and (Double(intVal) / 100 = AValue) then   // E2089
  ;
if (Scaled = intVal) and (intVal / 100 = AValue) then           // portabel
  ;

Serial < 60-grenen er 1900-skuddårsfiksjonen, ikke en off-by-one: Serienummer 60 er Excels ikke-eksisterende 1900-02-29, så serienumre under det trenger den ekstra dagen før DecodeDate ser dem. Portabilitetsarbeid bør aldri stille endre slik logikk, og det er nettopp derfor den sikre endringen fjerner casten og lar aritmetikken være urørt

Hva bryter når nil er et prosedyreargument?

En naken nil sendt dit en prosedyretype forventes, feiler under overload-oppløsning på de eldre kompilatorene. Kallstedet i HotXLS er ResolveIndexedColor, som er overloaded og tar en TXLSTryResolveSystemColor-callback de fleste callers ikke trenger. Nyere kompilatorer løser nil mot prosedyreparameteren og velger riktig overload. XE5 gjør ikke det, og diagnosen peker på overload-settet i stedet for argumentet, noe som er slik du mister tjue minutter

Det portable svaret er å gi null-callbacken en type. En variabel på enhetsnivå av prosedyretypen nullinitialiseres av språket, så den er allerede nil uten initialiserer og bærer typeinformasjonen den gamle resolveren trenger. Der en variabel på enhetsnivå ville vært overkill, gjør en typet lokal som tildeles nil samme jobb

var
  // En naken nil-prosedyraliteral bindes ikke i den eldre kompilatorens
  // overload-oppløsning; en typet, nullinitialisert variabel gjør det
  NilSystemColorResolver: TXLSTryResolveSystemColor;

// ...

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

// Samme retting med en typet lokal i XLSX-arbeidsboken
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;

Merk at dette er en reell forskjell på språknivå, ikke en kompilatorfeil det er verdt å omgå med defines. Den nullinitialiserte variabelen er korrekt på hver versjon i matrisen og koster én linje, så det finnes ingen conditional compilation her i det hele tatt. Bruk {$IF CompilerVersion} bare når plattformen faktisk skiller seg mellom releaser, noe som skjer nøyaktig én gang i denne batchen

Beskyttede VCL-metoder flytter seg mellom releaser

TPicture.LoadFromStream er public i dagens VCL og protected i de eldre versjonene HotXLS støtter, så et direkte kall kompileres nå og feiler da. HotXLS bruker det til å validere at en worksheet-bakgrunnsbildelast virkelig dekoderes, en signaturkontroll som kjører før HTML-eksportøren committer på å bygge inn byte-ene. Det klassiske Pascal-svaret gjelder: Deklarer en avleder i samme enhet utelukkende for å utvide synligheten, og cast gjennom den på kallstedet

type
  // TPicture.LoadFromStream er protected på de eldre VCL-versjonene
  // biblioteket støtter; en avleder i samme enhet eksponerer 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-class-trikset er trygt her fordi avlederen ikke legger til felt og aldri instansieres; casten endrer bare hva kompilatoren lar deg navngi. Det er likevel verdt en kommentar ved deklarasjonen, fordi en leser som bare bygger på en aktuell IDE ellers vil se en meningsløs type. Håndtering av bakgrunnsbilder dukker opp igjen i renderingsstien for det tilpassede VCL-rutenettet, der den samme dekodede payloaden mater arket på skjermen

GdiplusStartup-token-typen endret seg to ganger

Den eneste avvisningen i batchen som faktisk krever conditional compilation, er var-parameterens type i GdiplusStartup, som endret seg på tvers av VCL-generasjoner på en måte som gjør at ingen enkelt skrivemåte er gyldig overalt. Versjon-for-versjon-probing fastslo den faktiske oppførselen: Benene 12.0 til 20.0 aksepterer bare Cardinal, benene 21.0 og 22.0 aksepterer bare THandle eller ULONG_PTR, og 23.0 og 37.0 aksepterer begge. Med release-navn er det Cardinal fra XE5 til og med 10.3 Rio og THandle fra 10.4 Sydney og fremover. Fordi de to aksepterende områdene ikke overlapper for 12.0 til 22.0, fungerer ingen ubetinget deklarasjon: Vakten bruker CompilerVersion >= 34, som er Sydney, og kallet er fullt kvalifisert som Winapi.GDIPAPI.GdiplusStartup slik at enhetsoppløsningsrekkefølgen ikke kan substituere en annen deklarasjon på en versjon midt i området

function TXLSPageImageExporter.EncodeTiff(Stream: TStream): Integer;
var
  StartupInput: TGdiplusStartupInput;
  // GDIPAPI-ens var-parametertype for GdiplusStartup følger VCL-
  // generasjonen: Cardinal til og med Rio, THandle fra Sydney og fremover
  {$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;

Dette er TIFF-grenen til sidebildeeksportøren, så nedslagsfeltet ved å få den feil er hele rastereksportflaten, inkludert stiene som beskrives i eksport av et celleområde som ett bilde. Legg også merke til hva vakten ikke hevder: ULONG_PTR og THandle har samme bredde på begge plattformer, så valget handler om hvilken identifikator deklarasjonen navngir, ikke om 32- kontra 64-bits korrekthet

Hvorfor rapporterte den første probe-kjøringen ingenting?

Versjonsproben rapporterte ingenting ved første kjøring fordi res=$(...)-tildelinger ble gjort inne i en subshell, der de ikke forplanter seg til forelderen. dcc32 avslutter med 0 ved suksess, så exit-koden var riktig signal å fange, og skriptet fanget den i en variabel som sluttet å eksistere én linje senere. Hvert ben kom tilbake tomt, og output-en så ut som en probe som ikke hadde kompilert noe, som er nøyaktig det den var

Den andre feilen var verre, fordi den produserte et feil svar i stedet for ikke noe svar. Proben klassifiserte et ben ved å telle linjer som matchet Error, og Delphi prefikser ikke hver fatal feil med det ordet. F1026 File not found er fatal og matcher ikke, så en probe som ikke kunne løse en enhet i det hele tatt, ble skåret som et rent pass. XE5 leverer ikke Winapi.GDIPOPS.dcu, den første proben traff nøyaktig det, og den ble falskt grønn. Regelen som kom ut av det, er smal og verdt å si tydelig: Døm en kompilatorprobe etter den produserte artefakten eller kompilatorens egen summary-linje, aldri ved å grepe output-en etter et nøkkelord. Å grepe stderr etter Error er en heuristikk som feiler i den ene retningen du ikke har råd til, og rapporterer stille suksess

Hva koster det faktisk å støtte et tiår med kompilatorer?

Den ærlige oppsummeringen er at kodeendringene her er trivielle, mens prosessendringene ikke er det. Fire av de fem avvisningene ble rettet ved å skrive mer vanlig Pascal, ikke ved å legge til versjonsmaskineri: Fjern en cast, del i stedet for å caste, gi nil en type og deklarer en accessor-klasse. Bare GdiplusStartup fortjente en {$IF}. En kodebase som spenner fra XE5 til dagens release, blir ikke et kratt av conditional defines med mindre du lar harde casts og idiomer fra den nyeste kompilatoren samle seg i utgangspunktet

Det som virkelig koster, er byggetid og disiplin. Førtitre ben er et langsomt skript, og nettopp derfor drev det til pakketid og deretter til aldri. Det forsvarlige kompromisset er å beholde den raske fireskript-løkken for iterasjon og kjøre hele matrisen etter en tidsplan som ikke kan hoppes over, fordi feilmodusen ikke er et ødelagt bygg du legger merke til, men en støttet IDE som stille sluttet å være støttet for tolv releaser siden

Den forpliktelsen er baksiden av å levere en native komponent i det hele tatt. HotXLS leser og skriver XLS, XLSX og ODS gjennom bare Object Pascal, uten Excel-installasjon og uten COM-avhengighet, noe som gjør Office-fri arbeidsbokautomatisering mulig på en låst server. Den samme egenskapen betyr at kompilatoren er hele plattformkontrakten, så hver versjon i matrisen er et løfte som må verifiseres på nytt, ikke antas

Krysskompilator-byggmatrisen og den versjonssikre koden som omtales her, leveres som del av HotXLS Delphi Spreadsheet Component, som støtter Delphi og C++Builder fra XE5 til dagens release med forhåndsbygde biblioteksbinærer for hver støttet IDE