Technical Article

U?vr??ivanje PDFium VCL vezanja: ABI i sigurnost memorije

Pascal vezanje preko C knji?nice ?ita se kao obi?an Pascal. Pozovete metodu, dobijete zapis natrag, oslobodite ono ?to ste dodijelili. Nevolja je u tome ?to je PDFium C i C++ knji?nica s vlastitom konvencijom pozivanja, vlastitim ?irinama cijelih brojeva i vlastitim pravilima o tome tko posjeduje memoriju i tko je osloba?a. Ni?ta od toga ne prelazi granicu jezika samo od sebe. Svaki od tih ugovora mora se ru?no ponoviti u deklaracijama Pascala, a jedna pogre?na rije? pretvara ?ist poziv u korupciju stoga, skra?eni pomak ili dvostruko osloba?anje. Revizija v1.61.0 vezanja PDFium VCL otkrila je po jedan nedostatak svake vrste. Vrijedi pro?i kroz njih jer nisu specifi?ni za ovo vezanje. Oni su stalne opasnosti omotavanja bilo kojeg C API-ja u Delphi-ju ili Lazarus-u

cdecl je dio tipa funkcije, a ne ukras

PDFium je kompajlirani C. Na Win32 njegovi izvozi i, ?to je jo? va?nije, povratni pozivi koje poziva koriste konvenciju pozivanja cdecl. Pod cdecl pozivatelj ?isti stog nakon povratka poziva. Nativna zadana vrijednost u Delphi-ju je register, a standard Win32 C za povratne pozive je stdcall u nekim knji?nicama, gdje umjesto toga ?isti pozvani program. Kada struktura preda PDFium-u pokaziva? funkcije, a vi zaboravite cdecl na tipu tog pokaziva?a, dvije se strane ne sla?u oko toga tko prilago?ava pokaziva? stoga. Ili oboje to popravljaju, ili nitko, a pokaziva? stoga pluta za veli?inu argumenata pri svakom pozivu

Razlog za?to je ovaj nedostatak te?ko prona?i jest taj ?to o?te?enje nije lokalno. O?te?eni poziv se vra?a i izgleda u redu. Lo?e poravnanje se pojavljuje kasnije, u nekoj nepovezanoj funkciji ?iji okvir sada sjedi na pokaziva?u stoga koji je pomaknut za nekoliko bajtova, a manifestira se kao divlje ?itanje, lo?a povratna adresa ili ru?enje s pra?enjem unazad koje ne upu?uje nigdje blizu povratnog poziva koji ste zapravo pogrije?ili. Ispunjavanje obrazaca je klasi?no mjesto gdje ovo grize, jer je su?elje za ispunjavanje obrazaca zapis pun povratnih poziva koje PDFium ponovno poziva. Jedan od njih, FFI_OpenFile, predaje PDFium-u funkciju koju ?e pozvati za otvaranje vanjske datoteke, deklariranu kao function(pThis: PFPDF_FORMFILLINFO; fileFlag: Integer; wsURL: FPDF_WIDESTRING; mode: PAnsiChar): PFPDF_FILEHANDLER; cdecl. Zavr?ni cdecl je to?ka koju vrijedi kopirati. Ispustite ga i kod se i dalje kompajlira, i dalje povezuje i radi sve dok PDFium ne pozove funkciju. Konvencija pripada samom tipu funkcije. To nije opcionalni ?e?er, a kompajler vas ne?e upozoriti kada nedostaje jer je obi?an tip funkcije savr?eno legalan Pascal tip. Jedina obrana je tretirati konvenciju pozivanja kao obavezno polje svakog uvezenog potpisa i svakog povratnog poziva koji proslje?ujete prema van

size_t je ?irine pokaziva?a, a na FPC Win64 to zna?i 64 bita

Drugi nedostatak je neuskla?enost ?irine cijelog broja koja se pojavljuje samo na jednoj ciljnoj platformi. C-ov size_t definiran je tako da bude dovoljno ?irok da dr?i bilo koju veli?inu objekta, ?to na 64-bitnoj platformi zna?i 64-bitni neozna?eni cijeli broj. PDFium-ova su?elja za progresivno u?itavanje govore u pomacima bajtova size_t. Zapis FX_FILEAVAIL pru?atelja dostupnosti nosi povratni poziv IsDataAvail koji PDFium poziva s pomakom i veli?inom, a povratni poziv AddSegment zapisa FX_DOWNLOADHINTS prima isto. Oba parametra su size_t

IsDataAvail = function(
  pThis       : PFX_FILEAVAIL;
  offset, size: size_t): FPDF_BOOL; cdecl;

AddSegment = procedure(
  pThis       : PFX_DOWNLOADHINTS;
  offset, size: size_t); cdecl;

Ako te pomake deklarirate kao 32-bitni tip, vezanje radi na Win32 i na Delphi Win64, a zatim tiho puca na FPC i Lazarus Win64. Uzrok je suptilan. Na FPC Win64, NativeUInt je izvorni 64-bitni tip ?irine pokaziva?a, a size_t je njegov alias. Vezanje ima komentar u odjeljku tipova koji upozorava upravo protiv zasjenjivanja NativeUInt na FPC-u, jer redefiniranje na 32-bitni alias tamo prisilio bi size_t na 32 bita i pokvarilo svaki size_t parameter koji se prenosi u knji?nicu ili iz nje pi?e. 64-bitni pomak koji sti?e na 32-bitni parametar gubi svoju gornju polovicu. Za malu datoteku svaki pomak stane u 32 bita i ni?ta nije pogre?no. Za veliku datoteku, u trenutku kada pomak prije?e granicu od ?etiri gigabajta, skra?ena vrijednost upu?uje na sasvim drugo mjesto, PDFium pita je li pogre?an raspon bajtova dostupan, a progresivno u?itavanje se zaustavlja ili ?ita sme?e. Nedostatak je nevidljiv sve dok datoteka ne bude dovoljno velika i cilj ne bude onaj na kojem se size_t stvarno pro?irio

Pascal iznimka se nikada ne smije odmotavati kroz C okvir

Tre?a klasa odnosi se na model iznimaka, koji C nema. Kada PDFium pozove jedan od va?ih povratnih poziva, va? Pascal kod se izvodi unutar stoga C i C++ okvira koji ne znaju ni?ta o mehanizmu iznimaka u Delphi-ju. Ako va? povratni poziv podigne iznimku i dopusti joj da se ?iri, ona se odmotava kroz okvire koji nikada nisu izgra?eni da budu odmotani. Vlastito ?i??enje PDFium-a se ne pokre?e, njegove interne invarijante ostaju polovi?no a?urirane, a proces je sada u stanju koje knji?nica nikada nije predvidjela. Ugovor za ove povratne pozive je povratni kod, a ne iznimka

Dva povratna poziva ?ine ovo konkretnim. FPDF_FILEWRITE je ponor u koji PDFium zapisuje spremljeni dokument, a FPDF_FILEACCESS je izvor iz kojeg ?ita ulazni dokument. Oba su ovdje implementirana preko Delphi TStream, i oba mogu zakazati na na?in na koji bilo koji tok zakazuje: disk se napuni, tok se zatvori ispod vas, ?itanje ide izvan kraja. Povratni poziv za pisanje omotava svoje pisanje u tok i pretvara svaki neuspjeh u PDFium kod neuspjeha, umjesto da dopusti da pobjegne

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;

Strana ?itanja radi isto: neuspjelo ?itanje javlja nulu kako bi odgovaralo ugovoru FPDF_FILEACCESS umjesto podizanja iznimke preko granice. Goli except bez ponovnog podizanja izgleda pogre?no Pascal programeru koji je obu?en da nikada ne guta iznimke, i u obi?nom Pascalu to jest pogre?no. Na granici ABI-ja to je ispravan oblik, jer je jedina sigurna vrijednost za predaju C pozivatelju statusni kod koji on zna protuma?iti. Neuspjeh se i dalje ?iri, samo kroz povratnu vrijednost, a pozivaju?i kod iznad knji?nice ga prikazuje kao EPdfError kada se kontrola vrati na Pascal stranu ograde

Dvostruko osloba?anje se skriva na putanji pogre?ke

?etvrti nedostatak je vlasni?tvo. Ru?ka dokumenta PDFium-a otvara se knji?nicom i mora se zatvoriti to?no jednom, pomo?u FPDF_CloseDocument. Opasnost je putanja pogre?ke koja osloba?a ru?ku koju posjeduje i drugo ?i??enje. Zamislite rutinu koja stvara objekt omota?a, dodjeljuje mu svje?e otvorenu ru?ku dokumenta, a zatim radi daljnje postavljanje koje bi moglo zakazati. Ako postavljanje baci iznimku, rukovatelj ranim povratkom koji poziva FPDF_CloseDocument na sirovoj ru?ki ?e je zatvoriti, a zatim ?e je vlastiti destruktor objekta omota?a ponovno zatvoriti kada se objekt oslobodi. Ru?ka se osloba?a dvaput, ?to je nedefinirano pona?anje i vjerojatno ru?enje

Revizija je to prona?la na putanji uvoza u stilu nametanja koja gradi TPdf oko ve? otvorene ru?ke. Ispravak je da prijenos vlasni?tva postane jedini izvor istine. Jednom kada je ru?ka dodijeljena polju omota?a, omota? je posjeduje, a jedino ?i??enje na putanji pogre?ke je osloba?anje omota?a. Destruktor omota?a poziva FPDF_CloseDocument umjesto vas, tako da bi drugo eksplicitno zatvaranje dvostruko oslobodilo isti dokument. Ispravljeni rukovatelj pogre?kama osloba?a objekt i ponovno podi?e iznimku, a postoji to?no jedna putanja do zatvaranja

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 i knji?nica puna izvoza trebaju eksplicitno rastavljanje

Posljednja klasa odnosi se na memoriju kojom kompajler upravlja u va?e ime, a koju navika iz C-a mo?e tiho pokvariti. Mnoge pomo?ne funkcije ovog vezanja vra?aju zapis koji sadr?i WideString ili dinami?ki niz. To su polja s brojanjem referenci, a kompajler emitira skriveno knjigovodstvo za odr?avanje njihovog broja. Instinkt prenesen iz C-a je ?i??enje svje?eg zapisa pomo?u FillChar(Result, SizeOf(Result), 0). To ispisuje nule preko upravljane reference unutar zapisa bez prethodnog smanjenja broja referenci. Kompajler ponovno koristi jednu skrivenu privremenu varijablu za rezultat funkcije kroz iteracije petlje, tako da u drugoj iteraciji FillChar prepisuje aktivni pokaziva? niza koji nikada nije oslobo?en, i niz na koji je upu?ivao curi. Pozovete funkciju u petlji preko tisu?u bilje?ki i procurit ?ete tisu?u nizova

Rje?enje je pustiti jezik da o?isti zapis na na?in na koji zna, pomo?u Default(T), ?to osloba?a svako upravljano polje prije postavljanja na nulu

// 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);

Povezani problem vlasni?tva ?ivi na granici u?itavanja knji?nice. Ovo vezanje rje?ava nekoliko stotina pokaziva?a funkcija iz PDFium DLL-a pomo?u GetProcAddress nakon LoadLibrary. Ako jedan potreban izvoz nedostaje, djelomi?no vezano stanje je opasno: deseci pokaziva?a su va?e?i, ostatak je nil ili zastario, a svaki kasniji poziv kroz jedan od njih ska?e u modul koji je mo?da ve? istovaren. Vezanje to rje?ava istovarom knji?nice i pokretanjem potpunog ClearAllBindings koji vra?a svaki uvezeni pokaziva? na nil kad god se potreban izvoz ne uspije rije?iti. Nakon toga, nijedan pokaziva? funkcije ne visi u istovarenom modulu, a kasniji poziv ne uspijeva ?isto s provjerom nil-pokaziva?a umjesto grananja u oslobo?eni kod

Omota? je mjesto gdje se ?etiri ugovora ponavljaju ru?no

Nijedan od ovih pet nedostataka nije egzoti?an. Oni su predvidljivi na?ini zakazivanja tankog Pascal sloja preko C API-ja, i grupiraju se jer je taj sloj upravo mjesto gdje se ?etiri odvojena ugovora moraju ponovno deklarirati. Konvencija pozivanja mora biti napisana kao cdecl na svakom povratnom pozivu. ?irina cijelog broja mora odgovarati size_t na jednoj ciljnoj platformi na kojoj se stvarno ?iri. Model iznimaka mora se pretvoriti u povratne kodove pri svakom povratnom pozivu koji izlazi iz Pascala. Vlasni?tvo nad svakom ru?kom i svakim upravljanim poljem mora se navesti jednom i po?tovati na svakoj putanji, uklju?uju?i putanje pogre?aka koje nitko ne koristi do produkcije. Propustite bilo koji i dobit ?ete nedostatak ?iji se simptom pojavljuje daleko od uzroka, ?to je ono ?to ovu kategoriju ?ini skupom. Vrijednost revizije bila je manje u bilo kojem pojedina?nom ispravku, a vi?e u tretiranju svakog od njih kao vlastite discipline koju treba provjeriti kroz cijelo vezanje

Ako ?elite vidjeti vezanje kako radi stvarni posao umjesto da ?uva svoje rubove, tehnike predmemorije prikazivanja i zumiranja u na?oj bilje?ci o predmemoriji prikazivanja i performansama zumiranja prikazuju putanju prikazivanja, a vodi? za vi?eplatformski kompajler u izgradnji Lazarus i FPC preglednika mjesto je gdje je pona?anje Win64 size_t ovdje opisano zapravo va?no. Obje se temelje na istom radu na sigurnosti memorije i ABI-ju koji se isporu?uje u softveru PDFium Component za Delphi, Lazarus i C++Builder, zajedno s API-jima za prikazivanje, ekstrakciju teksta i obrasce koji su pokriveni drugdje na ovom blogu