Tekninen artikkeli

Kuusi PDFium-kutsua, jotka unohtivat renderöintilukon Delphissä

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