PDFiumPas:n renderöintilukko on asiakirjakohtainen kriittinen osio — EnterRenderLock ja LeaveRenderLock, tuettuna TRTLCriticalSection-kentällä TPdf:ssä — tarkoitettu kääriäkseen jokainen kutsu PDFiumin rasteroijaan, jotta sivua ei voida purkaa tai ladata uudelleen kesken lennossa olevan renderöinnin alta. Kuusi metodia, tasan jaettuna TPdf- ja TPdfView-luokkien kesken, kutsuivat PDFiumin bittikartta- ja pienoiskuvapoiminta-API:a suoraan ja ohittivat tuon lukon kokonaan, aukko, jonka PDFiumPas v2.26.0 sulki kääräisemällä kaikki kuusi samaan lukkopariin, jota jokainen muu renderöinnin sisääntulopiste jo käytti
Tässä käsitelty aukko ei ole tämän blogin muualla käsitelty ABI-kovennuskierros, joka kävi läpi cdecl-kutsukäytäntöyhteensopimattomuuden ja FPC Win64 -osoitinlevyskatkaisun samassa PDFium-sidonnassa. Se, mikä seuraa, on kapeampi ja mekaanisempi: lukkokattavuustarkistuslista kuudelle kutsupaikalle, jotka kaikki ulottuvat PDFiumin renderöintipolkuun, miksi jokainen niistä oli helppo ohittaa, ja miksi kilpa-ajotilanne, joka seuraa puuttuvasta lukosta, on yksi tämän koodikannan vaikeimmin pyynnöstä toistettavista vioista
Mitä renderöintilukko todella suojaa
PDFiumPas sarjallistaa renderöinnin, koska PDFiumin ladattua sivua ei ole turvallista lukea yhdestä säikeestä, kun toinen säie voi vapaasti vapauttaa sen. TPdf omistaa TRTLCriticalSection-olion kentässä FRenderLock, alustettuna konstruktorissa ja vartioituna FRenderLockReady-lipulla, jotta purun jälkeen saapuva kutsu muuttuu hiljaiseksi ei-toiminnoksi sen sijaan, että se astuisi poistettuun kriittiseen osioon. EnterRenderLock ja LeaveRenderLock ovat ainoat sanktioidut tavat sisään ja ulos tuosta osiosta
procedure TPdf.EnterRenderLock;
begin
if FRenderLockReady then
EnterCriticalSection(FRenderLock);
end;
procedure TPdf.LeaveRenderLock;
begin
if FRenderLockReady then
LeaveCriticalSection(FRenderLock);
end;
TPdf.RenderPage, RenderTile ja RenderPageProgressive noudattivat jo tuota kuria ennen kuin tämä tietty tarkastus koskaan alkoi, kukin ottaen lukon ennen PDFiumiin kutsumista ja vapauttaen sen finally-lohkossa, jotta taustalla tapahtuva esirenderöinti ja etualalla tapahtuva UnloadPage samassa TPdf-instanssissa eivät voi mennä päällekkäin. Aukko, jonka PDFiumPas v2.26.0 löysi, ei ollut noissa ilmeisissä sisääntulopisteissä — se ilmestyi kuudessa metodissa, jotka lukevat kuin lukijoita renderöintien sijaan, vaikka jokainen niistä pyytää PDFiumia rasteroimaan pikseleitä ennen kuin se voi palauttaa mitään
Mitkä kuusi kutsua ohittivat renderöintilukon?
TPdf.GetObjectBitmap, TPdf.GetBitmap ja TPdf.GetThumbnail muodostivat puolet listasta, ja TPdfView.GetObjectBitmap, TPdfView.GetBitmap ja TPdfView.GetThumbnail muodostivat toisen puolen — samat kolme toimintoa, kahdennettuna kahden komponenttiluokan yli, jotka paljastavat saman taustalla olevan sivun. Kaikki kuusi lopulta kutsuvat joko FPDFImageObj_GetBitmap- tai FPDFPage_GetThumbnailAsBitmap-funktiota, ja molemmat noista PDFium-sisääntulopisteistä rasteroivat paikan päällä sen sijaan, että antaisivat takaisin viittauksen johonkin jo renderöityyn. Mikään kuudesta metodinimestä ei sano renderöi, mikä on järkevä selitys sille, miksi niitä ei kirjoitettu samaa tarkistuslistaa vasten kuin RenderPage ja RenderTile ensimmäisellä kerralla
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;
Miksi TPdfView vartioi lukkokutsuaan nil-tarkistuksella
TPdfView ei omista omaa kriittistä osiotaan — jokainen sen kuudesta lukkokutsusta välitetään funktioille FPdf.EnterRenderLock ja FPdf.LeaveRenderLock, käärittynä tarkistukseen, että liittyvä TPdf-viittaus ei ole nil ensin. Tuo vartija on olemassa, koska TPdfView voi istua lomakkeella suunnitteluaikana, tai lyhyesti yhden asiakirjan sulkeutumisen ja seuraavan avautumisen välissä, ilman että TPdf on vielä osoitettu FPdf:lle. Vartijan ohittaminen vaihtaisi yhden kaatumisen toiseen, koska lukituskutsu nil-viittausta vastaan epäonnistuu yhtä vähän sulavasti kuin kilpa-ajotilanne, jota lukko on olemassa estämään
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;
Miksi RenderPage(HDC) kuuluu samaan tarkastukseen?
TPdfView.RenderPage laitekontekstia vasten ei ole yksi näistä kuudesta — se ilmestyi julkaisu aiemmin, PDFiumPas v2.25.0:ssa, ja se ansaitsee paikan tässä tarkistuslistassa, koska se on sama vika eri allekirjoituksella. Tuo ylikuormitus kutsui FPDF_RenderPage-funktiota suoraan ilman EnterRenderLock-kutsua ja ilman SetArithmeticMask-kutsua, joka vartioi FPU-poikkeuksia vastaan vanhemmilla Delphi-kääntäjillä, samalla kun TBitmap-ylikuormitus muutaman rivin alempana samassa luokassa jo kantoi molemmat. Kaksi tarkastuskierrosta, jotka nappaavat saman vikatilan julkaisun päässä toisistaan, kertoo vähemmän mistä tahansa yksittäisestä metodista ja enemmän bugin muodosta: se piiloutuu mihin tahansa ylikuormitukseen, jota kukaan ei lue uudelleen heti, kun sen sisarus näyttää oikealta
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;
Miksi tämä kilpa-ajotilanne on lähes mahdoton toistaa?
PDFiumPas:n renderöintilukkoaukko ei epäonnistu joka ajolla, tai edes useimmilla ajoilla, koska se tarvitsee kaksi tiettyä asiaa osumaan samaan TPdf-instanssiin kerralla: rasterointikutsun, joka on jo lennossa, ja samanaikaisen UnloadPage- tai ReloadPage-kutsun, joka saapuu tuon saman ikkunan sisällä. Yksisäikeinen testaus ei koskaan harjoita polkua lainkaan, ja jopa aidosti monisäikeiset työkuormat laukaisevat sen vain, kun taustalla tapahtuva renderöinti ja asiakirjan elinkaaritapahtuma sattuvat menemään päällekkäin yhden sivun eliniän sisällä. Realistisin laukaisin on peruttavissa oleviin futuureihin rakennettu taustalla tapahtuva PDF-esirenderöinti, jossa työsäie rasteroi seuraavan sivun, samalla kun käyttöliittymäsäie lataa uudelleen tai purkaa nykyisen käyttäjän syötteen perusteella
FPDFImageObj_GetBitmap ja FPDFPage_GetThumbnailAsBitmap käyvät läpi sivu-oliorakenteita, joita UnloadPage saa vapaasti vapauttaa kesken läpikäynnin, joten kilpa-ajotilanne, joka todella laukeaa, ei aina tuota välitöntä access violation -virhettä myöskään. Rakenne, joka luetaan hetken liian myöhään, voi yhtä helposti antaa takaisin roskapikseleitä, tai vioittaa kasan metatietoja, jotka kaatavat vasta useita toisiinsa liittymättömiä varauksia myöhemmin, funktiossa, joka ei koskaan koskenut PDF-sivua. Se on rehellinen syy siihen, miksi tämä bugiluokka voi selvitä koodikannassa useiden julkaisukierrosten yli: pinojälki vikahetkellä osoittaa harvoin lähelläkään niitä kuutta riviä, joista todella puuttui lukko
Mikä muuttuu kutsujille
GetBitmap, GetObjectBitmap, GetThumbnail ja RenderPage:n HDC-ylikuormitus pitävät julkiset allekirjoituksensa täsmälleen samoina kuin ennen, koska korjaus on sisäinen lukitus, joka lisätään olemassa olevien kutsujen ympärille eikä migraatio. Kannattaa muistaa, että renderöintilukko on rajattu per TPdf-instanssi, ei globaali prosessille, joten kaksi säiettä, jotka renderöivät kahta erikseen ladattua asiakirjaa, toimivat silti täysin rinnakkain — lukko vain sarjallistaa toiminnot yhtä asiakirjaa vasten, jota molemmat säikeet sattuvat jakamaan. Jos lukituksesi on jo pätevä ja renderöinnit tuntuvat silti hitailta zoomauksen tai vierityksen alla, se on eri kysymys, johon vastataan artikkelissa PDFiumin renderöintivälimuisti ja zoomauksen suorituskykytaktiikat — oikeellisuus ja nopeus ovat täällä erillisiä akseleita, ja tämä korjaus koskettaa vain ensimmäistä niistä
Kuusi metodia ja yksi sisarusylikuormitus ovat pieni murto-osa PDFium-pinnasta, jonka PDFiumPas paljastaa, mutta ne olivat se murto-osa, joka käyttäytyi väärin vain kuormituksen alla, jota kukaan ei sattunut ajamaan debuggerissa. Itse renderöintilukko, ja koko joukko renderöinnin sisääntulopisteitä, jotka se nyt kattaa, toimitetaan osana PDFium-komponenttia Delphille, C++Builderille ja Lazarus/FPC:lle