Ključavnica izrisa PDFiumPas je kritični odsek za posamezen dokument — EnterRenderLock in LeaveRenderLock, podprta s poljem TRTLCriticalSection v TPdf — ki naj bi ovil vsak klic rasterizatorja PDFium, da strani ni mogoče odstraniti ali znova naložiti med izrisom, ki še poteka. Šest metod, enakomerno razdeljenih med TPdf in TPdfView, je neposredno klicalo API-je PDFium za pridobivanje bitnih slik in sličic ter v celoti preskočilo to ključavnico, kar je PDFiumPas v2.26.0 odpravil tako, da je vseh šest ovil v isti par ključavnic, ki so ga že uporabljale druge vstopne točke za izris
Obravnavana vrzel ni del utrjevanja ABI, opisanega drugje na tem spletnem dnevniku, kjer sta bila obravnavana neusklajenost klicne konvencije cdecl in skrajšanje širine kazalca FPC Win64 v isti vezavi PDFium. Nadaljevanje je ožje in bolj mehansko: kontrolni seznam pokritosti s ključavnico za šest klicnih mest, ki vsa dosežejo izrisno pot PDFium, razlog, zakaj je bilo vsako od njih zlahka spregledati, ter razlog, zakaj je tekmovalno stanje zaradi manjkajoče ključavnice ena težje ponovljivih napak v tej zbirki kode
Kako ključavnica izrisa dejansko ščiti
PDFiumPas izris zaporedno usklajuje, ker naložene strani PDFium ni varno brati v eni niti, medtem ko jo lahko druga nit sprosti. TPdf ima v FRenderLock objekt TRTLCriticalSection, inicializiran v konstruktorju in varovan z zastavico FRenderLockReady, zato klic po razgradnji postane tiha prazna operacija namesto vstopa v izbrisan kritični odsek. EnterRenderLock in LeaveRenderLock sta edini dovoljeni poti v ta odsek in iz njega
procedure TPdf.EnterRenderLock;
begin
if FRenderLockReady then
EnterCriticalSection(FRenderLock);
end;
procedure TPdf.LeaveRenderLock;
begin
if FRenderLockReady then
LeaveCriticalSection(FRenderLock);
end;
TPdf.RenderPage, RenderTile in RenderPageProgressive so to disciplino upoštevali že pred začetkom te revizije, saj vsaka metoda prevzame ključavnico pred klicem v PDFium in jo sprosti v bloku finally, zato se predizris v ozadju in osprednji UnloadPage na istem primerku TPdf ne moreta prekrivati. Vrzel, ki jo je odkril PDFiumPas v2.26.0, ni bila v teh očitnih vstopnih točkah — pojavila se je v šestih metodah, ki so bile videti kot dostopniki, ne kot izrisi, čeprav mora vsaka od njih pred vrnitvijo rezultata od PDFium zahtevati rasterizacijo slikovnih pik
Katerih šest klicev je preskočilo ključavnico izrisa
TPdf.GetObjectBitmap, TPdf.GetBitmap in TPdf.GetThumbnail so sestavljali polovico seznama, drugo polovico pa TPdfView.GetObjectBitmap, TPdfView.GetBitmap in TPdfView.GetThumbnail — iste tri operacije, podvojene v razredih obeh komponent, ki izpostavljata isto osnovno stran. Vseh šest prej ali slej pokliče FPDFImageObj_GetBitmap ali FPDFPage_GetThumbnailAsBitmap, obe vstopni točki PDFium pa rasterizirata takoj, namesto da bi vrnili sklic na nekaj že izrisanega. V imenih teh šestih metod ni ničesar, kar bi nakazovalo izris, zato je razumljivo, da pri prvem pisanju niso bile vključene na isti kontrolni seznam kot RenderPage in RenderTile
function TPdf.GetObjectBitmap(Index: Integer): TBitmap;
var
Bitmap: FPDF_BITMAP;
begin
Result:= nil;
EnterRenderLock;
try
Bitmap:= FPDFImageObj_GetBitmap(GetObjectHandle(Index));
finally
LeaveRenderLock;
end;
if Bitmap<> nil then
try
Result:= ToBitmap(Bitmap);
finally
FPDFBitmap_Destroy(Bitmap);
end;
end;
Zakaj TPdfView klic ključavnice varuje s preverjanjem nil
TPdfView nima lastnega kritičnega odseka — vseh šest njegovih klicev ključavnice posreduje v FPdf.EnterRenderLock in FPdf.LeaveRenderLock, vendar šele po preverjanju, da povezani sklic TPdf ni nil. Varovalo obstaja, ker je lahko TPdfView med načrtovanjem na obrazcu ali kratek čas med zapiranjem enega dokumenta in odpiranjem naslednjega, ko v FPdf še ni dodeljen noben TPdf. Preskok varovala bi eno sesutje zamenjal za drugo, saj klic ključavnice prek sklica nil ni nič bolj varen od tekmovalnega stanja, ki naj bi ga ključavnica preprečila
function TPdfView.GetThumbnail: TBitmap;
var
PdfBitmap: FPDF_BITMAP;
begin
CheckActive;
Result:= nil;
if FPdf<> nil then
FPdf.EnterRenderLock;
try
PdfBitmap:= FPDFPage_GetThumbnailAsBitmap(Page);
finally
if FPdf<> nil then
FPdf.LeaveRenderLock;
end;
if PdfBitmap<> nil then
try
Result:= ToBitmap(PdfBitmap);
finally
FPDFBitmap_Destroy(PdfBitmap);
end;
end;
Zakaj je RenderPage(HDC) del iste revizije
TPdfView.RenderPage za kontekst naprave ni eden od šestih klicev — pojavil se je že v prejšnji izdaji, v PDFiumPas v2.25.0, vendar sodi na ta kontrolni seznam, ker gre za isto napako z drugačnim podpisom. Ta preobremenitev je neposredno poklicala FPDF_RenderPage brez EnterRenderLock in brez klica SetArithmeticMask, ki pri starejših prevajalnikih Delphi varuje pred izjemami FPU, medtem ko je preobremenitev za TBitmap nekaj vrstic niže v istem razredu že vsebovala oboje. Dve reviziji, ki sta v razmiku ene izdaje odkrili isti način odpovedi, povesta manj o posamezni metodi in več o obliki napake: skriva se v tisti preobremenitvi, ki je nihče več ne pregleda, ko je njena sorodna različica videti pravilna
procedure TPdfView.RenderPage(DeviceContext: HDC; Left, Top, Width,
Height: Integer; Rotation: TRotation; Options: TRenderOptions);
var
ArithmeticMask: TArithmeticMask;
begin
CheckActive;
if FPdf<> nil then
FPdf.EnterRenderLock;
ArithmeticMask:= SetArithmeticMask;
try
FPDF_RenderPage(DeviceContext, FPage, Left, Top, Width, Height,
Ord(Rotation), EncodeRenderOptions(Options));
finally
RestoreArithmeticMask(ArithmeticMask);
if FPdf<> nil then
FPdf.LeaveRenderLock;
end;
end;
Zakaj je to tekmovalno stanje skoraj nemogoče ponoviti
Vrzel v ključavnici izrisa PDFiumPas ne povzroči napake pri vsakem zagonu ali celo pri večini zagonov, ker morata na istem primerku TPdf hkrati nastopiti dve stvari: klic rasterizacije mora že potekati, sočasni UnloadPage ali ReloadPage pa mora prispeti v istem časovnem oknu. Enonitno testiranje te poti sploh ne izvede, tudi resnično večnitne obremenitve pa jo sprožijo le, ko se izris v ozadju in dogodek življenjskega cikla dokumenta prekrivata med življenjsko dobo ene strani. Najverjetnejši sprožilec je predizris PDF v ozadju, zgrajen na preklicljivih prihodnjih opravilih, pri katerem delovna nit rasterizira naslednjo stran, uporabniška nit pa ob vnosu uporabnika znova naloži ali odstrani trenutno stran
FPDFImageObj_GetBitmap in FPDFPage_GetThumbnailAsBitmap prehajata po strukturah predmetov strani, ki jih lahko UnloadPage sprosti sredi prehoda, zato dejansko sproženo tekmovalno stanje tudi ne povzroči vedno takojšnje kršitve dostopa. Prepozno branje strukture lahko vrne popačene slikovne pike ali poškoduje metapodatke kopice, sesutje pa se zgodi šele pri več nepovezanih dodelitvah v funkciji, ki se strani PDF sploh ni dotaknila. To je iskren razlog, zakaj lahko takšna napaka preživi več izdaj: sled sklada na mestu odpovedi redko kaže blizu šestih vrstic, ki so jim dejansko manjkale ključavnice
Kaj to pomeni za klicatelje
GetBitmap, GetObjectBitmap, GetThumbnail in preobremenitev RenderPage za HDC ohranijo svoje javne podpise, saj je popravek notranja dodana ključavnica okoli obstoječih klicev, ne selitev API-ja. Ne pozabite, da velja ključavnica izrisa za posamezen primerek TPdf, ne globalno za proces, zato lahko dve niti, ki izrisujeta dva ločeno naložena dokumenta, še vedno delujeta povsem vzporedno — ključavnica zaporedno izvaja le operacije nad dokumentom, ki si ga niti delita. Če so vaše ključavnice že pravilne, izris pa je pri povečavi ali drsenju še vedno počasen, gre za drugo vprašanje, na katero odgovarja članek o predpomnilniku izrisa PDFium in zmogljivosti povečave — pravilnost in hitrost sta ločeni osi, ta popravek pa vpliva samo na prvo
Šest metod in ena sorodna preobremenitev predstavljajo majhen del površine PDFium, ki jo izpostavlja PDFiumPas, vendar so bile prav to metode, ki so se napačno obnašale samo pod obremenitvijo, ki je nihče ni izvajal v razhroščevalniku. Sama ključavnica izrisa in celoten nabor vstopnih točk izrisa, ki jih zdaj pokriva, sta del komponente PDFium Component za Delphi, C++Builder in Lazarus/FPC