Tekninen artikkeli

Delphin ristikääntäjien matriisi: HotXLS XE5:stä alkaen

HotXLS toimittaa yhden Object Pascal -koodikannan jokaiselle Delphi- ja C++Builder-julkaisulle XE5:stä eteenpäin, ja build-All-Lib-TRIAL.cmd on skripti joka todistaa sen: 43 build-haaraa, joissa on 12 Delphi-versiota Win32- ja Win64-kohteille sekä 10 C++Builder Win32- ja 9 Win64-paketin buildit. Versioiden v2.363–v2.374 välillä skriptiä ei koskaan ajettu loppuun ja XE5-haara oli rikki koko ajan

Kun vika nähtiin, mikään siinä ei ollut hienovaraista. Viisi erillistä rakennetta jotka nykyinen kääntäjä hyväksyy huomautuksitta ovat RAD Studio XE5:llä kovia virheitä, ja build-matriisi merkitsee sen numerolla 12.0. v2.375.0 korjasi kaikki viisi ja matriisi muuttui jälleen vihreäksi tuloksella 43/43. Seuraavaksi käydään läpi jokainen hylkäys, miksi vanha kääntäjä on tyyppisyistä oikeassa kahdessa niistä ja nolompi osa: sotkun diagnosointiin kirjoitettu probe-skripti ilmoitti ensimmäisellä ajollaan väärän läpäisyn

Miksi XE5-haara rapautui kenenkään huomaamatta?

XE5-haara rapautui, koska päivittäinen kehitys ajoi vain 37.0:n neljän skriptin joukkoa, eikä vihreä paikallinen build kerro mitään kääntäjästä jota et kutsunut. Täysi matriisi on erillinen hidas skripti jonka trial-installer kutsuu ennen kuin Inno Setup kerää tiedostot, joten se suoritetaan paketoinnin yhteydessä eikä commitin yhteydessä. Kaksitoista julkaisua mahtui tähän aukkoon

Haaran aritmetiikka kannattaa avata, koska siinä elää kattavuusharha. DELPHI_TRIAL_VERSIONS luettelee versiot 12.0:sta 37.0:aan ja jokainen näistä 12 versiosta rakennetaan kahdesti, Win32:lle ja Win64:lle. CB_TRIAL_WIN32_VERSIONS listaa 10 versiota ja CB_TRIAL_WIN64_VERSIONS vain 9, koska XE5:llä on C++Builder-pakettiprojekti mutta se ei toimita Win64-paketin käynnistysobjektia c0pkg64.o. Kaksitoista plus kaksitoista plus kymmenen plus yhdeksän on 43. Neljän ajaminen ja koodikannan kutsuminen siirrettäväksi on luokkavirhe, ja juuri tämä virhe päästi tilanteen syntymään

HotXLS on törmännyt samaan ongelman muotoon myös vastakkaisesta suunnasta. Uusi unit joka saavutetaan uses-lausekkeella mutta puuttuu .cbproj-tiedoston listasta kääntyy Delphillä täydellisesti, koska dcc vetää listaamattomat unitit implisiittisesti pakettiin ja korkeintaan antaa W1033-huomautuksen. C++Builder tuottaa .obj-tiedoston vain unitille joka on nimetty <DelphiCompile>-listassa, joten sama koodi kuolee ilink-vaiheessa unresolved externaliin. Yksi toolchain piilottaa asian jonka toinen nappaa kiinni. Se on koko peruste ajaa matriisi eikä luottaa edustavaan kääntäjään

Kovat tyyppicastit jotka vanhat Win32-kääntäjät hylkäävät

Kaksi viidestä hylkäyksestä on sama bugi eri vaatteissa: kova tyyppicasti kohdistetaan liukulausekkeeseen eikä muuttujaan. Win32:ssa vanhemmat kääntäjät laskevat aritmetiikan x87-pinon kautta, joten Double-arvon sisältävä yhteenlasku säilytetään 80-bittisenä ylimääräisenä tarkkuutena ja sen staattiseksi tyypiksi tulee 10-tavuinen Extended. Kymmenen tavun muuttaminen kahdeksan tavun TDateTime-arvoksi ei ole laillinen typecast ja kääntäjä ilmoittaa E2089 Invalid typecast

Raivostuttava yksityiskohta on että muuttujamuoto toimii. TDateTime(Serial) kääntyy jokaisella matriisin versiolla, koska Serial on jo 8 tavua ja cast säilyttää koon. Lisää siihen mitä tahansa ja lauseke levenee alla. Korjaus ei ole leveämpi cast tai conditional define vaan castin poistaminen: implisiittinen real-to-real-sijoitus muuntaa oikein jokaisella HotXLS:n tukemalla kääntäjällä ja kertoo mitä koodi oikeasti tarkoittaa

// XE5 hylkää tämän (Win32): jokainen yhteenlasku lasketaan 10-tavuisena
// Extended-arvona ja 10-to-8-kavennuscast nostaa E2089-virheen
if Dates1904 then
  Value := TDateTime(Serial + XLSDate1904Offset)
else if Serial < 60 then
  Value := TDateTime(Serial + 1)
else
  Value := TDateTime(Serial);        // tämä hyväksytään: ei yhteenlaskua

// Versioturvallinen: anna real-to-real-sijoituksen hoitaa muunnos
if Dates1904 then
  Value := Serial + XLSDate1904Offset
else if Serial < 60 then
  Value := Serial + 1
else
  Value := Serial;

// Sama hylkäysluokka soluarvon pakkaajassa: kokonaisluvun kova Double-cast
// Jaa sen sijaan - operaattori tuottaa jo real-arvon
if (Scaled = intVal) and (Double(intVal) / 100 = AValue) then   // E2089
  ;
if (Scaled = intVal) and (intVal / 100 = AValue) then           // siirrettävä
  ;

Haara Serial < 60 on vuoden 1900 karkausvuosifiktio eikä off-by-one: serial 60 on Excelin olematon 1900-02-29, joten sitä pienemmät serialit tarvitsevat ylimääräisen päivän ennen kuin DecodeDate näkee ne. Siirrettävyystyön ei pidä koskaan muuttaa hiljaa tämän kaltaista logiikkaa, minkä vuoksi turvallinen muutos poistaa castin ja jättää aritmetiikan ennalleen

Mitä hajoaa kun nil on proseduraalinen argumentti?

Paljas nil proseduraalityyppiä odottavassa kohdassa ei sitoudu vanhempien kääntäjien overload-ratkaisussa. HotXLS:n kutsukohta on ResolveIndexedColor, joka on overloadattu ja ottaa TXLSTryResolveSystemColor-callbackin jota useimmat kutsujat eivät tarvitse. Uudemmat kääntäjät ratkaisevat nil-arvon proseduraalista parametria vasten ja valitsevat oikean overloadin. XE5 ei tee niin, ja diagnostiikka osoittaa overload-joukkoa eikä argumenttia, joten siinä hukkaa kaksikymmentä minuuttia

Siirrettävä vastaus on antaa null-callbackille tyyppi. Unit-tason proseduraalityyppinen muuttuja alustetaan kielellä nollaan, joten se on ilman initialisaattoria jo nil ja kuljettaa vanhan resolverin tarvitseman tyyppitiedon. Kun unit-tason muuttuja olisi ylimitoitettu, tyypitetty paikallinen muuttuja jolle annetaan nil tekee saman

var
  // Vanhemmat kääntäjät eivät sido proseduraalista nil-literaalia
  // overload-ratkaisussa; tyypitetty nollaan alustettu muuttuja sitoutuu
  NilSystemColorResolver: TXLSTryResolveSystemColor;

// ...

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

// Sama korjaus tyypitetyllä paikallisella muuttujalla XLSX-työkirjassa
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;

Huomaa että tämä on todellinen kielitason ero eikä kääntäjäbugi jota kannattaisi kiertää defineillä. Nollaan alustettu muuttuja on oikein jokaisella matriisin versiolla ja maksaa yhden rivin, joten tässä ei ole conditional compilationia lainkaan. Käytä {$IF CompilerVersion}-rakennetta vain kun alusta oikeasti eroaa julkaisujen välillä, mikä tapahtuu tässä batchissa täsmälleen kerran

Suojatut VCL-metodit liikkuvat julkaisujen välillä

TPicture.LoadFromStream on nykyisessä VCL:ssä public mutta vanhoissa HotXLS:n tukemissa versioissa protected, joten suora kutsu kääntyy nyt ja epäonnistuu silloin. HotXLS käyttää sitä varmistaakseen että worksheetin background-kuvan payload todella dekoodautuu, eli tarkistukseen joka tehdään ennen kuin HTML-exporter sitoutuu upottamaan tavut. Klassinen Pascal-vastaus pätee: määrittele samaan unitiin descendant pelkästään näkyvyyden laajentamiseksi ja castaa sen kautta kutsukohdassa

type
  // TPicture.LoadFromStream on protected HotXLS:n tukemissa vanhoissa
  // VCL-versioissa; saman unitin descendant avaa sen näkyviin
  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-luokan temppu on tässä turvallinen, koska descendant ei lisää kenttiä eikä sitä koskaan luoda; cast muuttaa vain sen mitä kääntäjä sallii sinun nimetä. Kommentti määrittelyssä on silti arvokas, koska vain nykyisellä IDE:llä rakentava lukija näkee muuten turhalta näyttävän tyypin. Taustakuvien käsittely näkyy taas mukautetussa VCL-ruudukon renderöintipolussa, jossa sama dekoodattu payload syötetään ruudulla näkyvään sheettiin

GdiplusStartup-tokenin tyyppi muuttui kahdesti

Ainoa tämän batchin hylkäys joka todella tarvitsee ehdollista käännöstä on GdiplusStartup-funktion var-parametrin tyyppi, joka muuttui VCL-sukupolvien välillä tavalla joka ei jätä yhtä kaikkialla kelpaavaa kirjoitusasua. Versiokohtainen probetus lukitsi todellisen käyttäytymisen: versiot 12.0–20.0 hyväksyvät vain Cardinal-tyypin, versiot 21.0 ja 22.0 vain THandle- tai ULONG_PTR-tyypin ja versiot 23.0 sekä 37.0 hyväksyvät molemmat. Julkaisunimillä se tarkoittaa Cardinaalia XE5:stä 10.3 Rioon ja THandlea 10.4 Sydneystä eteenpäin. Koska hyväksyvät alueet eivät leikkaa toisiaan versioissa 12.0–22.0, mikään ehdoton määrittely ei toimi: suoja käyttää ehtoa CompilerVersion >= 34, joka on Sydney, ja kutsu kvalifioidaan kokonaan muotoon Winapi.GDIPAPI.GdiplusStartup, jotta unitien ratkaisujärjestys ei voi valita eri määrittelyä jossakin keskellä aluetta

function TXLSPageImageExporter.EncodeTiff(Stream: TStream): Integer;
var
  StartupInput: TGdiplusStartupInput;
  // GDIPAPI:n GdiplusStartup-var-parametrin tyyppi seuraa VCL-sukupolvea:
  // Cardinal Rioon asti, THandle Sydneystä eteenpäin
  {$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;

Tämä on sivukuvien exporter-palvelun TIFF-haara, joten väärän ratkaisun vaikutusalue on koko rasteriviennin pinta mukaan lukien polut jotka kuvataan artikkelissa solualueen vieminen yhtenä kuvana. Huomaa myös mitä suoja ei väitä: ULONG_PTR ja THandle ovat saman levyisiä molemmilla alustoilla, joten valinta koskee sitä minkä tunnisteen määrittely nimeää eikä 32- ja 64-bittisyyden oikeellisuutta

Miksi ensimmäinen probe-ajo ei ilmoittanut mitään?

Versioprobe ei ilmoittanut ensimmäisellä ajollaan mitään, koska res=$(...)-sijoitukset tehtiin subshellin sisällä, eivätkä ne siirry parentille. dcc32 palauttaa onnistumisessa arvon 0, joten exit code oli oikea talteen otettava signaali ja skripti tallensi sen muuttujaan joka lakkasi olemasta yhtä riviä myöhemmin. Jokainen haara tuli takaisin tyhjänä ja tuloste näytti probelta joka ei ollut kääntänyt mitään, mikä oli täsmälleen totta

Toinen virhe oli pahempi, koska se tuotti tyhjän tuloksen sijaan väärän vastauksen. Probe luokitteli haaran laskemalla rivit jotka täsmäsivät sanaan Error, eikä Delphi aloita jokaista fataalia virhettä sillä sanalla. F1026 File not found on fataali eikä täsmää, joten probe joka ei pystynyt ratkaisemaan unitia lainkaan pisteytettiin puhtaaksi läpäisyksi. XE5 ei toimita Winapi.GDIPOPS.dcu-tiedostoa, ensimmäinen probe osui juuri tähän ja muuttui väärin vihreäksi. Tästä seurannut sääntö on kapea ja kannattaa sanoa suoraan: arvioi kääntäjäprobe tuotetun artefaktin tai kääntäjän oman summary-rivin perusteella, älä greppaa tulostetta avainsanalla. Stderrin greppaaminen Error-sanalla on heuristiikka joka epäonnistuu suunnassa johon sinulla ei ole varaa, ilmoittaen hiljaa onnistumisesta

Mitä vuosikymmenen kääntäjien tukeminen oikeasti maksaa?

Rehellinen laskelma on että koodimuutokset ovat tässä mitättömiä mutta prosessimuutokset eivät. Neljä viidestä hylkäyksestä korjattiin kirjoittamalla tavallisempaa Pascalia eikä lisäämällä versiokoneistoa: poista cast, jaa castin sijaan, anna nil-arvolle tyyppi ja määrittele accessor-luokka. Vain GdiplusStartup ansaitsi {$IF}-rakenteen. XE5:stä nykyiseen julkaisuun ulottuva koodikanta ei muutu ehdollisten definejen ryteiköksi ellei kovia casteja ja uusimman kääntäjän idiomeja päästetä kertymään alusta alkaen

Se mikä oikeasti maksaa on build-aika ja kuri. 43 haaraa on hidas skripti, minkä vuoksi se ajautui paketointiaikaan ja sitten ei koskaan mihinkään. Puolustettava keskitie on pitää nopea neljän skriptin kierros iterointia varten ja ajaa täysi matriisi aikataululla jota ei voi ohittaa, koska vikatila ei ole huomattava rikki mennyt build vaan tuettu IDE joka lakkasi hiljaa olemasta tuettu kaksitoista julkaisua sitten

Tämä velvollisuus on minkä tahansa natiivin komponentin toimittamisen kääntöpuoli. HotXLS lukee ja kirjoittaa XLS-, XLSX- ja ODS-tiedostoja vain Object Pascalilla ilman Excel-asennusta ja COM-riippuvuutta, mikä mahdollistaa Office-vapaan työkirja-automaation lukitulla palvelimella. Sama ominaisuus tarkoittaa että kääntäjä on koko alustasopimus, joten jokainen matriisin versio on lupaus joka täytyy varmistaa uudelleen eikä olettaa

Tässä käsitelty ristikääntäjien build-matriisi ja versioturvallinen koodi toimitetaan HotXLS Delphi Spreadsheet Component -komponentin osana, joka tukee Delphiä ja C++Builderia XE5:stä nykyiseen julkaisuun asti ja tarjoaa valmiit kirjastobinaarit jokaiselle tuetulle IDE:lle