Le verrou de rendu de PDFiumPas est une section critique par document — EnterRenderLock et LeaveRenderLock, adossés à un champ TRTLCriticalSection sur TPdf — destiné à envelopper chaque appel dans le rastériseur de PDFium afin qu'une page ne puisse pas être déchargée ou rechargée sous les pieds d'un rendu en cours. Six méthodes réparties également entre TPdf et TPdfView appelaient directement les API PDFium d'extraction de bitmap et de vignette et contournaient entièrement ce verrou, un écart que PDFiumPas v2.26.0 a comblé en enveloppant les six dans la même paire de verrou que chaque autre point d'entrée de rendu utilisait déjà
L'écart couvert ici n'est pas la passe de durcissement ABI couverte ailleurs sur ce blog, qui traitait d'une discordance de convention d'appel cdecl et d'une troncature de largeur de pointeur FPC Win64 dans la même liaison PDFium. Ce qui suit est plus étroit et plus mécanique : une liste de vérification de couverture de verrou pour six points d'appel qui atteignent tous le chemin de rendu de PDFium, pourquoi chacun était facile à manquer, et pourquoi la course qui découle de l'absence de verrou est l'un des défauts les plus difficiles à reproduire à la demande dans cette base de code
Ce que le verrou de rendu protège réellement
PDFiumPas sérialise le rendu parce que la page chargée de PDFium n'est pas sûre à lire depuis un thread pendant qu'un autre thread est libre de la libérer. TPdf détient une TRTLCriticalSection dans FRenderLock, initialisée dans le constructeur et gardée par un drapeau FRenderLockReady afin qu'un appel arrivant après le démontage devienne un no-op silencieux plutôt que d'entrer dans une section critique supprimée. EnterRenderLock et LeaveRenderLock sont le seul moyen sanctionné d'entrer et de sortir de cette section
procedure TPdf.EnterRenderLock;
begin
if FRenderLockReady then
EnterCriticalSection(FRenderLock);
end;
procedure TPdf.LeaveRenderLock;
begin
if FRenderLockReady then
LeaveCriticalSection(FRenderLock);
end;
TPdf.RenderPage, RenderTile, et RenderPageProgressive suivaient déjà cette discipline avant même que cet audit particulier ne commence, chacune prenant le verrou avant d'appeler PDFium et le relâchant dans un bloc finally afin qu'un pré-rendu en arrière-plan et un UnloadPage au premier plan sur la même instance TPdf ne puissent se chevaucher. L'écart que PDFiumPas v2.26.0 a trouvé ne se situait pas dans ces points d'entrée évidents — il est apparu dans six méthodes qui se lisent comme des accesseurs plutôt que des rendus, alors même que chacune d'elles demande à PDFium de rastériser des pixels avant de pouvoir renvoyer quoi que ce soit
Quels six appels ont contourné le verrou de rendu ?
TPdf.GetObjectBitmap, TPdf.GetBitmap, et TPdf.GetThumbnail constituaient la moitié de la liste, et TPdfView.GetObjectBitmap, TPdfView.GetBitmap, et TPdfView.GetThumbnail constituaient l'autre moitié — les mêmes trois opérations, dupliquées à travers les deux classes de composant qui exposent la même page sous-jacente. Les six finissent par appeler soit FPDFImageObj_GetBitmap, soit FPDFPage_GetThumbnailAsBitmap, et ces deux points d'entrée PDFium rastérisent sur-le-champ plutôt que de renvoyer une référence à quelque chose déjà rendu. Rien dans aucun des six noms de méthode ne dit render, ce qui est une explication raisonnable du fait qu'elles n'ont pas été écrites selon la même liste de vérification que RenderPage et RenderTile la première fois
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;
Pourquoi TPdfView protège-t-il son appel de verrou avec une vérification nil ?
TPdfView ne détient pas sa propre section critique — chacun de ses six appels de verrou transmet vers FPdf.EnterRenderLock et FPdf.LeaveRenderLock, enveloppé dans une vérification que la référence TPdf associée n'est pas nil d'abord. Cette protection existe parce qu'un TPdfView peut se trouver sur un formulaire au moment de la conception, ou brièvement entre la fermeture d'un document et l'ouverture du suivant, sans TPdf encore assigné à FPdf. Sauter la protection échangerait un plantage contre un autre, puisqu'un appel de verrouillage contre une référence nil échoue tout aussi mal que la course que le verrou existe pour empêcher
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;
Pourquoi RenderPage(HDC) appartient-il au même audit ?
TPdfView.RenderPage contre un contexte de périphérique ne fait pas partie des six — il est apparu une version plus tôt, dans PDFiumPas v2.25.0, et il mérite une place dans cette liste de vérification car c'est le même défaut portant une signature différente. Cette surcharge appelait FPDF_RenderPage directement, sans EnterRenderLock ni l'appel SetArithmeticMask qui protège contre les exceptions FPU sur les anciens compilateurs Delphi, alors que la surcharge TBitmap assise quelques lignes plus bas dans la même classe portait déjà les deux. Deux passes d'audit attrapant le même mode d'échec à une version d'intervalle en dit moins sur une méthode particulière que sur la forme du bogue : il se cache dans quelle que soit la surcharge que personne ne relit une fois que sa jumelle paraît correcte
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;
Pourquoi cette course est-elle presque impossible à reproduire ?
L'écart de verrou de rendu de PDFiumPas n'échoue pas à chaque exécution, ni même à la plupart des exécutions, car il faut que deux choses spécifiques atterrissent sur la même instance TPdf en même temps : un appel de rastérisation déjà en cours, et un UnloadPage ou ReloadPage concurrent arrivant à l'intérieur de cette même fenêtre. Les tests monothreads n'exercent jamais du tout ce chemin, et même des charges de travail véritablement multithreads ne le déclenchent que lorsqu'un rendu en arrière-plan et un événement de cycle de vie de document se trouvent se chevaucher pendant la durée de vie d'une page. Le déclencheur le plus réaliste est le pré-rendu PDF en arrière-plan construit sur des futures annulables, où un thread de travail rastérise la page suivante pendant que le thread d'interface recharge ou décharge la page courante sur action de l'utilisateur
FPDFImageObj_GetBitmap et FPDFPage_GetThumbnailAsBitmap parcourent des structures d'objets de page qu'UnloadPage est libre de libérer en plein parcours, si bien qu'une course qui se déclenche réellement ne produit pas toujours immédiatement une violation d'accès non plus. Une structure lue un instant trop tard peut tout aussi facilement renvoyer des pixels de charabia, ou corrompre des métadonnées de tas qui ne font planter que plusieurs allocations sans rapport plus tard, dans une fonction qui n'a jamais touché à une page PDF. C'est la raison honnête pour laquelle cette classe de bogue peut survivre dans une base de code sur plusieurs cycles de release : la trace de pile au point de l'échec ne pointe presque jamais nulle part près des six lignes auxquelles il manquait effectivement un verrou
Ce qui change pour les appelants
GetBitmap, GetObjectBitmap, GetThumbnail, et la surcharge HDC de RenderPage conservent leurs signatures publiques exactement telles qu'elles étaient, car la correction est un verrouillage interne ajouté autour d'appels existants plutôt qu'une migration. Il vaut la peine de se rappeler que le verrou de rendu est limité par instance TPdf, pas global au processus, si bien que deux threads rendant deux documents chargés séparément s'exécutent toujours pleinement en parallèle — le verrou ne sérialise que les opérations contre le seul document que les deux threads se trouvent partager. Si votre verrouillage est déjà sain et que les rendus semblent tout de même lents sous zoom ou défilement, c'est une question différente, à laquelle répond l'article sur le cache de rendu PDFium et les tactiques de performance du zoom — la correction et la vitesse sont des axes séparés ici, et cette correction ne touche que le premier
Six méthodes et une surcharge jumelle sont une petite fraction de la surface PDFium que PDFiumPas expose, mais c'était la fraction qui ne se comportait mal que sous une charge que personne ne se trouvait exécuter dans un débogueur. Le verrou de rendu lui-même, et l'ensemble complet des points d'entrée de rendu qu'il couvre désormais, font partie du composant PDFium pour Delphi, C++Builder, et Lazarus/FPC