Il render lock di PDFiumPas è una sezione critica per documento — EnterRenderLock e LeaveRenderLock, sostenuti da un campo TRTLCriticalSection su TPdf — pensato per avvolgere ogni chiamata nel rasterizzatore di PDFium cosicché una pagina non possa essere scaricata o ricaricata sotto un rendering in corso. Sei metodi divisi equamente tra TPdf e TPdfView chiamavano direttamente le API di PDFium per bitmap ed estrazione miniature e saltavano del tutto quel lock, una lacuna che PDFiumPas v2.26.0 ha chiuso avvolgendo tutti e sei nella stessa coppia di lock già usata da ogni altro punto di ingresso di rendering
La lacuna trattata qui non è il passaggio di irrobustimento ABI trattato altrove su questo blog, che percorreva un disallineamento di convenzione di chiamata cdecl e un troncamento di larghezza puntatore FPC Win64 nello stesso binding PDFium. Ciò che segue è più circoscritto e più meccanico: una checklist di copertura del lock per sei punti di chiamata che raggiungono tutti il percorso di rendering di PDFium, perché ciascuno era facile da perdere, e perché la race condition che segue dal lock mancante è uno dei difetti più difficili da riprodurre a comando in questa codebase
Cosa protegge realmente il render lock
PDFiumPas serializza il rendering perché la pagina caricata di PDFium non è sicura da leggere da un thread mentre un altro thread è libero di rilasciarla. TPdf possiede un TRTLCriticalSection in FRenderLock, inizializzato nel costruttore e protetto da un flag FRenderLockReady cosicché una chiamata arrivata dopo la distruzione diventi un no-op silenzioso invece di entrare in una sezione critica eliminata. EnterRenderLock e LeaveRenderLock sono l'unico modo autorizzato di entrare e uscire da quella sezione
procedure TPdf.EnterRenderLock;
begin
if FRenderLockReady then
EnterCriticalSection(FRenderLock);
end;
procedure TPdf.LeaveRenderLock;
begin
if FRenderLockReady then
LeaveCriticalSection(FRenderLock);
end;
TPdf.RenderPage, RenderTile e RenderPageProgressive seguivano già questa disciplina prima ancora che iniziasse questo particolare audit, ciascuno prendendo il lock prima di chiamare PDFium e rilasciandolo in un blocco finally cosicché un pre-rendering in background e una UnloadPage in primo piano sulla stessa istanza TPdf non possano sovrapporsi. La lacuna che PDFiumPas v2.26.0 ha trovato non era in quei punti di ingresso ovvi — è emersa in sei metodi che si leggono come accessori piuttosto che come rendering, anche se ognuno di essi chiede a PDFium di rasterizzare pixel prima di poter restituire qualcosa
Quali sei chiamate saltavano il render lock?
TPdf.GetObjectBitmap, TPdf.GetBitmap e TPdf.GetThumbnail componevano metà dell'elenco, e TPdfView.GetObjectBitmap, TPdfView.GetBitmap e TPdfView.GetThumbnail componevano l'altra metà — le stesse tre operazioni, duplicate tra le due classi componente che espongono la stessa pagina sottostante. Tutte e sei alla fine chiamano o FPDFImageObj_GetBitmap o FPDFPage_GetThumbnailAsBitmap, ed entrambi quei punti di ingresso di PDFium rasterizzano sul posto invece di restituire un riferimento a qualcosa già renderizzato. Nulla nei nomi dei sei metodi dice render, il che è una spiegazione ragionevole del perché non siano stati scritti contro la stessa checklist di RenderPage e RenderTile la prima volta
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;
Perché TPdfView protegge la propria chiamata al lock con un controllo nil
TPdfView non possiede una propria sezione critica — ognuna delle sue sei chiamate al lock inoltra a FPdf.EnterRenderLock e FPdf.LeaveRenderLock, avvolta prima in un controllo che il riferimento TPdf associato non sia nil. Quella protezione esiste perché un TPdfView può risiedere su una form in fase di progettazione, o brevemente tra la chiusura di un documento e l'apertura del successivo, senza alcun TPdf assegnato a FPdf ancora. Saltare la protezione avrebbe scambiato un crash per un altro, poiché una chiamata di locking contro un riferimento nil fallisce non più elegantemente della race condition che il lock esiste per prevenire
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;
Perché RenderPage(HDC) appartiene allo stesso audit?
TPdfView.RenderPage contro un device context non è una delle sei — è emerso in una release precedente, in PDFiumPas v2.25.0, e si guadagna un posto in questa checklist perché è lo stesso difetto travestito con una firma diversa. Quel overload chiamava FPDF_RenderPage direttamente senza né EnterRenderLock né la chiamata SetArithmeticMask che protegge dalle eccezioni FPU su compilatori Delphi più vecchi, mentre il overload TBitmap seduto poche righe più sotto nella stessa classe portava già entrambi. Due passaggi di audit che catturano la stessa modalità di fallimento a distanza di una release dicono meno su un singolo metodo e più sulla forma del bug: si nasconde in qualunque overload nessuno rilegga una volta che il suo gemello sembra corretto
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;
Perché questa race condition è quasi impossibile da riprodurre?
La lacuna del render lock di PDFiumPas non fallisce a ogni esecuzione, e nemmeno nella maggior parte delle esecuzioni, perché richiede che due cose specifiche cadano contemporaneamente sulla stessa istanza TPdf: una chiamata di rasterizzazione già in corso, e una UnloadPage o ReloadPage concorrente che arriva dentro quella stessa finestra. Il testing single-thread non esercita mai affatto quel percorso, e persino carichi di lavoro genuinamente multi-thread lo fanno scattare solo quando un rendering in background e un evento di ciclo di vita del documento capitano di sovrapporsi entro la vita di una pagina. Il trigger più realistico è il pre-rendering PDF in background costruito su future annullabili, dove un worker thread rasterizza la pagina successiva mentre il thread UI ricarica o scarica quella corrente in risposta all'input dell'utente
FPDFImageObj_GetBitmap e FPDFPage_GetThumbnailAsBitmap percorrono strutture di oggetto pagina che UnloadPage è libera di rilasciare a metà attraversamento, quindi una race condition che effettivamente scatta non produce nemmeno sempre un access violation immediato. Una struttura letta un momento troppo tardi può altrettanto facilmente restituire pixel spazzatura, o corrompere metadati dell'heap che mandano in crash solo diverse allocazioni non correlate più tardi, in una funzione che non ha mai toccato una pagina PDF. Questa è la ragione onesta per cui questa classe di bug può sopravvivere in una codebase attraverso diversi cicli di release: lo stack trace nel punto di fallimento raramente punta da qualche parte vicino alle sei righe a cui effettivamente mancava un lock
Cosa cambia per i chiamanti
GetBitmap, GetObjectBitmap, GetThumbnail, e il overload HDC di RenderPage mantengono le proprie firme pubbliche esattamente come erano, poiché la correzione è locking interno aggiunto attorno alle chiamate esistenti piuttosto che una migrazione. Vale la pena ricordare che il render lock ha ambito per istanza TPdf, non globale al processo, quindi due thread che renderizzano due documenti caricati separatamente girano comunque completamente in parallelo — il lock serializza solo le operazioni contro l'unico documento che entrambi i thread capitano di condividere. Se il tuo locking è già solido e i rendering sembrano comunque lenti sotto zoom o scroll, quella è una domanda diversa, a cui risponde l'articolo sulle tattiche di cache di rendering PDFium e prestazioni allo zoom — correttezza e velocità sono assi separati qui, e questa correzione tocca solo il primo
Sei metodi e un overload gemello sono una piccola frazione della superficie PDFium che PDFiumPas espone, ma erano la frazione che si comportava male solo sotto un carico che nessuno capitava di eseguire in un debugger. Il render lock stesso, e l'intero insieme di punti di ingresso di rendering che ora copre, fanno parte del componente PDFium per Delphi, C++Builder e Lazarus/FPC