Sama Object Pascal -lähdekoodi voi käyttäytyä eri tavalla Delphin ja FPC/Lazaruksen alla neljällä tavalla, jotka toistuvasti haittaavat PDFium-komponentin koodia: FPC vapauttaa funktion palauttamat väliaikaiset tietueet (record temporaries) ennen kuin in-jäsenyystesti on ehtinyt lukea ne loppuun. dcc32 toimitetaan aluetarkistus pois päältä, joten rajojen ulkopuoliset taulukon indeksit lukevat hiljaa roskatietoa. Vain Delphi 13 hyväksyy nimettömän array of Byte -taulukon sijoittamisen TBytes-muuttujaan ilman tyyppimuunnosta. Ja Delphin AnsiString-yhdistäminen (concatenation) voi hävittää tavuja arvolla $80 tai yli piilotetun koodisivujen edestakaisen muunnoksen (round-trip) kautta. Kukin näistä tuottaa testisarjan, joka on vihreä yhdellä kääntäjällä ja punainen — tai mikä pahempaa, hiljaa väärä — toisella kääntäjällä
Jos olet perustamassa kahden kääntäjän projektia ensimmäistä kertaa, Lazarus- ja FPC-katseluohjelman läpikäynti kattaa peruspolun: paketit, hakupolut ja piirtoikkunan saamisen näytölle. Tämä artikkeli on opassovelluksen vastakohta. Se on luettelo asioista, joihin törmäsimme peruspolun toimiessa — kun CI oli vihreänä FPC:n alla, vihreänä Delphin alla, ja sitten toisella puolella läpi mennyt muutos räjähti toisella. Jokainen alla oleva sudenkuoppa on peräisin todellisesta virheestä PDFiumPas-testisarjassa tai sen demoissa, ja commit-tason jäljitys on tiivistetty minimaaliseksi toisinnoksi sisältäen perussyyn ja standardoimamme korjauksen
Miksi joukko (set) palautuu tyhjänä FPC:n alla, mutta ei Delphissä?
Yhden lauseen selitys: FPC voi purkaa funktion tietuepalautusta pitelevän väliaikaisen muuttujan ennen kuin sitä lukeva lauseke on valmis. Siten X in Func().Issues voi testata jäsenyyttä jo vapautettua joukkoa vasten, kun taas vastaava Delphi-lauseke toimii. PDF/E-yhteensopivuustestimme törmäsivät tähän ensimmäisessä versiossaan. Validaattori palauttaa tietueen, jonka Issues-kenttä on rikkomuslippujen joukko, ja testiväitteet (assertions) kutsuivat funktiota suoraan lausekkeen sisällä
// Epäluotettava FPC:n alla: funktion palauttama väliaikainen tietue
// voidaan vapauttaa ennen kuin 'in'-testi lukee Issues-arvon
AssertTrue(pveiLzwUsed in ValidateAnsi(Pdf).Issues);
// Luotettava molemmilla kääntäjillä: kiinnitä tulos ensin paikalliseen muuttujaan
var
Vr: TPdfEValidationResult;
begin
Vr := ValidateAnsi(Pdf);
AssertTrue(pveiLzwUsed in Vr.Issues);
end;
Lausekkeen sisäinen muoto luki joukon tyhjänä FPC-käännöksessä, joten jokainen lippua odottanut assertion epäonnistui, kun taas identtinen Delphi-käännös meni läpi. Perussyy on kääntäjien välinen ero siinä, miten ne hallitsevat funktion palauttamien väliaikaisten tietueiden elinkaarta laajempien lausekkeiden sisällä: Delphi pitää väliaikaisen muuttujan elossa lausekkeen loppuun asti, kun taas FPC:n tekemä tietueen vapautus voi kilpailla sitä vielä lukevan joukkojäsenyysoperaattorin kanssa. Olimme ja kertaalleen dokumentoineet saman käyttäytymisen kommenteissa PDF/A-testiyksikön FlagPresent-apufunktiossa, mutta toimme virheen silti uudelleen kirjoittaessamme uusia testejä alusta alkaen, mikä kertoo siitä, kuinka luonnolliselta rikkinäinen muoto näyttää. Korjaus on mekaaninen ja se kannattaa ottaa yleissäännöksi: älä koskaan ketjuta kentän lukemista tai joukkotestiä suoraan tietueen palauttavaan funktio kutsuun, vaan sijoita tulos ensin paikalliseen muuttujaan ja lue kenttä vasta sen jälkeen. Se maksaa yhden rivin ja poistaa kokonaisen kääntäjäriippuvaisen epävakauden luokan
Miksi Delphi hyväksyy taulukon indeksin, jota FPC kieltäytyy kääntämästä?
Yhden lauseen selitys: dcc32 kääntää rajojen ulkopuolisen indeksin kiinteärajaisessa taulukossa ja lukee tai kirjoittaa suoritusaikana hiljaa viereistä muistia ilman virheilmoitusta (koska aluetarkistus on oletuksena pois päältä), kun taas FPC hylkää saman indeksin jo käännösaikana. PDFium-komponentti ilmoittaa nelikulmion pisteet 1-pohjaisena taulukkona TQuadrilateralPoint = array [1..4] of TPdfPoint vastaten sitä, miten PDF:n QuadPoints-tietueet yleensä numeroidaan. Esittelykoodi, joka täytti sen nollapohjaisella silmukalla, toimi kuukausia Delphin alla
var
I: Integer;
begin
for I := 0 to 3 do // väärin: taulukko on [1..4]
Data.AttachmentPoints[I] := Corner[I]; // dcc32-oletus: kääntyy, indeksi 0
// koskettaa hiljaa viereistä muistia
// FPC: käännösaikainen aluetarkistusvirhe
for I := Low(TQuadrilateralPoint) to High(TQuadrilateralPoint) do
Data.AttachmentPoints[I] := Corner[I - 1]; // oikein molemmilla kääntäjillä
end;
Delphi-käännös antoi väärän positiivisen tuloksen: aluetarkistuksen ollessa pois päältä (mikä on dcc32:n oletus) indeksi 0 osui kenttään, joka sijaitsi tietueessa juuri ennen taulukkoa, ja demo näytti toimivan. Saman demon siirtäminen Lazarukseen tuotti välittömästi käännösaikaisen aluetarkistusvirheen FPC:ltä. Indeksin korjaaminen paljasti toisen, syvemmän virheen kirjaston huomautuspolussa, jonka roskaluvut olivat peittäneet. Tätä virhettä käsitellään nelikulmiopisteiden merkintähuomautuksia käsittelevässä artikkelissa. Tapauksesta saatiin kaksi opetusta. Ensinnäkin suosi Low()- ja High()-funktioita kirjaimellisten rajojen sijaan aina, kun taulukon tyyppi ei ole rakenteellisesti nollapohjainen. Toiseksi kohtele FPC-kääntämistä tai vähintään yhtä Delphi-käännöstä {$R+}-direktiivillä pakollisena testinä jokaiselle uudelle demolle tai testille: dcc32:n oletukset eivät kerro tämän luokan virheistä, eikä toimiva ohjelma ole todiste siitä, että se on oikein
TBytes-sijoitus, jonka vain Delphi 13 hyväksyy
Yhden lauseen selitys: nimettömäksi array of Byte -taulukoksi määritellyn kentän sijoittaminen TBytes-muuttujaan kääntyy Delphi 13:ssa (kääntäjäversio 37.0), mutta epäonnistuu Delphi 12 Athensissa ja kaikissa aiemmissa versioissa virheellä E2010 Incompatible types: 'TArray<Byte>' and 'Dynamic array'. Tässä ei ole kyse Delphi vs FPC -erosta vaan pikemminkin Delphin erosta omaan historiaansa. Se kuitenkin koettelee samaa monikääntäjäkoodipohjaa samalla tavalla: uusin kääntäjä hyväksyy hiljaa rakenteen, jonka kaikki muut hylkäävät
type
TValidator = class
private
FBuffer: array of Byte; // nimetön dynaaminen taulukkotyyppi
end;
var
OrigBytes: TBytes;
begin
OrigBytes := FBuffer; // Vain Delphi 13; E2010 Delphi 12
// Athensissa ja aiemmissa
OrigBytes := TBytes(FBuffer); // kääntyy kaikkialla; sama tavuasettelu
// turvallinen kova tyyppimuunnos
end;
Toimitimme täsmälleen tämän validaatiorutiinissa, joka kehitettiin ja testattiin paikallisesti Delphi 13:ssa, jossa implisiittinen muunnos hyväksyttiin hiljaa. Lähdekoodiasennus palvelee kuitenkin suuria määriä Delphi 12:n ja vanhempien versioiden käyttäjiä, ja heillä yksikkö ei yksinkertaisesti kääntynyt. Rakenteellinen korjaus on joko yllä esitetty kova tyyppimuunnos — mikä on turvallista, koska nimetön array of Byte ja TBytes jakavat saman dynaamisen taulukon rakenteen — tai mieluummin kentän määrittely suoraan nimettynä tyyppinä kuten TBytes, jotta muunnosta ei koskaan tarvita. Prosessitason korjaus on tärkeämpi: uusimmalla työkaluketjulla kääntyvä rakenne ei todista mitään vanhemmista kääntäjistä, joita käyttäjäsi todellisuudessa ajavat. Tämän tyyppinen regressio on näkymätön, kunnes käännät koodin kaikkia tuettuja versioita vasten. Julkaisuskriptimme kääntävät nyt kirjaston koko kääntäjämatriisissa juuri siksi, ettei paikallinen 37.0-käännös voi havaita vain 13-versiossa sallittua väljyyttä
AnsiString-tavu, joka katoaa kiinalaisessa Windows-koneessa
Yhden lauseen selitys: raa'an tavun arvolla $80 tai yli yhdistäminen AnsiString-muuttujaan +-operaattorilla voi korvata kyseisen tavun hiljaa kysymysmerkillä ? ($3F) Delphin alla. Tämä johtuu siitä, että lauseke tekee implisiittisen AnsiString → UnicodeString → AnsiString -edestakaisinmuunnoksen järjestelmän koodisivun kautta. Havaitsimme tämän PDF/A-testissä, joka rakentaa nimen sisältäen yksittäisen $FE-tavun (joka ei koskaan ole laillinen UTF-8-alkutavu) varmistaakseen, että validaattori merkitsee nimet, jotka eivät ole standardin ISO 19005-2 kohdan 6.1.8 mukaisesti kelvollista UTF-8-koodia
var
BadName: AnsiString;
begin
// Delphissä monen tavun järjestelmäkoodisivulla (havaittu CP936:lla)
// yhdistäminen muuntuu UnicodeStringin kautta ja $FE, joka
// ei ole kelvollinen CP936-sarja, palautuu merkkinä '?' ($3F)
BadName := '/Bad' + AnsiChar($FE) + 'Name';
// Turvallinen: rakenna ASCII-paikkamerkillä ja paikkaa tavu sitten paikoilleen
// indeksoitu sijoitus valmiiseen AnsiString-merkkijonoon ei tee edestakaisinmuunnosta
BadName := '/Bad' + #1 + 'Name';
BadName[5] := AnsiChar($FE);
end;
Kiinalaisessa Windows-järjestelmässä, joka käyttää koodisivua 936, yhdistetty merkkijono ei koskaan sisältänyt $FE-tavua. Kirjasto raportoi siis aivan oikein ei-mitään, ja testi muuttui punaiseksi näyttäen kirjastovirheeltä. Kirjasto ei kuitenkaan ollut väärässä: FPC-ympäristö, joka syötti todella $FE-tavun sisältävän PDF:n, sai odotetun lipun. Korruptio tapahtui Delphin testiohjelman sisällä, kun merkkijonolauseketta arvioitiin, koska Delphin Unicode-ensisijainen merkkijonomalli muuntaa sekamuotoiset AnsiString-lausekkeet UnicodeStringin kautta. Tavu $FE ei ole kelvollinen alkutavu CP936:ssa, joten edestakaisinmuunnos korvaa sen. Rajat on syytä todeta suoraan: yksitavuisella länsimaisella koodisivulla, kuten CP1252:lla, sama lauseke yleensä selviää, minkä vuoksi tämä virhe piilee useimmissa kehityskoneissa ja tulee esiin vain itäaasialaisissa järjestelmissä tai lokalisoiduissa CI-ajureissa. Omakohtainen sääntömme: älä koskaan rakenna binäärisiä testivektoreita, jotka sisältävät tavuja arvolla $80 tai yli, AnsiString-yhdistämisen kautta. Joko paikkaa tavut paikoilleen merkkijonon valmistuttua (kuten yllä) tai rakenna vektori TBytes-muodossa alusta alkaen
Mitä kahden kääntäjän työnkulun tulisi tarkistaa oletuksena
Neljä sudenkuoppaa, yksi kaava: kukin kääntäjä kertoo eri alijoukosta virheitäsi. FPC:n käännösaikainen aluetarkistus löysi rajojen ulkopuolisen indeksin, jota dcc32 suoritti hiljaa kuukausia, ja dcc32:n Unicode-merkkijonomalli paljasti koodisivuriippuvuuden, jota puhdas tavupohjainen FPC-käännös ei koskaan laukaise. Käytännön seuraus on se, ettei kumpikaan vihreä putki yksin riitä. Ristiinkääntäminen ei ole pelkkä siirrettävyyden valintaruutu, se on toinen staattinen analysaattori ja toinen suoritusaikainen malli samalle lähdekoodille — samassa hengessä kuin puolustukselliset rajatarkistukset, joita käsitellään ABI:n ja muistinhallinnan suojausta käsittelevässä artikkelissa
Näistä tapauksista syntyneet pysyvät säännöt ovat riittävän lyhyitä muistettavaksi. Kiinnitä funktion palauttamat tietueet paikalliseen muuttujaan ennen kenttien lukemista. Käy läpi kiinteärajaiset taulukot Low()- ja High()-funktioilla, ja aja vähintään yksi aluetarkistettu tai FPC-käännös ennen kuin luotat uuteen demoon. Tee tyyppimuunnos nimettömille dynaamisille taulukoille nimenomaisesti tai määrittele ne nimetyillä tyypeillä, ja käännä koko kääntäjämatriisi ennen julkaisua. Pidä raa'at suuret tavut kokonaan poissa AnsiString-yhdistämisestä. Mikään näistä ei vaadi mitattavaa vaivaa, kun ne ovat tulleet tavoiksi, ja jokainen niistä sulkee vikatilan, jota yhden kääntäjän työnkulku ei rakenteellisesti voi havaita
Kaikki neljä ongelmaa löydettiin ja korjattiin PDFium Component -kirjaston ylläpidon yhteydessä, joka toimittaa saman Object Pascal -lähdekoodin Delphille, C++Builderille ja FPC/Lazarukselle ja ajaa sen yhteensopivuus- ja regressiotestit kaikissa näissä työkaluketjuissa. Siten tämän artikkelin sudenkuopat ovat testien, eivät pelkän muistin suojassa