Techninis straipsnis

Delphi kompiliatorių build matrica: HotXLS nuo XE5

HotXLS vieną Object Pascal kodų bazę pateikia kiekvienam Delphi ir C++Builder leidimui nuo XE5, o build-All-Lib-TRIAL.cmd yra scenarijus, kuris tai įrodo: 43 build žingsniai, apimantys 12 Delphi versijų Win32 ir Win64 bei 10 C++Builder Win32 ir 9 Win64 paketų build. Nuo v2.363 iki v2.374 šis scenarijus niekada nebuvo paleistas iki galo, o XE5 žingsnis visą laiką buvo sugadintas

Kai tai pamatėme, nieko subtilaus nebeliko. Penkios atskiros konstrukcijos, kurias dabartinis kompiliatorius priima be pastabų, RAD Studio XE5 yra kietos klaidos, o build matrica jas žymi 12.0. v2.375.0 leidime visos penkios ištaisytos ir matrica vėl tapo žalia – 43 iš 43. Toliau pateikiamas kiekvienas atmetimas, kodėl senasis kompiliatorius pagrįstai atmeta du iš jų dėl tipų ir dar gėdingesnė dalis: zondo scenarijus, parašytas šiai netvarkai diagnozuoti, pirmą kartą pranešė apie netikrą sėkmę

Kodėl XE5 žingsnis galėjo tyliai gesti, niekam nepastebint?

XE5 žingsnis sunyko, nes kasdienis kūrimas paleisdavo tik 37.0 keturių scenarijų rinkinį, o žalias vietinis build nieko nepasako apie kompiliatorių, kurio nepaleidote. Visa matrica yra atskiras lėtas scenarijus, kurį trial diegiklis kviečia prieš Inno Setup surenkant failus, todėl jis tikrinamas pakavimo metu, o ne commit metu. Per šį tarpą telpa dvylika leidimų

Verta išdėstyti žingsnių aritmetiką, nes būtent ten slypi aprėpties iliuzija. DELPHI_TRIAL_VERSIONS išvardija 12.0–37.0 ir kiekviena iš 12 versijų buildinama du kartus – Win32 ir Win64. CB_TRIAL_WIN32_VERSIONS turi 10 versijų, o CB_TRIAL_WIN64_VERSIONS tik 9, nes XE5 turi C++Builder paketo projektą, bet nepateikia Win64 paketo paleidimo objekto c0pkg64.o. Dvylika plius dvylika plius dešimt plius devyni yra 43. Paleisti keturis ir vadinti kodų bazę nešiojama yra kategorijos klaida, būtent ji leido šiam atvejui atsirasti

HotXLS yra nukentėjusi nuo tokios pačios problemos ir iš kitos pusės. Naujas unitas, pasiekiamas per uses sąlygą, bet neįtrauktas į .cbproj failų sąrašą, Delphi aplinkoje kompiliuojasi puikiai, nes dcc netiesiogiai įtraukia į paketą neįrašytus unitus ir blogiausiu atveju išveda W1033 pastabą. C++Builder sukuria .obj tik unitams, nurodytiems <DelphiCompile>, todėl tas pats kodas ilink etape žlunga su neišspręstu išoriniu simboliu. Viena toolchain slepia tai, ką kita pagauna. Tai visas argumentas paleisti matricą, o ne pasitikėti reprezentatyviu kompiliatoriumi

Kieti tipų cast, kuriuos atmeta seni Win32 kompiliatoriai

Dvi iš penkių klaidų yra tas pats gedimas su skirtingais drabužiais: kietas tipo cast pritaikytas slankiojo kablelio išraiškai, o ne kintamajam. Win32 aplinkoje senesni kompiliatoriai aritmetiką vertina per x87 steką, todėl sudėtis, kurioje dalyvauja Double, atliekama su 80 bitų papildomu tikslumu, o jos statinis tipas tampa 10 baitų Extended. 10 baitų susiaurinimas iki 8 baitų TDateTime nėra teisėtas tipo cast, todėl kompiliatorius pateikia E2089 Invalid typecast

Erzinanti detalė – kintamojo forma veikia. TDateTime(Serial) kompiliuojasi kiekvienoje matricos versijoje, nes Serial jau yra 8 baitų, o cast išlaiko dydį. Pridėkite ką nors ir išraiška po jumis išsiplečia. Pataisymas nėra platesnis cast ar sąlyginė apibrėžtis, o cast atsisakymas: netiesioginis realaus į realų priskyrimas teisingai konvertuojamas visuose HotXLS palaikomuose kompiliatoriuose ir tiksliai nusako, ką kodas reiškia

// XE5 (Win32) atmeta: kiekviena sudėtis vertinama kaip 10 baitų
// Extended, o 10 į 8 baitų siaurinantis cast kelia E2089
if Dates1904 then
  Value := TDateTime(Serial + XLSDate1904Offset)
else if Serial < 60 then
  Value := TDateTime(Serial + 1)
else
  Value := TDateTime(Serial);        // šis priimamas: nėra sudėties

// Saugu versijoms: leiskime realaus į realų priskyrimui atlikti konversiją
if Dates1904 then
  Value := Serial + XLSDate1904Offset
else if Serial < 60 then
  Value := Serial + 1
else
  Value := Serial;

// Tokios pačios klasės atmetimas langelio reikšmės pakuotoje: kietas Double cast
// sveikajam skaičiui. Vietoj to dalykite – operatorius jau grąžina realų tipą
if (Scaled = intVal) and (Double(intVal) / 100 = AValue) then   // E2089
  ;
if (Scaled = intVal) and (intVal / 100 = AValue) then           // nešiojama
  ;

Šaka Serial < 60 yra 1900 keliamųjų metų fikcija, o ne vieneto klaida: serialas 60 yra neegzistuojanti Excel 1900-02-29, todėl serialams iki jo reikia pridėti dieną prieš DecodeDate. Perkeliamumo darbas niekada neturi tyliai keisti tokios logikos, todėl saugus redagavimas pašalina cast ir palieka aritmetiką nepakeistą

Kas sugenda, kai nil yra procedūrinis argumentas?

Plikas nil, perduotas ten, kur tikimasi procedūrinio tipo, senesniuose kompiliatoriuose nesusiejamas perkrovos sprendimo metu. HotXLS iškvietimo vieta yra ResolveIndexedColor, kuris turi overload ir priima TXLSTryResolveSystemColor callback, kurio daugumai iškvietėjų nereikia. Naujesni kompiliatoriai nil susieja su procedūriniu parametru ir pasirenka teisingą overload. XE5 to nepadaro, o diagnostika rodo overload rinkinį, o ne argumentą, todėl prarandate dvidešimt minučių

Nešiojamas atsakymas – suteikti nuliniam callback tipą. Unit lygio procedūrinio tipo kintamasis kalbos inicializuojamas nuliu, todėl jau yra nil be inicializatoriaus, o senajam sprendikliui suteikia reikiamą tipo informaciją. Kai unit lygio kintamasis būtų per didelis, tą patį atlieka tipinis local, kuriam priskirta nil

var
  // Senesnių kompiliatorių overload sprendimas nesusieja
  // nil procedūrinio literalo; tipinis nuliais inicializuotas kintamasis susiejamas
  NilSystemColorResolver: TXLSTryResolveSystemColor;

// ...

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

// Tas pats pataisymas su tipiniu local XLSX darbaknygėje
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;

Atkreipkite dėmesį, kad tai tikras kalbos lygio skirtumas, o ne kompiliatoriaus klaida, kurią verta apeiti defines. Nuliais inicializuotas kintamasis teisingas kiekvienoje matricos versijoje ir kainuoja vieną eilutę, todėl čia visai nereikia sąlyginio kompiliavimo. {$IF CompilerVersion} naudokite tik tada, kai platforma tarp leidimų iš tikrųjų skiriasi, o šioje partijoje taip nutinka lygiai vieną kartą

Apsaugoti VCL metodai tarp leidimų keičia prieinamumą

TPicture.LoadFromStream dabartiniame VCL yra public, o senesnėse HotXLS palaikomose versijose – protected, todėl tiesioginis iškvietimas dabar kompiliuojasi, o tada žlunga. HotXLS jį naudoja tikrinti, ar darbalapio fono vaizdo naudingoji apkrova iš tikrųjų iškoduojama – tai kontrolė, vykdoma prieš HTML eksportuotojui įsipareigojant įterpti baitus. Taikomas klasikinis Pascal atsakymas: tame pačiame unite deklaruokite palikuonį vien tam, kad praplėstumėte matomumą, o iškvietimo vietoje naudokite cast

type
  // TPicture.LoadFromStream senesnėse palaikomose VCL versijose yra protected;
  // tame pačiame unite esantis palikuonis jį atveria
  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);

Prieigos klasės triukas čia saugus, nes palikuonis neprideda laukų ir niekada nesukuriamas; cast tik pakeičia tai, ką kompiliatorius leidžia įvardyti. Vis tiek verta palikti komentarą prie deklaracijos, nes žmogui, kuris kuria tik su dabartine IDE, kitaip atrodys kaip beprasmis tipas. Fono vaizdų tvarkymas dar kartą pasirodo pasirinktiniame VCL tinklelio atvaizdavimo kelyje, kur tas pats iškoduotas turinys maitina ekrane rodomą lapą

GdiplusStartup žetono tipas pasikeitė du kartus

Vienintelis partijos atmetimas, kuriam iš tikrųjų reikia sąlyginio kompiliavimo, yra GdiplusStartup var parametro tipas, tarp VCL kartų pasikeitęs taip, kad vienas rašybos variantas netinka visur. Versijomis paremtas tikrinimas nustatė tikrą elgesį: 12.0–20.0 žingsniai priima tik Cardinal, 21.0 ir 22.0 – tik THandle arba ULONG_PTR, o 23.0 ir 37.0 priima abu. Leidimų pavadinimais tai reiškia Cardinal nuo XE5 iki 10.3 Rio ir THandle nuo 10.4 Sydney. Kadangi 12.0–22.0 priimamų intervalų sankirtos nėra, besąlyginė deklaracija neveikia: vartai remiasi CompilerVersion >= 34, tai yra Sydney, o iškvietimas visiškai kvalifikuojamas kaip Winapi.GDIPAPI.GdiplusStartup, kad unitų sprendimo tvarka nepasirinktų kitos deklaracijos kurioje nors tarpinėje intervalo versijoje

function TXLSPageImageExporter.EncodeTiff(Stream: TStream): Integer;
var
  StartupInput: TGdiplusStartupInput;
  // GDIPAPI GdiplusStartup var parametro tipas priklauso nuo VCL kartos:
  // Cardinal iki Rio, THandle nuo 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;

Tai yra TIFF puslapio vaizdų eksportuotojo šaka, todėl klaidos mastas apima visą rastrinės išvesties paviršių, įskaitant kelius, aprašytus eksportuojant langelių diapazoną kaip vieną vaizdą. Taip pat svarbu, ko šie vartai neteigia: ULONG_PTR ir THandle abiejose platformose yra tokio pat pločio, todėl pasirinkimas susijęs su tuo, kurį identifikatorių deklaracija įvardija, o ne su 32 ar 64 bitų teisingumu

Kodėl pirmasis zondo paleidimas nieko nepranešė?

Versijos zondas per pirmą paleidimą nieko nepranešė, nes res=$(...) priskyrimai buvo atliekami subshell viduje, kur jie nepersiduoda tėviniam procesui. dcc32 sėkmės atveju grąžina 0, todėl išėjimo kodas buvo teisingas signalas, kurį reikėjo pagauti, o scenarijus jį fiksavo kintamajame, nustojusiame egzistuoti po vienos eilutės. Visi žingsniai grįžo tušti, o išvestis atrodė kaip zondas, kuris nieko nesukompiliavo, ir būtent taip buvo

Antrasis gedimas buvo blogesnis, nes sukūrė neteisingą atsakymą, o ne jokio atsakymo. Zondas žingsnį klasifikavo skaičiuodamas eilutes, atitinkančias Error, o Delphi ne kiekvieną kritinę klaidą pradeda šiuo žodžiu. F1026 File not found yra kritinė klaida ir neatitinka paieškos, todėl zondas, visai nesugebėjęs išspręsti unito, buvo įvertintas kaip sėkmingas. XE5 neturi Winapi.GDIPOPS.dcu, pirmasis zondas pataikė būtent į tai ir klaidingai tapo žalias. Išvada siaura ir verta aiškaus sakinio: kompiliatoriaus zondą vertinkite pagal sukurtą artefaktą arba paties kompiliatoriaus suvestinės eilutę, niekada ne pagal išvesties grepinimą pagal raktinį žodį. stderr grepinimas ieškant Error yra euristika, kuri sugenda tuo vieninteliu būdu, kurio negalite sau leisti, tyliai pranešdama apie sėkmę

Kiek iš tikrųjų kainuoja dešimtmečio kompiliatorių palaikymas?

Sąžininga apskaita tokia: čia aprašyti kodo pakeitimai trivialūs, o proceso pakeitimai – ne. Keturi iš penkių atmetimų pataisyti parašius paprastesnį Pascal, o ne pridėjus versijų mechanizmą: pašalinti cast, dalyti vietoje cast, suteikti tipą nil, deklaruoti prieigos klasę. Tik GdiplusStartup užsitarnavo {$IF}. Kodų bazė, apimanti XE5 iki dabartinio leidimo, netampa sąlyginių defines raizgalyne, nebent nuo pradžių leidžiate kauptis kietiems cast ir naujausio kompiliatoriaus idiomoms

Tikroji kaina yra build laikas ir disciplina. Keturiasdešimt trys žingsniai yra lėtas scenarijus, todėl jis ir nuplaukė į pakavimo laiką, o paskui apskritai buvo pamirštas. Gintinas vidurys – iteracijai palikti greitą keturių scenarijų ciklą, o visą matricą paleisti pagal grafiką, kurio negalima praleisti, nes gedimo režimas nėra sugadintas build, kurį pastebite, o palaikoma IDE, kuri tyliai nustojo būti palaikoma prieš dvylika leidimų

Ši pareiga yra natūralaus komponento siuntimo kita pusė. HotXLS vien Object Pascal kalba skaito ir rašo XLS, XLSX bei ODS, be įdiegto Excel ir be COM priklausomybės, todėl užrakintame serveryje įmanomas Office nenaudojantis darbaknygių automatizavimas. Ta pati savybė reiškia, kad kompiliatorius yra visa platformos sutartis, todėl kiekviena matricos versija yra pažadas, kurį reikia iš naujo patikrinti, o ne laikyti savaime suprantamu

Kryžminių kompiliatorių build matrica ir čia aptartas versijoms saugus kodas pateikiami HotXLS Delphi Spreadsheet komponente, kuris palaiko Delphi ir C++Builder nuo XE5 iki dabartinio leidimo su iš anksto sukompiliuotais bibliotekų dvejetainiais failais kiekvienai palaikomai IDE