Isti vir Object Pascal se lahko pod prevajalnikoma Delphi in FPC/Lazarus obnaša različno na štiri načine, ki pogosto vplivajo na kodo komponente PDFium: FPC uniči začasne zapise (funkcijske rezultate) pred zaključkom preverjanja pripadnosti množici z in, prevajalnik dcc32 se privzeto prevede brez preverjanja obsega, tako da branje zunaj meja polja tiho prebere smeti, le Delphi 13 sprejme dodelitev anonimnega polja array of Byte spremenljivki TBytes brez eksplicitne pretvorbe, Delphijevo spajanje nizov AnsiString pa lahko uniči bajte pri vrednostih $80 ali več zaradi skrite pretvorbe kodne tabele prek Unicode. Vsaka od teh pasti povzroči, da je testni nabor na enem prevajalniku zelen, na drugem pa rdeč ali, kar je še slabše, tiho napačen
Če prvič nastavljate projekt z dvema prevajalnikoma, vodnik za pregledovalnik Lazarus in FPC opisuje uspešno pot: pakete, iskalne poti in prikaz okna za izris na zaslonu. Ta članek pa je nasprotje vadnice. Je seznam stvari, na katere smo naleteli po tem, ko je uspešna pot že delovala, ko je bil CI zelen pod FPC in zelen pod Delphijem, nato pa je sprememba, ki je na eni strani šla skozi, na drugi eksplodirala. Vsaka spodnja past izhaja iz resničnega neuspeha v testnem naboru PDFiumPas ali njegovih predstavitvah, forenzične ugotovitve na ravni commitov pa so strnjene v minimalno reprodukcijo, osnovni vzrok in popravek, ki smo ga standardizirali
Zakaj se množica pod FPC prebere kot prazna, v Delphiju pa ne?
Na kratko: FPC lahko sprosti začasno spremenljivko, ki hrani rezultantni zapis funkcije, preden se zaključi izraz, ki bere polje tega rezultata. Tako lahko klic X in Func().Issues preveri pripadnost množici glede na že sproščen zapis, medtem ko enakovreden izraz v Delphiju deluje. Naši testi skladnosti PDF/E so na to naleteli v svoji prvi različici. Validator vrne zapis, katerega polje Issues je množica zastavic kršitev, trditve (assertions) pa so klic vključile v isto vrstico
// Unreliable under FPC: the function-result record temporary
// can be released before the 'in' test reads Issues
AssertTrue(pveiLzwUsed in ValidateAnsi(Pdf).Issues);
// Reliable on both compilers: pin the result to a local first
var
Vr: TPdfEValidationResult;
begin
Vr := ValidateAnsi(Pdf);
AssertTrue(pveiLzwUsed in Vr.Issues);
end;
Vrstična oblika je pod prevajalnikom FPC prebrala množico kot prazno, zato je vsaka trditev, ki je pričakovala zastavico, spodletela, medtem ko je enak prevod v Delphiju uspel. Osnovni vzrok je razlika v tem, kako prevajalnika upravljata življenjsko dobo začasnih rezultatov funkcij znotraj večjih izrazov: Delphi ohranja začasni rezultat pri življenju do konca stavka, medtem ko lahko FPC-jevo sproščanje začasnega zapisa prehiti operator pripadnosti množici, ki ga še vedno bere. To vedenje smo dokumentirali že enkrat prej, v komentarju pri pomožni funkciji FlagPresent v testni enoti PDF/A, nato pa smo napako kljub temu ponovili pri pisanju novih testov iz nič, kar kaže, kako naravno je videti napačna oblika. Popravek je mehaničen in ga je vredno sprejeti kot splošno pravilo: nikoli ne vežite dostopa do polja ali preizkusa množice neposredno na klic funkcije, ki vrne zapis; rezultat najprej dodelite lokalni spremenljivki, nato pa preberite polje. To stane eno vrstico in odpravi celoten razred težav, odvisnih od prevajalnika
Zakaj Delphi sprejme indeks polja, ki ga FPC noče prevesti?
Na kratko: dcc32 prevede izvenkrmni indeks v polje s fiksnimi mejami in ob privzeto izklopljenem preverjanju obsega ob izvajanju tiho prebere ali zapiše sosednji pomnilnik brez kakršne koli napake, medtem ko FPC tak indeks zavrne že ob prevajanju. Komponenta PDFium deklarira točke štirikotnikov kot polje z začetkom pri 1, TQuadrilateralPoint = array [1..4] of TPdfPoint, kar ustreza običajnemu številčenju vnosov QuadPoints v PDF. Predstavitev, ki je polje napolnila z običajno zanko z začetkom pri 0, je v Delphiju delovala mesece
var
I: Integer;
begin
for I := 0 to 3 do // wrong: the array is [1..4]
Data.AttachmentPoints[I] := Corner[I]; // dcc32 default: compiles, index 0
// silently touches adjacent memory
// FPC: compile-time range check error
for I := Low(TQuadrilateralPoint) to High(TQuadrilateralPoint) do
Data.AttachmentPoints[I] := Corner[I - 1]; // correct on both compilers
end;
Gradnja v Delphiju je bila lažno pozitivna: ob izklopljenem preverjanju obsega (kar je privzeto za dcc32) je indeks 0 pristal na polju, ki v zapisu leži tik pred poljem, in zdelo se je, da predstavitev deluje. Prenos iste predstavitve na Lazarus je takoj sprožil napako preverjanja obsega ob prevajanju v FPC, popravek indeksa pa je nato razkril drugo, globljo napako v poti označevanja knjižnice, tisto, ki je podrobno opisana v članku o označbah s točkami QuadPoints. Iz tega incidenta izhajata dve spoznanji. Prvič, raje uporabljajte Low() in High() namesto dobesednih meja, kadar tip polja že po zasnovi nima začetka pri 0. Drugi korak: prevod s prevajalnikom FPC ali vsaj ena gradnja v Delphiju z omogočeno direktivo {$R+} mora biti obvezno prvo sito za vsako novo predstavitev ali test. Privzete nastavitve dcc32 vas o tej vrsti napak ne bodo obvestile, uspešen zagon programa pa še ni dokaz o njegovi pravilnosti
Dodelitev TBytes, ki jo sprejme le Delphi 13
Na kratko: dodelitev polja, ki je deklarirano kot anonimni array of Byte, spremenljivki tipa TBytes se prevede v Delphiju 13 (različica prevajalnika 37.0), vendar spodleti v Delphiju 12 Athens in vseh starejših različicah z napako E2010 Incompatible types: 'TArray<Byte>' and 'Dynamic array'. Tukaj ne gre za razhajanje med Delphi in FPC, temveč za razhajanje med Delphijem in njegovo lastno preteklostjo, vendar na enak način vpliva na isto kodo z več prevajalniki: najnovejši prevajalnik tiho sprejme konstrukcijo, ki jo vse ostalo zavrne
type
TValidator = class
private
FBuffer: array of Byte; // anonymous dynamic array type
end;
var
OrigBytes: TBytes;
begin
OrigBytes := FBuffer; // Delphi 13 only; E2010 on Delphi 12
// Athens and earlier
OrigBytes := TBytes(FBuffer); // compiles everywhere; same byte layout,
// safe hard cast
end;
To smo poslali v validacijski rutini, razviti in lokalno preizkušeni v Delphiju 13, kjer je bila implicitna pretvorba tiho sprejeta. Namestitveni program s celotno izvorno kodo pa služi velikemu številu uporabnikov na Delphiju 12 in starejših različicah, pri njih pa se enota preprosto ni prevedla. Strukturni popravek je bodisi eksplicitna pretvorba, prikazana zgoraj (ki je varna, saj anonimni array of Byte in TBytes delita enak zapis dinamičnega polja), ali še bolje, deklaracija polja kot poimenovanega tipa TBytes že na samem začetku, s čimer se izognete vsaki pretvorbi. Popravek procesa pa je pomembnejši: konstrukcija, ki se prevede na vašem najnovejšem prevajalniku, ne dokazuje ničesar o starejših različicah, ki jih dejansko uporabljajo vaši uporabniki. Ta razred regresij je neviden, dokler ne zgradite projekta na vseh podprtih različicah. Naši skripti za izdajo zdaj prevajajo knjižnico na celotni matriki prevajalnikov prav zato, ker lokalna gradnja 37.0 ne more ujeti popustljivosti, ki je specifična le za različico 13
Bajt AnsiString, ki izgine na kitajskem sistemu Windows
Na kratko: spajanje surovega bajta z vrednostjo $80 ali več v AnsiString z uporabo operatorja + lahko v Delphiju tiho zamenja ta bajt z vprašajem ? ($3F), saj izraz izvede implicitno pretvorbo iz AnsiString v UnicodeString in nazaj v AnsiString prek sistemske kodne tabele. To smo ugotovili pri testu PDF/A, ki ustvari ime z izoliranim bajtom $FE (ki nikoli ni veljaven vodilni bajt UTF-8), da bi preverili, ali validator označi imena, ki niso veljaven UTF-8 v skladu s standardom ISO 19005-2, točka 6.1.8
var
BadName: AnsiString;
begin
// On Delphi with a multi-byte system code page (observed on CP936),
// the concatenation round-trips through UnicodeString and $FE, which
// is not a valid CP936 sequence, comes back as '?' ($3F)
BadName := '/Bad' + AnsiChar($FE) + 'Name';
// Safe: build with an ASCII placeholder, then patch the byte in place;
// indexed assignment into a settled AnsiString does not round-trip
BadName := '/Bad' + #1 + 'Name';
BadName[5] := AnsiChar($FE);
end;
Na kitajskem sistemu Windows s kodno tabelo 936 speti niz sploh ni vseboval bajta $FE, zato knjižnica pravilno ni sporočila ničesar, test pa je postal rdeč in je bil videti kot napaka knjižnice. Knjižnica ni bila nikoli napačna: testni nabor pod FPC, ki je dobil PDF z dejanskim bajtom $FE, je prejel pričakovano zastavico. Do poškodbe je prišlo znotraj Delphijeve testne izvršljive datoteke med ocenjevanjem izrazov nizov, saj Delphijev Unicode model nizov pretvori mešane izraze AnsiString prek tipa UnicodeString, $FE pa ni veljaven vodilni bajt v CP936, zato ga krožna pretvorba zamenja. Bodite iskreni glede meja: na enobajtnih zahodnih kodnih tabelah (kot je CP1252) tak izraz običajno preživi, zato se ta napaka skrije na večini razvojnih strojev in se pojavi le na vzhodnoazijskih sistemih ali lokaliziranih CI delavcih. Pravilo, ki smo ga sprejeli: nikoli ne gradite binarnih testnih vektorjev, ki vsebujejo bajte z vrednostjo $80 ali več, prek spajanja nizov AnsiString; bajte bodisi popravite na mestu, ko je niz že določen (kot je prikazano zgoraj), ali pa vektor že na začetku zgradite v tipu TBytes
Kaj mora delovni tok z dvema prevajalnikoma privzeto preverjati
Štiri pasti, en vzorec: vsak prevajalnik vas obvesti o drugačnem podnaboru vaših napak. FPC-jevo preverjanje obsega ob prevajanju je ujelo indeks zunaj meja, ki ga je dcc32 tiho izvajal mesece, dcc32-jev model nizov Unicode pa je razkril odvisnost od kodne tabele, ki je čist bajtno usmerjen FPC prevod nikoli ne sproži. Praktična posledica je, da noben samostojen zeleni cevovod ni dovolj. Cross-compiling ni le portability kljukica, ampak predstavlja drugi statični analizator in drugi izvajalni model nad istim virom, v enakem duhu kot obrambno preverjanje meja v članku o utrjevanju ABI in pomnilniške varnosti
Stalna pravila, ki so izšla iz teh incidentov, so dovolj kratka, da si jih zapomnite. Začasne zapise (funkcijske rezultate) pred branjem polj dodelite lokalni spremenljivki. Polja s fiksnimi mejami prehodite z uporabo Low() in High() ter pred zaupanjem kodi zaženite vsaj eno gradnjo s preverjanjem obsega ali gradnjo FPC. Eksplicitno pretvorite anonimna polja dinamičnih polj ali pa jih deklarirajte s poimenovanimi tipi, pred izdajo pa zgradite celotno matriko prevajalnikov. Surove visoke bajte popolnoma izključite iz spajanja nizov AnsiString. Ko te stvari postanejo navada, ne zahtevajo opaznega truda, vsaka od njih pa zapre način neuspeha, ki ga delovni tok z enim samim prevajalnikom strukturno ne more videti
Vse štiri težave so bile najdene in odpravljene med vzdrževanjem komponente PDFium Component, ki prinaša isto izvorno kodo za Delphi, C++Builder in FPC/Lazarus ter poganja svoje teste skladnosti in regresije na vseh teh prevajalnikih, tako da so pasti v tem članku varovane s testi in ne s spominom