Povezava Pascal nad knjižnico C se bere kot običajen Pascal. Pokličete metodo, dobite zapis nazaj, sprostite tisto, kar ste dodelili. Težava je v tem, da je PDFium knjižnica C in C++ z lastno konvencijo klicanja, lastnimi širinami celih števil in lastnimi pravili o tem, kdo si lasti pomnilnik in kdo ga sprosti. Nič od tega ne prečka jezikovne meje samo po sebi. Vsako od teh pogodb je treba ročno ponoviti v deklaracijah Pascal, ena sama napačna beseda pa spremeni čist klic v poškodbo sklada, skrajšan odmik ali dvojno sprostitev pomnilnika. Revizija povezave PDFium VCL različice v1.61.0 je razkrila po eno napako vsake vrste. Vredno jih je preučiti, saj niso specifične za to povezavo. Predstavljajo stalne nevarnosti ovijanja katerega koli vmesnika API za C v okolju Delphi ali Lazarus.
cdecl je del tipa funkcije, ne okras
PDFium je preveden C. Na platformi Win32 njegovi izvozi in, kar je še pomembneje, povratni klici, ki jih sproži, uporabljajo konvencijo klicanja cdecl. Pri cdecl klicatelj počisti sklad po vrnitvi klica. Privzeta nastavitev za Delphi je register, standard Win32 C za povratne klice pa je v nekaterih knjižnicah stdcall, kjer namesto tega sklad počisti klicani program. Ko struktura preda PDFium funkcijski kazalec in pozabite na cdecl pri tipu tega kazalca, se strani ne strinjata o tem, kdo prilagodi kazalec sklada. Ali ga popravita oba ali pa nobeden, kazalec sklada pa se ob vsakem klicu zamakne za velikost argumentov.
Razlog, zakaj je to napako težko najti, je v tem, da škoda ni lokalna. Pokvarjen klic se vrne in zdi se v redu. Neusklajenost se pokaže pozneje, v neki nepovezani funkciji, katere okvir zdaj sedi na kazalcu sklada, ki je zamaknjen za nekaj bajtov. To se kaže kot naključno branje, napačen povratni naslov ali sesutje s sledenjem nazaj, ki ne kaže nikamor blizu povratnega klica, ki ste ga dejansko napačno nastavili. Izpolnjevanje obrazcev je klasično mesto, kjer se to zgodi, saj je vmesnik za izpolnjevanje obrazcev zapis, poln povratnih klicev, v katere PDFium sliši nazaj. Eden izmed njih, FFI_OpenFile, preda PDFium funkcijo, ki jo bo poklical za odpiranje zunanje datoteke, deklarirano kot function(pThis: PFPDF_FORMFILLINFO; fileFlag: Integer; wsURL: FPDF_WIDESTRING; mode: PAnsiChar): PFPDF_FILEHANDLER; cdecl. Končni cdecl je podrobnost, ki jo je vredno prekopirati. Če jo izpustite, se koda še vedno prevede, poveže in deluje vse do trenutka, ko PDFium pokliče to funkcijo. Konvencija pripada samemu tipu funkcije. Ni neobvezen okras in prevajalnik vas ne bo opozoril, ko manjka, saj je preprost funkcijski tip popolnoma zakonit tip v jeziku Pascal. Edina obramba je, da konvencijo klicanja obravnavate kot obvezno polje vsake uvožene signature in vsakega povratnega klica, ki ga posredujete navzven.
size_t je širine kazalca, na FPC Win64 pa to pomeni 64 bitov
Druga napaka je neujemanje širine celih števil, ki se pojavi le na enem cilju. Tip size_t v jeziku C je definiran tako, da je dovolj širok, da zadrži katero koli velikost objekta, kar na 64-bitni platformi pomeni 64-bitno nepredznačeno celo število. Vmesniki PDFium za progresivno nalaganje uporabljajo bajtne odmike tipa size_t. Zapis ponudnika razpoložljivosti FX_FILEAVAIL vsebuje povratni klic IsDataAvail, ki ga PDFium pokliče z odmikom in velikostjo, povratni klic AddSegment v zapisu FX_DOWNLOADHINTS pa prejme isto. Oba parametra sta tipa size_t.
IsDataAvail = function(
pThis : PFX_FILEAVAIL;
offset, size: size_t): FPDF_BOOL; cdecl;
AddSegment = procedure(
pThis : PFX_DOWNLOADHINTS;
offset, size: size_t); cdecl;
Če te odmike deklarirate kot 32-bitni tip, povezava deluje na Win32 in na Delphi Win64, nato pa tiho spodleti na FPC in Lazarus Win64. Vzrok je subtilen. Na FPC Win64 je NativeUInt pristni 64-bitni tip s širino kazalca, size_t pa je njegov vzdevek. Povezava vsebuje opombo v sekciji tipov, ki opozarja prav pred senčenjem NativeUInt na FPC, saj bi ponovna definicija v 32-bitni vzdevek tam prisilila size_t na 32 bitov in pokvarila vsak parameter size_t, ki se prenese v knjižnico ali iz nje. A 64-bit offset arriving at a 32-bit parameter loses its top half. Za majhno datoteko se vsak odmik prilega 32 bitom in ni nič narobe. Za veliko datoteko pa v trenutku, ko odmik prečka mejo štirih gigabajtov, skrajšana vrednost kaže povsem drugam, PDFium vpraša, ali je na voljo napačno območje bajtov, progresivno nalaganje pa se zaustavi ali bere smeti. Napaka je nevidna, dokler datoteka ni dovolj velika in je cilj tisti, kjer se je size_t dejansko razširil.
Izjema Pascal se ne sme nikoli odviti skozi okvir C
Tretji razred govori o modelu izjem, ki ga C nima. Ko PDFium pokliče enega od vaših povratnih klicev, vaša koda Pascal se izvaja znotraj sklada okvirjev C in C++, ki ne vedo ničesar o Delphijevem mehanizmu izjem. Če vaš povratni klic sproži izjemo in ji pusti, da se širi, se ta odvije skozi okvirje, ki niso bili nikoli zgrajeni za odvijanje. PDFiumovo lastno čiščenje se ne izvede, njegovi notranji invariantni podatki ostanejo polovično posodobljeni in proces je zdaj v stanju, ki ga knjižnica ni nikoli predvidela. Pogodba za te povratne klice je povratna koda, ne izjema.
Dva povratna klica to naredita konkretno. FPDF_FILEWRITE je ponor, v katerega PDFium zapiše shranjen dokument, FPDF_FILEACCESS pa je vir, iz katerega bere vhodni dokument. Oba sta tukaj implementirana nad Delphi razredom TStream in oba lahko spodletita tako, kot spodleti vsak tok: disk se napolni, tok se zapre pod vami, branje poteka čez konec. Povratni klic za zapisovanje ovije svoj tok pisanja in pretvori vsak neuspeh v PDFiumovo kodo neuspeha, namesto da bi ji pustil pobegniti.
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;
Bralna stran stori enako: neuspešno branje sporoči ničlo, da ustreza pogodbi FPDF_FILEACCESS, namesto da bi sprožilo izjemo čez mejo. Gol except brez ponovnega sproženja se zdi napačen programerju Pascala, ki je naučen, da nikoli ne golta izjem, in v običajnem Pascalu je to res napačno. Na meji ABI pa je to pravilna oblika, saj je edina varna vrednost, ki jo je mogoče vrniti klicatelju v jeziku C, statusna koda, ki jo zna interpretirati. Neuspeh se še vedno širi, vendar prek povratne vrednosti, klicna koda nad knjižnico pa jo prikaže kot EPdfError, ko je nadzor spet na strani Pascala.
Dvojna sprostitev se skriva na poti napake
Četrta napaka je lastništvo. Ročico dokumenta PDFium odpre knjižnica in jo je treba zapreti natanko enkrat z metodo FPDF_CloseDocument. Nevarnost je pot napake, ki sprosti ročico, ki jo ima v lasti tudi drugo čiščenje. Predstavljajte si rutino, ki ustvari objekt ovoja, mu dodeli sveže odprto ročico dokumenta in nato izvede več nastavitev, ki lahko spodletijo. Če nastavitve sprožijo izjemo, bo rokovalnik za predčasno vrnitev, ki pokliče FPDF_CloseDocument na surovei ročici, to zaprl, nato pa jo bo lastni destruktor objekta ovoja ponovno zaprl, ko bo objekt sproščen. Ročica se sprosti dvakrat, kar je nedoločeno obnašanje in verjetno sesutje.
Revizija je to ugotovila na uvozni poti v slogu postavitve (imposition), ki zgradi TPdf okoli že odprte ročice. Popravek je v tem, da prenos lastništva postane edini vir resnice. Ko je ročica dodeljena polju ovoja, jo ima ovoj v lasti, edino čiščenje na poti napake pa je sprostitev ovoja. Destruktor ovoja pokliče FPDF_CloseDocument namesto vas, zato bi drugo eksplicitno zapiranje povzročilo dvojno sprostitev istega dokumenta. Popravljen rokovalnik napak sprosti objekt in ponovno sproži izjemo, pri čemer obstaja natanko ena pot do zapiranja.
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;
Upravljani zapisi in knjižnica, polna izvozov, potrebujeta eksplicitno razgradnjo
Zadnji razred govori o pomnilniku, ki ga prevajalnik upravlja v vašem imenu, navada iz jezika C na tem področju pa ga lahko tiho pokvari. Številne pomožne funkcije te povezave vrnejo zapis, ki vsebuje WideString ali dinamično polje. To so polja s štetjem referenc in prevajalnik odda skrito knjigovodstvo za vzdrževanje njihovih števil. Nagon, prenesen iz C, je čiščenje novega zapisa s FillChar(Result, SizeOf(Result), 0). To prepiše z ničlami upravljano referenco znotraj zapisa brez predhodnega zmanjšanja števca. Prevajalnik ponovno uporabi eno skrito začasno spremenljivko za rezultat funkcije skozi iteracije zanke, tako da v drugi iteraciji FillChar prepiše živi kazalec niza, ki ni bil nikoli sproščen, in niz, na katerega je kazal, pušča. Pokličite to funkcijo v zanki čez tisoč pripomb in izpustili boste tisoč nizov.
Rešitev je, da pustite jeziku, da počisti zapis tako, kot sam ve, z Default(T), kar sprosti katero koli upravljano polje pred ponastavitvijo na ničlo.
// 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);
Soroden problem lastništva živi na meji nalaganja knjižnice. Ta povezava razreši več sto funkcijskih kazalcev iz knjižnice PDFium DLL z GetProcAddress po LoadLibrary. Če manjka en zahtevan izvoz, je delno povezano stanje nevarno: na desetine kazalcev je veljavnih, ostali so nil ali zastareli, vsak kasnejši klic prek enega od njih pa skoči v modul, ki je morda že razbremenjen. Povezava to rešuje tako, da razbremeni knjižnico in zažene celoten klic ClearAllBindings, ki ponastavi vsak uvožen kazalec nazaj na nil, kadar koli se zahtevan izvoz ne razreši. Po tem noben funkcijski kazalec ne visi v razbremenjen modul, kasnejši klic pa čisto spodleti s preverjanjem kazalca nil, namesto da bi se preusmeril v sproščeno kodo.
Ovoj je mesto, kjer se štiri pogodbe ročno ponovijo
Nobena od teh petih napak ni eksotična. So predvidljivi načini neuspeha tanke plasti Pascal nad vmesnikom API za C in se kopičijo zato, ker je ta plast natanko tisto mesto, kjer je treba ponovno deklarirati štiri ločene pogodbe. Konvencijo klicanja je treba črkovati kot cdecl pri vsakem povratnem klicu. Širina celih števil se mora ujemati s size_t na tistem cilju, kjer se dejansko razširi. Model izjem je treba pretvoriti v povratne kode pri vsakem povratnem klicu, ki prečka meje Pascala. Lastništvo vsake ročice in vsakega upravljanega polja mora biti navedeno enkrat in upoštevano na vsaki poti, vključno s potmi napak, ki jih nihče ne preizkusi do produkcije. Izpustite katerega koli in prejeli boste napako, katere simptom se pojavi daleč od svojega vzroka, kar dela to kategorijo drago. Vrednost revizije je bila manj v posameznem popravku kot v obravnavi vsakega od teh kot lastne discipline za preverjanje celotne povezave.
Če želite videti, kako povezava opravlja resnično delo, namesto da le varuje svoje robove, tehnike predpomnilnika upodabljanja in povečave v našem zapisu o predpomnilniku upodabljanja in zmogljivosti povečave prikazujejo pot upodabljanja, vodnik za večplatformni prevajalnik pri izgradnji pregledovalnika za Lazarus in FPC pa je mesto, kjer dejansko šteje vedenje Win64 size_t, opisano tukaj. Obe gradita na enakem delu za varnost pomnilnika in ABI, ki se dostavlja v komponenti PDFium Component za Delphi, Lazarus in C++Builder, skupaj z vmesniki API za upodabljanje, ekstrakcijo besedila in obrazce, ki so obravnavani drugje na tem blogu.