Tehnični članak

Matrika gradnje Delphi: HotXLS od XE5 naprej

HotXLS eno kodno zbirko Object Pascal pošlje vsaki izdaji Delphi in C++Builder od XE5 naprej, build-All-Lib-TRIAL.cmd pa je skript, ki to dokazuje: 43 gradbenih vej, ki pokrivajo 12 različic Delphi na Win32 in Win64 ter 10 paketnih gradenj C++Builder za Win32 in 9 za Win64. Od v2.363 do v2.374 ta skript nikoli ni bil izveden do konca, veja XE5 pa je bila ves čas pokvarjena

Ko je bila napaka videna, nič pri njej ni bilo subtilnega. Pet različnih konstrukcij, ki jih trenutni prevajalnik sprejme brez pripombe, je v RAD Studio XE5, ki ga matrika označuje z 12.0, trdih napak. Izdaja v2.375.0 je vseh pet popravila in matrika je pri 43 od 43 spet postala zelena. Sledi vsaka zavrnitev, zakaj ima stari prevajalnik pri dveh, ki ju zavrne na podlagi tipov, morda celo prav, in bolj neprijeten del: preizkusni skript, napisan za diagnosticiranje nereda, je ob prvem zagonu prijavil lažen uspeh

Zakaj je veja XE5 propadala, ne da bi kdo opazil?

Veja XE5 je propadala, ker je vsakodnevni razvoj izvajal samo štirisklop 37.0, zelena lokalna gradnja pa ne pove ničesar o prevajalniku, ki ga niste zagnali. Celotna matrika je ločen počasen skript, ki ga namestitveni program trial pokliče, preden Inno Setup zbere datoteke, zato se izvede ob pakiranju namesto ob potrditvi spremembe. Dvanajst izdaj se je prilegalo v to vrzel

Aritmetiko vej je vredno zapisati, ker se v njej skriva iluzija pokritosti. DELPHI_TRIAL_VERSIONS našteva 12.0 do 37.0 in vsaka od teh 12 različic se zgradi dvakrat, za Win32 in Win64. CB_TRIAL_WIN32_VERSIONS našteva 10 različic, CB_TRIAL_WIN64_VERSIONS pa samo 9, ker ima XE5 projekt paketa C++Builder, ne izda pa začetnega objekta paketa Win64 c0pkg64.o. Dvanajst plus dvanajst plus deset plus devet je 43. Zagnati štiri od njih in kodo imenovati prenosljivo je napaka kategorije, prav ta napaka pa je to omogočila

HotXLS je ista oblika težave doživel tudi iz nasprotne smeri. Nova enota, ki je dosegljiva prek stavka uses, vendar manjka na seznamu datotek .cbproj, se pod Delphijem prevede brez težav, ker dcc neizrecno povleče nenavedene enote v paket in kvečjemu izda namig W1033. C++Builder izda .obj samo za enote, imenovane v <DelphiCompile>, zato ista koda na stopnji ilink umre z nerazrešenim zunanjim simbolom. Ena veriga orodij skrije, kar druga ujame. To je celoten argument za izvajanje matrike namesto zaupanja reprezentativnemu prevajalniku

Trdi pretvorbi tipov, ki ju stari prevajalniki Win32 zavrnejo

Dve od petih zavrnitev sta ista napaka v različnih oblačilih: trda pretvorba tipa je uporabljena na izrazu s plavajočo vejico namesto na spremenljivki. Na Win32 starejši prevajalniki aritmetiko izvajajo prek sklada x87, zato seštevanje, ki vključuje Double, poteka z 80-bitno dodatno natančnostjo in njegov statični tip postane 10-bajtni Extended. Pretvorba 10 bajtov navzdol v 8-bajtni TDateTime ni zakonita pretvorba tipa, prevajalnik pa to pove z E2089 Invalid typecast

Frustrirajoča podrobnost je, da je oblika s spremenljivko v redu. TDateTime(Serial) se prevede v vsaki različici matrike, ker je Serial že velik 8 bajtov in pretvorba ohrani velikost. Dodajte mu karkoli in izraz se pod vami razširi. Popravek ni širša pretvorba ali pogojna definicija, temveč prenehanje pretvarjanja: implicitna prireditev realnega števila drugemu realnemu številu se pravilno pretvori v vsakem prevajalniku, ki ga HotXLS podpira, in pove, kaj koda dejansko pomeni

// Zavrnitev v XE5 (Win32): vsako seštevanje se izračuna kot 10-bajtni
// Extended, zožitvena pretvorba 10 v 8 bajtov pa sproži E2089
if Dates1904 then
  Value := TDateTime(Serial + XLSDate1904Offset)
else if Serial < 60 then
  Value := TDateTime(Serial + 1)
else
  Value := TDateTime(Serial);        // ta je sprejet: brez seštevanja

// Različica, varna za vse različice: pretvorbo naj izvede prireditev med realnimi števili
if Dates1904 then
  Value := Serial + XLSDate1904Offset
else if Serial < 60 then
  Value := Serial + 1
else
  Value := Serial;

// Isti razred zavrnitve v pakirniku vrednosti celice: trda pretvorba Double
// celega števila. Namesto tega deli - operator že vrne realno vrednost
if (Scaled = intVal) and (Double(intVal) / 100 = AValue) then   // E2089
  ;
if (Scaled = intVal) and (intVal / 100 = AValue) then           // prenosljivo
  ;

Veja Serial < 60 je izmišljeno prestopno leto 1900, ne napaka za ena: serijska številka 60 je Excelov neobstoječi 1900-02-29, zato serijske številke pod njo potrebujejo dodatni dan, preden jih vidi DecodeDate. Delo za prenosljivost ne sme tiho spremeniti take logike, zato varni popravek tukaj odstrani pretvorbo in aritmetiko pusti nedotaknjeno

Kaj se zgodi, ko je nil proceduralni argument?

Goli nil, posredovan tam, kjer se pričakuje proceduralni tip, se pri razreševanju preobremenitve v starejših prevajalnikih ne more vezati. Mesto klica v HotXLS je ResolveIndexedColor, ki je preobremenjeno in sprejme povratni klic TXLSTryResolveSystemColor, ki ga večina klicateljev ne potrebuje. Novejši prevajalniki nil razrešijo glede na proceduralni parameter in izberejo pravo preobremenitev. XE5 tega ne stori, diagnostika pa pokaže na množico preobremenitev, ne na argument, in tako izgubite dvajset minut

Prenosljiv odgovor je, da ničelnemu povratnemu klicu daste tip. Spremenljivko na ravni enote jezik ničelno inicializira, zato je brez inicializatorja že nil in nosi informacije o tipu, ki jih želi stari razreševalnik. Kjer bi bila spremenljivka na ravni enote pretirana, isto delo opravi tipizirana lokalna spremenljivka, ki ji priredite nil

var
  // Proceduralni literal nil se v razreševanju preobremenitev starejših
  // prevajalnikov ne veže; tipizirana, ničelno inicializirana spremenljivka se
  NilSystemColorResolver: TXLSTryResolveSystemColor;

// ...

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

// Isti popravek s tipizirano lokalno spremenljivko v delovnem zvezku XLSX
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;

Upoštevajte, da je to prava razlika na ravni jezika, ne napaka prevajalnika, ki bi jo bilo smiselno obiti z definicijami. Ničelno inicializirana spremenljivka je pravilna v vsaki različici matrike in stane eno vrstico, zato pogojnega prevajanja tukaj sploh ni. Po {$IF CompilerVersion} posezite samo, kadar se platforma med izdajami res razlikuje, kar se v tem paketu zgodi natanko enkrat

Zaščitene metode VCL se med izdajami premikajo

TPicture.LoadFromStream je v trenutni VCL public, v starejših različicah, ki jih podpira HotXLS, pa protected, zato se neposreden klic zdaj prevede, takrat pa ne. HotXLS ga uporablja za preverjanje, ali se tovor slike ozadja delovnega lista res dekodira, torej za preverjanje podpisa, ki se izvede, preden se izvoznik HTML zaveže vdelavi bajtov. Velja klasičen Pascalov odgovor: v isti enoti deklarirajte potomca samo zato, da razširite vidnost, in ga na mestu klica uporabite za pretvorbo

type
  // TPicture.LoadFromStream je v starejših podprtih različicah VCL
  // protected; potomec v isti enoti ga izpostavi
  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);

Trik z dostopnim razredom je tukaj varen, ker potomec ne doda polj in nikoli ni ustvarjen; pretvorba samo spremeni, kako daleč vam prevajalnik dovoli poimenovati. Pri deklaraciji je še vedno vreden komentar, saj bo bralec, ki gradi samo na trenutnem IDE-ju, sicer videl nepotreben tip. Obravnava slik ozadja se znova pojavi v poti izrisovanja preglednice po meri VCL, kjer isti dekodirani tovor napaja list na zaslonu

Vrsta žetona GdiplusStartup se je spremenila dvakrat

Edina zavrnitev v paketu, ki resnično zahteva pogojno prevajanje, je vrsta parametra var pri GdiplusStartup, ki se je med generacijami VCL spremenila tako, da noben enoten zapis ni veljaven povsod. Preizkus po različicah je določil dejansko vedenje: veje 12.0 do 20.0 sprejmejo samo Cardinal, veje 21.0 in 22.0 samo THandle ali ULONG_PTR, 23.0 in 37.0 pa sprejmeta oboje. V imenih izdaj je to Cardinal od XE5 do 10.3 Rio in THandle od 10.4 Sydney naprej. Ker se sprejemajoča se razpona za 12.0 do 22.0 ne prekrivata, nobena brezpogojna deklaracija ne deluje: varovalo uporablja CompilerVersion >= 34, kar je Sydney, klic pa je v celoti kvalificiran kot Winapi.GDIPAPI.GdiplusStartup, da vrstni red razreševanja enot na kateri od vmesnih različic ne bi zamenjal deklaracije

function TXLSPageImageExporter.EncodeTiff(Stream: TStream): Integer;
var
  StartupInput: TGdiplusStartupInput;
  // Vrsta parametra var pri GDIPAPI GdiplusStartup sledi generaciji VCL
  // Cardinal do Ria, THandle od Sydney naprej
  {$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');
  // ... kodiraj ...
end;

To je veja TIFF izvoznika slik strani, zato je polmer učinka napačne izbire celotna površina rastrskega izvoza, vključno s potmi, opisanimi pri izvozu obsega celic kot ene slike. Upoštevajte tudi, česa varovalo ne trdi: ULONG_PTR in THandle sta na obeh platformah enake širine, zato izbira zadeva ime, ki ga deklaracija poimenuje, ne pravilnosti 32-bitne proti 64-bitni različici

Zakaj je prvi preizkusni zagon prijavil nič?

Preizkus različice pri prvem zagonu ni prijavil ničesar, ker so bile prireditve res=$(...) izvedene znotraj podlupine, kjer se ne prenesejo v nadrejeni proces. dcc32 ob uspehu konča z 0, zato je bila izhodna koda pravi signal za zajem, skript pa jo je zajel v spremenljivko, ki je eno vrstico pozneje prenehala obstajati. Vsaka veja se je vrnila prazna in izhod je bil videti kot preizkus, ki ni prevedel ničesar, kar je natanko to tudi bil

Druga napaka je bila hujša, ker je ustvarila napačen odgovor namesto nobenega. Preizkus je vejo razvrstil s štetjem vrstic, ki se ujemajo z Error, Delphi pa vsake usodne napake ne opremi s to besedo. F1026 File not found je usodna in se ne ujema, zato je bil preizkus, ki sploh ni mogel razrešiti enote, ocenjen kot čist uspeh. XE5 ne izda Winapi.GDIPOPS.dcu, prvi preizkus je zadel prav to in veja je lažno postala zelena. Pravilo, ki je iz tega nastalo, je ozko in ga je vredno povedati jasno: preizkus prevajalnika ocenite glede na ustvarjeni artefakt ali lastno povzetno vrstico prevajalnika, nikoli z iskanjem ključne besede v njegovem izhodu. Iskanje Error v stderr je hevristika, ki odpove v smeri, ki si je ne morete privoščiti, saj tiho sporoči uspeh

Koliko v resnici stane podpora desetletju prevajalnikov?

Pošten obračun je, da so spremembe kode tukaj trivialne, procesne spremembe pa ne. Štiri od petih zavrnitev so bile popravljene z bolj običajnim Pascalom, ne z dodajanjem mehanike različic: odstranite pretvorbo, delite namesto pretvarjanja, dajte nil tip in deklarirajte dostopni razred. Samo GdiplusStartup si je prislužil {$IF}. Kodna zbirka, ki sega od XE5 do trenutne izdaje, ne postane gošča pogojnih definicij, razen če že na začetku dovolite, da se nabirajo trde pretvorbe in idiomi najnovejšega prevajalnika

Pravi strošek sta čas gradnje in disciplina. Triinštirideset vej je počasen skript, prav zato je zdrsnil v čas pakiranja in nato v nikoli. Branljiva sredina je ohraniti hiter štirisklopni krog za iteriranje in celotno matriko izvajati po urniku, ki ga ni mogoče preskočiti, saj način odpovedi ni pokvarjena gradnja, ki jo opazite, temveč podprti IDE, ki je dvanajst izdaj nazaj tiho prenehal biti podprt

Ta obveznost je druga stran samega dostavljanja izvorne komponente. HotXLS bere in zapisuje XLS, XLSX in ODS samo prek Object Pascala, brez nameščenega Excela in brez odvisnosti COM, kar omogoča avtomatizacijo delovnih zvezkov brez Officea na zaklenjenem strežniku. Ista lastnost pomeni, da je prevajalnik celotna pogodba platforme, zato je vsaka različica v matriki obljuba, ki jo je treba znova preveriti, ne pa predpostaviti

Matrika gradnje za več prevajalnikov in tukaj obravnavana koda, varna za različice, sta del komponente HotXLS Delphi Spreadsheet Component, ki podpira Delphi in C++Builder od XE5 do trenutne izdaje s predzgrajenimi binarnimi knjižnicami za vsak podprt IDE