Pascal povezivanje (binding) preko C biblioteke čita se kao običan Pascal. Pozovete metod, dobijete zapis (record) nazad, oslobodite ono što ste alocirali. Problem je u tome što je PDFium C i C++ biblioteka sa sopstvenom konvencijom pozivanja, sopstvenim celobrojnim širinama i sopstvenim pravilima o tome ko poseduje memoriju a ko je oslobađa. Ništa od toga ne prelazi granicu jezika samo od sebe. Svaki od tih ugovora mora se ručno ponovo navesti u Pascal deklaracijama, a jedna pogrešna reč pretvara čist poziv u korupciju steka, odsečen ofset ili dvostruko oslobađanje. Revizija v1.61.0 verzije PDFium VCL povezivanja otkrila je po jedan defekt svake vrste. Vredi proći kroz njih jer nisu specifični za ovo povezivanje. Oni su stalne opasnosti obmotavanja bilo kog C API-ja u Delphi-ju ili Lazarus-u.
cdecl je deo tipa funkcije, a ne dekoracija
PDFium je kompajlirani C. Na Win32 sistemu, njegovi izvozi i, što je još važnije, povratni pozivi (callbacks) koje pokreće koriste konvenciju pozivanja cdecl. Pod cdecl, pozivalac čisti stek nakon što se poziv vrati. Podrazumevani izvorni režim u Delphi-ju je register, a Win32 C standard za povratne pozive u nekim bibliotekama je stdcall, gde umesto toga pozvani čisti stek. Kada struktura preda PDFium-u pokazivač na funkciju, a vi zaboravite cdecl na tipu tog pokazivača, dve strane se ne slažu oko toga ko podešava pokazivač steka. Ili oboje to popravljaju, ili niko, i pokazivač steka odstupa za veličinu argumenata pri svakom pozivu.
Razlog zašto je ovaj defekt teško pronaći jeste to š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 leži na pokazivaču steka koji je pomeren za nekoliko bajtova, a to se manifestuje kao nasumično čitanje, loša povratna adresa ili rušenje sa istorijom poziva koja ukazuje daleko od povratnog poziva koji ste zapravo pogrešili. Popunjavanje formulara (form-fill) je klasično mesto gde ovo pravi problem, jer je interfejs za popunjavanje formulara zapis prepun povratnih poziva koje PDFium poziva. Jedan od njih, FFI_OpenFile, predaje PDFium-u funkciju koju će pozvati da otvori spoljnu datoteku, deklarisanu kao function(pThis: PFPDF_FORMFILLINFO; fileFlag: Integer; wsURL: FPDF_WIDESTRING; mode: PAnsiChar): PFPDF_FILEHANDLER; cdecl. Završni cdecl ispunjava uslov i njega vredi prekopirati. Izostavite 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 opcioni ukras, i kompajler vas neće upozoriti kada nedostaje jer je običan tip funkcije savršeno legalan Pascal tip. Jedina odbrana je da se konvencija pozivanja tretira kao obavezno polje svakog uvezenog potpisa i svakog povratnog poziva koji prosleđujete spolja.
size_t je širine pokazivača, a na FPC Win64 to znači 64 bita
Drugi defekt je neslaganje u širini celog broja koje se pojavljuje samo na jednom ciljnom sistemu. C-ov size_t je definisan tako da bude dovoljno širok da drži bilo koju veličinu objekta, što na 64-bitnoj platformi znači 64-bitni neoznačeni ceo broj. PDFium interfejsi za progresivno učitavanje komuniciraju preko size_t bajt-ofseta. Zapis provajdera dostupnosti FX_FILEAVAIL nosi povratni poziv IsDataAvail koji PDFium poziva sa ofsetom i veličinom, a povratni poziv AddSegment zapisa FX_DOWNLOADHINTS prima isto to. Oba parametra su 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;
Ako deklarišete te ofsete kao 32-bitni tip, povezivanje radi na Win32 i na Delphi Win64, a zatim se tiho kvari na FPC i Lazarus Win64 sistemima. Uzrok je suptilan. Na FPC Win64, NativeUInt je stvarni 64-bitni tip širine pokazivača, a size_t je njegov alias. Povezivanje ima komentar u odeljku sa tipovima koji upozorava upravo na to da se ne senči NativeUInt na FPC-u, jer bi njegovo redefinisanje na 32-bitni alias tamo primoralo size_t na 32 bita i oštetilo svaki size_t parametar koji se prenosi ili ispisuje od strane biblioteke. 64-bitni ofset koji stiže na 32-bitni parametar gubi svoju gornju polovinu. Za malu datoteku svaki ofset staje u 32 bita i ništa nije pogrešno. Za veliku datoteku, onog trenutka kada ofset pređe granicu od četiri gigabajta, odsečena vrednost ukazuje na sasvim drugo mesto, PDFium pita da li je pogrešan opseg bajtova dostupan, a progresivno učitavanje se blokira ili čita smeće. Defekt je nevidljiv sve dok datoteka ne bude dovoljno velika, a ciljni sistem ne bude onaj gde se size_t zapravo proširio.
Pascal izuzetak nikada ne sme da se odmotava kroz C okvir
Treća klasa se odnosi na model izuzetaka, koji C nema. Kada PDFium pozove neki od vaših povratnih poziva, vaš Pascal kod se izvršava unutar steka C i C++ okvira koji ne znaju ništa o mehanizmu izuzetaka u Delphi-ju. Ako vaš povratni poziv podigne izuzetak i dozvoli mu da se propagira, on se odmotava kroz okvire koji nikada nisu napravljeni za odmotavanje. Sopstveno čišćenje PDFium-a se ne pokreće, njegove interne invarijante ostaju polovično ažurirane, a proces se nalazi u stanju koje biblioteka nikada nije predvidela. Ugovor za ove povratne pozive je povratni kod, a ne izuzetak.
Dva povratna poziva čine ovo konkretnim. FPDF_FILEWRITE je ponor (sink) u koji PDFium upisuje sačuvani dokument, a FPDF_FILEACCESS je izvor iz kog čita ulazni dokument. Oba su ovde implementirana preko Delphi TStream-a, i oba mogu da otkažu na način na koji svaki tok otkazuje: disk se popuni, tok se zatvori ispod vas, čitanje pređe kraj. Povratni poziv za upisivanje obmotava svoj upis u tok i pretvara svaki neuspeh u PDFium kod greške umesto da dozvoli da pobegne.
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: neuspelo čitanje prijavljuje nulu da bi se poklopilo sa ugovorom FPDF_FILEACCESS, umesto da podiže izuzetak preko granice. Goli except bez ponovnog podizanja izuzetka izgleda pogrešno Pascal programeru obučenom da nikada ne guta izuzetke, i u običnom Pascalu jeste pogrešno. Na ABI granici to je ispravan oblik, jer je jedina bezbedna vrednost koja se vraća C pozivaocu statusni kod koji on zna da interpretira. Neuspeh se i dalje širi, samo kroz povratnu vrednost, a pozivni kod iznad biblioteke ga prikazuje kao EPdfError kada se kontrola vrati na Pascal stranu ograde.
Dvostruko oslobađanje se krije na putanji greške
Četvrti defekt je vlasništvo. Ručku (handle) PDFium dokumenta otvara biblioteka i mora se zatvoriti tačno jednom, pomoću FPDF_CloseDocument. Opasnost je putanja greške koja oslobađa ručku koju takođe poseduje i drugo čišćenje. Zamislite rutinu koja kreira objekat omotača, dodeljuje mu sveže otvorenu ručku dokumenta, a zatim vrši dodatno podešavanje koje može da ne uspe. Ako podešavanje podigne izuzetak, rukovalac sa ranim povratkom koji poziva FPDF_CloseDocument na sirovoj ručki će je zatvoriti, a zatim će je sopstveni destruktor objekta omotača ponovo zatvoriti kada se objekat oslobodi. Ručka se oslobađa dvaput, što je nedefinisano ponašanje i verovatno rušenje.
Revizija je pronašla ovo na putanji uvoza u stilu nametanja (imposition) koja gradi TPdf oko već otvorene ručke. Rešenje je da prenos vlasništva bude jedini izvor istine. Kada se ručka dodeli polju omotača, omotač je poseduje, a jedino čišćenje na putanji greške je oslobađanje omotača. Destruktor omotača poziva FPDF_CloseDocument umesto vas, tako da bi drugo eksplicitno zatvaranje dvostruko oslobodilo isti dokument. Ispravljeni rukovalac greškama oslobađa objekat i ponovo podiže izuzetak, i postoji tač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 biblioteka puna izvoza zahtevaju eksplicitno uklanjanje
Poslednja klasa se odnosi na memoriju kojom kompajler upravlja u vaše ime, a koju navika iz C-a može tiho da ošteti. Mnoge pomoćne funkcije ovog povezivanja vraćaju zapis koji sadrži WideString or dynamic array. To su polja sa brojem referenci, i kompajler emituje skriveno vođenje evidencije da bi održao njihove brojeve. Instinkt prenet iz C-a je čišćenje novog zapisa pomoću FillChar(Result, SizeOf(Result), 0). To upisuje nule preko upravljane reference unutar zapisa bez prethodnog dekrementiranja. Kompajler ponovo koristi jednu skrivenu privremenu promenljivu za rezultat funkcije kroz iteracije petlje, tako da na drugoj iteraciji FillChar prepisuje aktivni pokazivač stringa koji nikada nije oslobođen, a string na koji je ukazivao curi. Pozovite funkciju u petlji preko hiljadu anotacija i iscureće vam hiljada stringova.
Rešenje je da se jeziku prepusti čišćenje zapisa na način na koji on to zna, sa Default(T), što oslobađa svako upravljano polje pre nego što ga postavi 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 biblioteke. Ovo povezivanje razrešava nekoliko stotina pokazivača funkcija iz PDFium DLL-a pomoću GetProcAddress nakon LoadLibrary. Ako jedna potrebna izvozna funkcija nedostaje, delimično povezano stanje je opasno: desetine pokazivača su validne, ostatak je nil ili zastareo, a svaki kasniji poziv preko nekog od njih skače u modul koji je možda već istovaren. Povezivanje rešava ovo istovarom biblioteke i pokretanjem kompletnog ClearAllBindings koji resetuje svaki uvezeni pokazivač nazad na nil kad god potrebna izvozna funkcija ne uspe da se razreši. Nakon toga, nijedan pokazivač funkcije ne visi u istovarenom modulu, a kasniji poziv otkazuje čisto sa proverom nil pokazivača, umesto da se grana u oslobođeni kod.
Omotač je mesto gde se četiri ugovora ponovo ručno navode
Nijedan od ovih pet defekata nije egzotičan. Oni su predvidljivi režimi otkazivanja tankog Pascal sloja preko C API-ja, i grupišu se jer je taj sloj upravo mesto gde četiri posebna ugovora moraju ponovo biti deklarisana. Konvencija pozivanja mora biti napisana kao cdecl na svakom povratnom pozivu. Širina celog broja mora da se podudara sa size_t na ciljnom sistemu gde se zapravo proširuje. Model izuzetaka mora biti konvertovan u povratne kodove pri svakom povratnom pozivu koji izlazi iz Pascala. Vlasništvo nad svakom ručkom i svakim upravljanim poljem mora biti navedeno jednom i poštovano na svakoj putanji, uključujući putanje grešaka koje niko ne koristi do produkcije. Propustite bilo koji i dobićete defekt čiji se simptom pojavljuje daleko od svog uzroka, što je ono što ovu kategoriju čini skupom. Vrednost revizije bila je manje u pojedinačnoj popravci, a više u tretiranju svakog od njih kao sopstvene discipline koju treba proveriti kroz celo povezivanje.
Ako želite da vidite kako povezivanje radi stvaran posao umesto da samo čuva svoje ivice, tehnike keširanja renderovanja i zumiranja u našoj belešci o performansama keša renderovanja i zumiranja prikazuju putanju renderovanja, a vodič za unakrsno kompajliranje u izgradnji Lazarus i FPC pregledača je mesto gde je Win64 size_t ponašanje opisano ovde zapravo važno. Oba se grade na istoj memorijskoj bezbednosti i ABI radu koji se isporučuje u PDFium komponenti za Delphi, Lazarus i C++Builder, zajedno sa API-jima za renderovanje, ekstrakciju teksta i formulare koji su pokriveni na drugim mestima na ovom blogu.