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 Component 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 tretira svaku povratnu vrijednost različitu od 1 kao neuspjeh
// pisanja. Pascal iznimka se ne smije odmotavati kroz ovaj cdecl/C++
// okvir, pa je uhvatite i umjesto toga prijavite neuspjeh.
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 sada posjeduje ručku
Result.InitializeFormFill;
Result.ReloadPage;
except
// Result.Free zatvara ručku. Drugi FPDF_CloseDocument(NewDoc)
// ovdje bi dvostruko oslobodio isti PDFium dokument.
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() umjesto FillChar: kompajler ponovno koristi jednu skrivenu privremenu
// varijablu za rezultat funkcije kroz iteracije petlje, pa bi FillChar nulirao
// aktivne WideString pokazivače bez oslobađanja.
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