Articol tehnic

Șase apeluri PDFium care au uitat blocajul de randare în Delphi

Blocajul de randare al PDFiumPas este o secțiune critică per document — EnterRenderLock și LeaveRenderLock, susținute de un câmp TRTLCriticalSection pe TPdf — menit să învelească fiecare apel în rasterizatorul PDFium, astfel încât o pagină să nu poată fi descărcată sau reîncărcată de sub o randare în curs. Șase metode, împărțite egal între TPdf și TPdfView, apelau direct API-urile PDFium de extragere de bitmap și miniatură și săreau complet acel blocaj, un gol pe care PDFiumPas v2.26.0 l-a închis învelind toate șase în aceeași pereche de blocaj pe care fiecare alt punct de intrare de randare deja o folosea

Golul acoperit aici nu este trecerea de întărire ABI acoperită în altă parte pe acest blog, care a parcurs o nepotrivire de convenție de apel cdecl și o trunchiere de lățime de pointer FPC Win64 în aceeași legătură PDFium. Ce urmează este mai restrâns și mai mecanic: o listă de verificare a acoperirii blocajului pentru șase puncte de apel care ajung toate în calea de randare a PDFium, de ce fiecare a fost ușor de ratat, și de ce cursa care rezultă din lipsa blocajului este unul din defectele mai greu de reprodus la cerere din acest cod

Ce protejează efectiv blocajul de randare

PDFiumPas serializează randarea pentru că pagina încărcată a PDFium nu este sigură de citit dintr-un thread în timp ce un alt thread este liber să o elibereze. TPdf deține un TRTLCriticalSection în FRenderLock, inițializat în constructor și protejat de un steag FRenderLockReady, astfel încât un apel care sosește după demontare devine un no-op silențios, în loc să intre într-o secțiune critică ștearsă. EnterRenderLock și LeaveRenderLock sunt singura cale sancționată de intrare și ieșire din acea secțiune

procedure TPdf.EnterRenderLock;
begin
  if FRenderLockReady then
    EnterCriticalSection(FRenderLock);
end;

procedure TPdf.LeaveRenderLock;
begin
  if FRenderLockReady then
    LeaveCriticalSection(FRenderLock);
end;

TPdf.RenderPage, RenderTile și RenderPageProgressive deja urmau acea disciplină înainte ca acest audit particular să înceapă vreodată, fiecare preluând blocajul înainte de a apela în PDFium și eliberându-l într-un bloc finally, astfel încât o pre-randare de fundal și un UnloadPage de prim-plan pe aceeași instanță TPdf nu se pot suprapune. Golul pe care l-a găsit PDFiumPas v2.26.0 nu era în acele puncte de intrare evidente — a apărut în șase metode care se citesc ca accesori, nu ca randări, deși fiecare din ele cere PDFium să rasterizeze pixeli înainte de a putea returna orice

Care șase apeluri au sărit blocajul de randare?

TPdf.GetObjectBitmap, TPdf.GetBitmap și TPdf.GetThumbnail formau jumătate din listă, iar TPdfView.GetObjectBitmap, TPdfView.GetBitmap și TPdfView.GetThumbnail formau cealaltă jumătate — aceleași trei operații, duplicate pe cele două clase de componente care expun aceeași pagină de bază. Toate șase apelează în cele din urmă fie FPDFImageObj_GetBitmap, fie FPDFPage_GetThumbnailAsBitmap, iar ambele acele puncte de intrare PDFium rasterizează pe loc, în loc să returneze o referință la ceva deja randat. Nimic din niciunul din cele șase nume de metodă nu spune render, ceea ce este o explicație rezonabilă pentru de ce nu au fost scrise față de aceeași listă de verificare ca RenderPage și RenderTile prima dată

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;

De ce protejează TPdfView apelul său de blocaj cu o verificare nil

TPdfView nu deține propria secțiune critică — fiecare din cele șase apeluri de blocaj ale sale redirecționează spre FPdf.EnterRenderLock și FPdf.LeaveRenderLock, învelite într-o verificare că referința TPdf asociată nu este nil mai întâi. Acea gardă există pentru că un TPdfView poate sta pe un formular la momentul de proiectare, sau pe scurt între închiderea unui document și deschiderea următorului, fără niciun TPdf atribuit lui FPdf încă. Sărirea gărzii ar schimba un accident cu altul, întrucât un apel de blocare împotriva unei referințe nil eșuează la fel de puțin grațios ca și cursa pe care blocajul există pentru a o preveni

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;

De ce aparține RenderPage(HDC) aceluiași audit?

TPdfView.RenderPage pe un context de dispozitiv nu este unul din cele șase — a apărut într-o lansare anterioară, în PDFiumPas v2.25.0, și își câștigă un loc în această listă de verificare pentru că este același defect purtând o semnătură diferită. Acea suprascriere apela FPDF_RenderPage direct fără nici EnterRenderLock, nici apelul SetArithmeticMask care protejează împotriva excepțiilor FPU pe compilatoare Delphi mai vechi, în timp ce suprascrierea TBitmap așezată câteva linii mai jos în aceeași clasă purta deja ambele. Două treceri de audit care prind același mod de eșec la o lansare distanță spune mai puțin despre orice metodă individuală și mai mult despre forma bug-ului: se ascunde în orice suprascriere nimeni nu re-recitește odată ce sora ei arată corect

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;

De ce este această cursă aproape imposibil de reprodus?

Golul de blocaj de randare al PDFiumPas nu eșuează la fiecare rulare, sau nici măcar la majoritatea rulărilor, pentru că necesită două lucruri specifice care ajung pe aceeași instanță TPdf deodată: un apel de rasterizare deja în curs, și un UnloadPage sau ReloadPage concurent care sosește în interiorul aceleiași ferestre. Testarea single-thread nu exercită deloc calea, iar chiar sarcinile de lucru cu adevărat multi-thread o declanșează doar atunci când o randare de fundal și un eveniment de ciclu de viață al documentului se întâmplă să se suprapună în interiorul duratei de viață a unei pagini. Cel mai realist declanșator este pre-randarea PDF în fundal construită pe futures anulabile, unde un thread worker rasterizează pagina următoare în timp ce thread-ul UI reîncarcă sau descarcă pagina curentă la intrarea utilizatorului

FPDFImageObj_GetBitmap și FPDFPage_GetThumbnailAsBitmap parcurg structuri de obiecte de pagină pe care UnloadPage este liber să le elibereze la mijlocul parcurgerii, așa că o cursă care chiar se declanșează nu produce întotdeauna nici măcar o încălcare de acces imediată. O structură citită cu un moment prea târziu poate la fel de ușor să returneze pixeli gunoi, sau să corupă metadate de heap care abia mai târziu prăbușesc câteva alocări fără legătură, într-o funcție care nu a atins niciodată o pagină PDF. Acesta este motivul onest pentru care această clasă de bug poate supraviețui într-un cod pe parcursul mai multor cicluri de lansare: stack trace-ul la punctul de eșec rareori indică undeva aproape de cele șase linii cărora efectiv le lipsea un blocaj

Ce se schimbă pentru apelanți

GetBitmap, GetObjectBitmap, GetThumbnail, și suprascrierea HDC a RenderPage își păstrează semnăturile publice exact așa cum erau, întrucât soluția este blocare internă adăugată în jurul apelurilor existente, nu o migrare. Merită reamintit că blocajul de randare este limitat per instanță TPdf, nu global procesului, așa că două thread-uri care randează două documente încărcate separat tot rulează complet în paralel — blocajul serializează doar operațiile față de documentul unic pe care ambele thread-uri se întâmplă să îl împartă. Dacă blocarea dvs. este deja solidă, iar randările tot par lente la zoom sau derulare, aceea este o întrebare diferită, la care se răspunde în articolul despre tacticile de performanță ale cache-ului de randare PDFium și zoom — corectitudinea și viteza sunt axe separate aici, iar această soluție atinge doar prima

Șase metode și o suprascriere soră sunt o fracțiune mică din suprafața PDFium pe care o expune PDFiumPas, dar au fost fracțiunea care se comporta greșit doar sub sarcină pe care nimeni nu se întâmpla să o ruleze într-un debugger. Blocajul de randare însuși, și setul complet de puncte de intrare de randare pe care acum le acoperă, sunt livrate ca parte a componentei PDFium pentru Delphi, C++Builder și Lazarus/FPC