Teknisk artikel

Sex PDFium-anrop som glömde rendreringslåset i Delphi

PDFiumPas rendreringslås är en per-dokument kritisk sektion — EnterRenderLock och LeaveRenderLock, backad av ett TRTLCriticalSection-fält på TPdf — avsett att omsluta varje anrop till PDFiums rasteriserare så att en sida inte kan laddas ur eller laddas om under en pågående rendrering. Sex metoder, jämnt fördelade mellan TPdf och TPdfView, anropade PDFiums bitmap- och miniatyrbildsextraktions-API:er direkt och hoppade helt över det låset, en lucka som PDFiumPas v2.26.0 tätade genom att omsluta alla sex i samma låspar varje annan rendreringsingångspunkt redan använde

Luckan som täcks här är inte den ABI-härdningspassering som täcks på annat håll på den här bloggen, som gick igenom en cdecl-anropskonventionsmissmatchning och en FPC Win64-pekarbreddstrunkering i samma PDFium-bindning. Det som följer är smalare och mer mekaniskt: en låstäckningschecklista för sex anropsplatser som alla når in i PDFiums rendreringsväg, varför var och en var lätt att missa, och varför kapplöpningen som följer av det saknade låset är en av de svårare defekterna i den här kodbasen att reproducera på begäran

Vad rendreringslåset faktiskt skyddar

PDFiumPas serialiserar rendrering eftersom PDFiums laddade sida inte är säker att läsa från en tråd medan en annan tråd är fri att frigöra den. TPdf äger ett TRTLCriticalSection i FRenderLock, initierat i konstruktorn och skyddat av en FRenderLockReady-flagga så att ett anrop som anländer efter nedmontering blir en tyst no-op istället för att gå in i en raderad kritisk sektion. EnterRenderLock och LeaveRenderLock är det enda sanktionerade sättet in och ut ur den sektionen

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

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

TPdf.RenderPage, RenderTile, och RenderPageProgressive följde redan den disciplinen innan just den här granskningen någonsin startade, var och en tog låset innan den anropade in i PDFium och släppte det i ett finally-block så att en bakgrundsförrendring och en förgrunds-UnloadPage på samma TPdf-instans inte kan överlappa. Luckan PDFiumPas v2.26.0 hittade fanns inte i de uppenbara ingångspunkterna — den dök upp i sex metoder som läser som åtkomstmetoder snarare än rendringar, även om var och en av dem ber PDFium rasterisera pixlar innan den kan returnera något

Vilka sex anrop hoppade över rendreringslåset?

TPdf.GetObjectBitmap, TPdf.GetBitmap, och TPdf.GetThumbnail utgjorde hälften av listan, och TPdfView.GetObjectBitmap, TPdfView.GetBitmap, och TPdfView.GetThumbnail utgjorde den andra hälften — samma tre operationer, duplicerade över de två komponentklasserna som exponerar samma underliggande sida. Alla sex anropar till slut antingen FPDFImageObj_GetBitmap eller FPDFPage_GetThumbnailAsBitmap, och båda de PDFium-ingångspunkterna rasteriserar på plats snarare än att lämna tillbaka en referens till något redan rendrat. Inget i något av de sex metodnamnen säger render, vilket är en rimlig förklaring till varför de inte skrevs mot samma checklista som RenderPage och RenderTile första gången

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;

Varför skyddar TPdfView sitt låsanrop med en nil-kontroll?

TPdfView äger ingen egen kritisk sektion — vart och ett av dess sex låsanrop vidarebefordrar till FPdf.EnterRenderLock och FPdf.LeaveRenderLock, omslutet i en kontroll att den associerade TPdf-referensen inte är nil först. Den skyddsklausulen finns eftersom en TPdfView kan sitta på ett formulär vid designtid, eller kortvarigt mellan att ett dokument stängs och nästa öppnas, utan någon TPdf tilldelad till FPdf ännu. Att hoppa över skyddet skulle byta en krasch mot en annan, eftersom ett låsanrop mot en nil-referens misslyckas inte mer graciöst än kapplöpningen låset finns för att förhindra

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;

Varför hör RenderPage(HDC) hemma i samma granskning?

TPdfView.RenderPage mot en enhetskontext är inte en av de sex — den dök upp en version tidigare, i PDFiumPas v2.25.0, och den förtjänar en plats i den här checklistan eftersom det är samma defekt som bär en annan signatur. Den överlagringen anropade FPDF_RenderPage rakt igenom utan varken EnterRenderLock eller SetArithmeticMask-anropet som skyddar mot FPU-undantag på äldre Delphi-kompilatorer, medan TBitmap-överlagringen som sitter några rader nedanför i samma klass redan bar båda. Två granskningspasseringar som fångar samma felmönster en version isär säger mindre om någon enskild metod och mer om buggens form: den gömmer sig i vilken överlagring som helst ingen läser om igen när dess syskon ser korrekt ut

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;

Varför är den här kapplöpningen nästan omöjlig att reproducera?

PDFiumPas rendreringslåslucka misslyckas inte vid varje körning, eller ens vid de flesta körningar, eftersom den behöver två specifika saker landa på samma TPdf-instans samtidigt: ett rasteriseringsanrop redan pågående, och en samtidig UnloadPage eller ReloadPage som anländer inom samma fönster. Entrådig testning utövar aldrig vägen alls, och även genuint flertrådiga arbetsbelastningar utlöser den bara när en bakgrundsrendrering och en dokumentlivscykelhändelse råkar överlappa inom en sidas livstid. Den mest realistiska utlösaren är bakgrunds-PDF-förrendrering byggd på avbrytbara futures, där en arbetstråd rasteriserar nästa sida medan UI-tråden laddar om eller ur den aktuella vid användarinmatning

FPDFImageObj_GetBitmap och FPDFPage_GetThumbnailAsBitmap går igenom sidobjekts-strukturer som UnloadPage är fri att frigöra mitt i genomgången, så en kapplöpning som faktiskt utlöses producerar inte alltid en omedelbar åtkomstöverträdelse heller. En struktur läst ett ögonblick för sent kan precis lika lätt lämna tillbaka skräppixlar, eller korrumpera heap-metadata som bara kraschar flera orelaterade allokeringar senare, i en funktion som aldrig rörde en PDF-sida. Det är den ärliga anledningen till att den här buggkategorin kan överleva i en kodbas över flera releasecykler: stackspåret vid felpunkten pekar sällan någonstans nära de sex raderna som faktiskt saknade ett lås

Vad ändras för anropare

GetBitmap, GetObjectBitmap, GetThumbnail, och HDC-överlagringen av RenderPage behåller sina publika signaturer exakt som de var, eftersom fixen är intern låsning tillagd runt befintliga anrop snarare än en migrering. Det är värt att komma ihåg att rendreringslåset är omfångat per TPdf-instans, inte globalt för processen, så två trådar som rendrerar två separat laddade dokument körs fortfarande fullt parallellt — låset serialiserar bara operationer mot det enda dokument båda trådarna råkar dela. Om din låsning redan är sund och rendringar fortfarande känns långsamma vid zoom eller rullning är det en annan fråga, besvarad i artikeln om PDFium-rendreringscache och zoomprestandataktik — korrekthet och hastighet är separata axlar här, och den här fixen rör bara den första

Sex metoder och en syskonöverlagring är en liten del av PDFium-ytan PDFiumPas exponerar, men de var den del som bara misskötte sig under belastning ingen råkade köra i en debugger. Rendreringslåset självt, och den fullständiga uppsättningen rendreringsingångspunkter det nu täcker, levereras som en del av PDFium-komponenten för Delphi, C++Builder, och Lazarus/FPC