Pascal susiejimas C bibliotekai skaitomas kaip įprastas Pascal kodas. Iškviečiate metodą, gaunate įrašą atgal, atlaisvinate tai, ką priskyrėte. Bėda ta, kad PDFium yra C ir C++ biblioteka su sava iškvietimo konvencija, savo sveikųjų skaičių pločiais bei taisyklėmis, kas valdo atmintį ir kas ją atlaisvina. Niekas ausis nekerta kalbos ribos. Kiekviena iš šių sutarčių turi būti rankiniu būdu deklaruota Pascal aprašuose, o vienas neteisingas žodis paverčia tvarkingai atrodantį iškvietimą dėklo sugadinimu, sutrumpintu poslinkiu arba dvigubu atlaisvinimu (double free). v1.61.0 versijos PDFium VCL susiejimo auditas atskleidė po vieną kiekvienos rūšies defektą. Juos verta apžvelgti, nes jie būdingi ne tik šiam susiejimui – tai yra nuolatiniai pavojai, kylantys apgaubiant bet kurį C API naudojant Delphi arba Lazarus.
cdecl yra funkcijos tipo dalis, o ne dekoracija
PDFium yra sukompiliuotas C kodas. Win32 platformoje jo eksportuojamos funkcijos ir, dar svarbiau, jo iškviečiami atgaliniai ryšiai (callbacks) naudoja cdecl iškvietimo konvenciją. Naudojant cdecl, iškvietėjas sutvarko dėklą po to, kai funkcija grąžina rezultatą. Delphi numatytoji konvencija yra register, o Win32 C standartas atgaliniams iškvietimams kai kuriose bibliotekose yra stdcall, kur dėklą sutvarko pati iškviestoji funkcija. Kai struktūra perduoda PDFium funkcijos rodiklį (pointer) ir pamirštate nurodyti cdecl to rodiklio tipe, abi pusės nesutaria, kas turėtų koreguoti dėklo rodiklį. Abu tai daro arba nei vienas iš jų, todėl dėklo rodiklis nukrypsta per argumentų dydį kiekvieno iškvietimo metu.
Priežastis, kodėl šį defektą sunku rasti, yra ta, kad žala pasireiškia ne iškvietimo vietoje. Sugadintas iškvietimas grąžina rezultatą ir viskas atrodo gerai. Neatitikimas išryškėja vėliau, kitoje nesusijusioje funkcijoje, kurios rėmas dabar yra ant dėklo rodiklio, kuris nukrypęs keliais baitais, ir tai pasireiškia kaip neteisingas nuskaitymas, blogas grąžinimo adresas arba lūžimas su pėdsaku (backtrace), kuris rodo toli nuo atgalinio iškvietimo, kurį padarėte klaidingai. Formų pildymas yra klasikinė vieta, kur tai nutinka, nes formų pildymo sąsaja yra įrašas, pilnas atgalinių ryšių funkcijų, kurias PDFium iškviečia. Viena iš jų, FFI_OpenFile, perduoda PDFium funkciją, kuri bus iškviečiama išoriniam failui atidaryti, deklaruota kaip function(pThis: PFPDF_FORMFILLINFO; fileFlag: Integer; wsURL: FPDF_WIDESTRING; mode: PAnsiChar): PFPDF_FILEHANDLER; cdecl. Pabaigoje esantis cdecl yra labai svarbus. Pašalinkite jį, ir kodas vis tiek sėkmingai kompiliuosis, susisies bei veiks tol, kol PDFium iškvies šią funkciją. Konvencija priklauso pačiam funkcijos tipui. Tai nėra pasirenkamas dalykas, ir kompiliatorius neįspės jūsų apie jos trūkumą, nes paprastas funkcijos tipas yra visiškai leistinas Pascal tipas. Vienintelė gynyba yra traktuoti iškvietimo konvenciją kaip privalomą kiekvieno priimto parašo ir kiekvieno perduodamo atgalinio ryšio lauką.
size_t yra rodiklio pločio, o FPC Win64 platformoje tai reiškia 64 bitus
Antrasis defektas yra sveikojo skaičiaus pločio neatitikimas, kuris pasireiškia tik vienoje tikslinėje platformoje. C kalbos size_t yra apibrėžtas taip, kad būtų pakankamai platus bet kurio objekto dydžiui išsaugoti, o tai 64 bitų platformoje reiškia 64 bitų nespaustą sveikąjį skaičių be ženklo. PDFium progresyvaus įkėlimo sąsajos naudoja size_t baitų poslinkius. Prieinamumo teikėjo FX_FILEAVAIL įrašas turi IsDataAvail atgalinį iškvietimą, kurį PDFium iškviečia su poslinkiu ir dydžiu, o FX_DOWNLOADHINTS įrašo AddSegment atgalinis iškvietimas gauna tuos pačius parametrus. Abu šie parametrai yra size_t tipo.
IsDataAvail = function(
pThis : PFX_FILEAVAIL;
offset, size: size_t): FPDF_BOOL; cdecl;
AddSegment = procedure(
pThis : PFX_DOWNLOADHINTS;
offset, size: size_t); cdecl;
Jei deklaruosite šiuos poslinkius kaip 32 bitų tipą, susiejimas veiks Win32 ir Delphi Win64 platformose, bet tyliai suges FPC ir Lazarus Win64 aplinkoje. Priežastis yra subtili. FPC Win64 platformoje NativeUInt yra tikras rodiklio pločio 64 bitų tipas, o size_t yra jo slapyvardis. Susiejimo tipo sekcijoje yra komentaras, įspėjantis nedubliuoti NativeUInt FPC aplinkoje, nes pakeitus jį į 32 bitų slapyvardį, size_t būtų priverstas naudoti 32 bitus ir sugadintų kiekvieną size_t parametrą, perduodamą bibliotekai arba jos įrašomą. 64 bitų poslinkis, atvykęs į 32 bitų parametrą, praranda savo viršutinę pusę. Mažam failui kiekvienas poslinkis telpa į 32 bitus ir problemų nekyla. Dideliam failui, kai tik poslinkis peržengia keturių gigabaitų ribą, sutrumpinta reikšmė rodo į visiškai kitą vietą, PDFium klausia, ar prieinamas neteisingas baitų diapazonas, ir progresyvus įkėlimas sustoja arba nuskaito šiukšles. Defektas nematomas, kol failas nėra pakankamai didelis ir platforma nėra ta, kurioje size_t iš tikrųjų išsiplėtė.
Pascal išimtis niekada neturi būti išvyniota per C rėmą
Trečioji klasė yra susijusi su išimčių (exception) modeliu, kurio C kalba neturi. Kai PDFium iškviečia vieną iš jūsų atgalinio ryšio funkcijų, jūsų Pascal kodas veikia C ir C++ rėmų dėkle, kuris nieko nežino apie Delphi išimčių mechanizmą. Jei jūsų atgalinis iškvietimas sukelia išimtį ir leidžia jai plisti, ji išvyniojama per rėmus, kurie niekada nebuvo tam pritaikyti. Paties PDFium išteklių atlaisvinimas nepaleidžiamas, jo vidinės būsenos lieka pusiau atnaujintos, o procesas atsiduria tokioje būsenoje, kurios biblioteka nenumatė. Šių atgalinių iškvietimų sutartis reikalauja grąžinti būsenos kodą, o ne kelti išimtį.
Du atgaliniai iškvietimai tai iliustruoja konkrečiai. FPDF_FILEWRITE yra imtuvas, į kurį PDFium įrašo išsaugotą dokumentą, o FPDF_FILEACCESS yra šaltinis, iš kurio nuskaitomas įvesties dokumentas. Abu jie čia įgyvendinti naudojant Delphi TStream ir abu gali sugesti taip, kaip sugenda bet koks srautas: užsipildo diskas, srautas uždaromas po jumis arba skaitymas peržengia pabaigą. Rašymo iškvietimas apgaubia srauto įrašymą ir paverčia bet kokią nesėkmę PDFium nesėkmės kodu, užuot leidęs jai ištrūkti į išorę.
function WriteBlock(
pThis: PFPDF_FILEWRITE;
pData: Pointer;
Size : LongWord): Integer; cdecl;
begin
// PDFium treats any non-1 return as a write failure. A Pascal exception
// must not unwind through this cdecl/C++ frame, so trap it and report
// failure instead.
Result := 0;
try
PPdfWrite(pThis).Stream.WriteBuffer(pData^, Size);
Result := 1;
except
end;
end;
Skaitymo pusė daro tą patį: nepavykęs skaitymas praneša nulį, kad atitiktų FPDF_FILEACCESS sutartį, užuot kėlęs išimtį per ribą. Paprastas except blokas be pakartotinio išmties kėlimo atrodo klaidingai Pascal programuotojui, išmokytam niekada nepraryti išimčių, ir įprastame Pascal kode tai būtų klaida. Tačiau ties ABI riba tai yra teisinga forma, nes vienintelė saugi reikšmė, kurią galima grąžinti C iškvietėjui, yra būsenos kodas, kurį jis moka interpretuoti. Klaida vis tiek plinta, tik per grąžinamą reikšmę, o iškviečiantis kodas virš bibliotekos pateikia ją kaip EPdfError, kai tik valdymas grįžta į Pascal pusę.
Dvigubas atlaisvinimas slepiasi klaidų apdorojimo kelyje
Ketvirtasis defektas susijęs su nuosavybe (ownership). PDFium dokumento deskriptorius (handle) yra atidaromas bibliotekos ir turi būti uždarytas lygiai vieną kartą naudojant FPDF_CloseDocument. Pavojus kyla klaidų apdorojimo kelyje, kuris atlaisvina deskriptorių, kurį taip pat valdo ir kitas išteklių valymo žingsnis. Įsivaizduokite rutiną, kuri sukuria apvalkalo objektą, priskiria jam naujai atidarytą dokumento deskriptorių ir atlieka kitus nustatymus, kurie gali nepavykti. Jei nustatymas sukelia klaidą, ankstyvo grąžinimo apdorojimo programa, kuri iškviečia FPDF_CloseDocument neapdorotam deskriptoriui, jį uždarys, o po to paties apvalkalo objekto destruktorius vėl jį uždarys, kai objektas bus atlaisvintas. Deskriptorius atlaisvinamas du kartus, kas sukelia neapibrėžtą elgesį ir tikėtiną programos lūžimą.
Auditas tai nustatė puslapių išdėstymo (imposition) stiliaus importavimo kelyje, kuris sukuria TPdf objektą aplink jau atidarytą deskriptorių. Sprendimas yra paversti nuosavybės perdavimą vieninteliu teisingumo šaltiniu. Kai tik deskriptorius priskiriamas apvalkalo laukui, apvalkalas jį valdo, o vienintelis išteklių valymas klaidos kelyje yra atlaisvinti apvalkalą. Apvalkalo destruktorius iškviečia FPDF_CloseDocument už jus, todėl antrasis aiškus uždarymas sukeltų to paties PDFium dokumento dvigubą atlaisvinimą. Ištaisyta klaidų apdorojimo programa atlaisvina objektą ir vėl pakelia išimtį, o uždarymas vyksta tik vienu keliu.
Result := TPdf.Create(nil);
try
Result.FDocument := NewDoc; // Result now owns the handle
Result.InitializeFormFill;
Result.ReloadPage;
except
// Result.Free closes the handle. A second FPDF_CloseDocument(NewDoc)
// here would double-free the same PDFium document.
Result.Free;
raise;
end;
Valdomiems įrašams ir bibliotekai, pilnai eksportuojamų funkcijų, reikalingas aiškus išteklių atlaisvinimas
Paskutinė klasė yra susijusi su atmintimi, kurią kompiliatorius valdo jūsų vardu, o C programavimo įpročiai ją tyliai sugadina. Daugelis šio susiejimo pagalbinių funkcijų grąžina įrašą, kuriame yra WideString arba dinaminis masyvas. Tai yra nuorodų skaičiavimo laukai, o kompiliatorius sukuria paslėptą apskaitą jų skaičiui palaikyti. Iš C kalbos atėjęs instinktas yra išvalyti naują įrašą naudojant FillChar(Result, SizeOf(Result), 0). Tai užrašo nulius ant valdomos nuorodos įraše, prieš tai jos nesumažinus. Kompiliatorius pakartotinai naudoja vieną paslėptą laikiną kintamąjį funkcijos rezultatui keliose ciklo iteracijose, todėl antroje iteracijoje FillChar perrašo gyvą eilutės rodiklį, kuris niekada nebuvo atlaisvintas, ir eilutė, į kurią jis rodė, nuteka (leak). Iškvieskite funkciją cikle tūkstančiui anotacijų, ir nutekinsite tūkstantį eilučių.
Sprendimas – leisti kalbai išvalyti įrašą taip, kaip ji moka, naudojant Default(T), kas atlaisvina bet kurį valdomą lauką prieš jį išvalant.
// Default() instead of FillChar: the compiler reuses one hidden temp for
// the function result across loop iterations, so FillChar would zero live
// WideString pointers without releasing them.
Result := Default(TPdfAnnotation);
Susijusi nuosavybės problema egzistuoja bibliotekos įkėlimo riboje. Šis susiejimas išsprendžia kelis šimtus funkcijų rodiklių iš PDFium DLL failo naudodamas GetProcAddress po LoadLibrary. Jei trūksta bent vieno reikiamo eksporto, dalinai susieta būsena yra pavojinga: dešimtys rodiklių yra teisingi, o kiti yra nil arba pasenę, todėl bet koks vėlesnis iškvietimas per vieną iš jų nuves į modulį, kuris jau gali būti iškeltas iš atminties. Susiejimas tai sprendžia iškeldamas biblioteką ir paleisdamas pilną ClearAllBindings, kuris atstato kiekvieną importuotą rodiklį į nil reikšmę, kai nepavyksta išspręsti reikiamo eksporto. Po to jokie funkcijų rodikliai nerodo į iškeltą modulį, o vėlesnis iškvietimas nepavyksta su aiškia nil rodiklio patikra, užuot šakojęsis į atlaisvintą kodą.
Apvalkalas yra vieta, kur rankiniu būdu iš naujo deklaruojamos keturios sutartys
Nė vienas iš šių penkių defektų nėra egzotiškas. Tai yra nuspėjami plono Pascal sluoksnio virš C API lūžimo būdai, ir jie telkiasi čia, nes šis sluoksnis yra būtent ta vieta, kur turi būti deklaruotos keturios atskiros sutartys. Iškvietimo konvencija turi būti nurodyta kaip cdecl kiekvienam atgaliniam ryšiui. Sveikojo skaičiaus plotis turi atitikti size_t toje vienoje platformoje, kurioje jis iš tikrųjų išsiplečia. Išimčių modelis turi būti konvertuojamas į grąžinimo kodus kiekviename atgaliniame iškvietime, kuris kerta Pascal ribas. Kiekvieno deskriptoriaus ir valdomo lauko nuosavybė turi būti aiškiai nurodyta bei vykdoma kiekviename kelyje, įskaitant klaidų kelius, kurių niekas netestuoja iki gamybinės aplinkos. Praleiskite bet kurį iš jų, ir gausite defektą, kurio simptomas pasireiškia toli nuo jo priežasties, o tai ir padaro šią kategoriją brangią. Audito vertė buvo ne tiek atskiruose pataisymuose, kiek kiekvieno iš šių aspektų traktavime kaip atskiros disciplinos, kurią reikia tikrinti visame susiejime.
If you want to see the binding doing real work rather than guarding its edges, the render-cache and zoom techniques in our note on render-cache and zoom performance show the rendering path, and the cross-compiler walkthrough in building a Lazarus and FPC viewer is the place the Win64 size_t behavior described here actually matters. Both build on the same memory-safety and ABI work that ships in the PDFium Component for Delphi, Lazarus, and C++Builder, alongside the rendering, text-extraction, and form APIs covered elsewhere on this blog.