PDFiumPas' gjengivelseslås er en per-dokument kritisk seksjon — EnterRenderLock og LeaveRenderLock, støttet av et TRTLCriticalSection-felt på TPdf — ment å pakke inn hvert kall inn i PDFiums rasteriserer, slik at en side ikke kan lastes ut eller lastes inn på nytt under en pågående gjengivelse. Seks metoder delt likt mellom TPdf og TPdfView kalte PDFiums bitkart- og miniatyrbilde-uttrekkings-API-er direkte og hoppet over den låsen fullstendig, et gap PDFiumPas v2.26.0 lukket ved å pakke inn alle seks i det samme låseparet hvert annet gjengivelsesinngangspunkt allerede brukte
Gapet dekket her er ikke ABI-herdingspasseringen dekket andre steder på denne bloggen, som gikk gjennom et cdecl-kallekonvensjons-mismatch og en FPC Win64-pekerbredde-avkutting i den samme PDFium-bindingen. Det som følger, er snevrere og mer mekanisk: en lås-dekningssjekkliste for seks kallesteder som alle rekker inn i PDFiums gjengivelsesvei, hvorfor hver av dem var lett å overse, og hvorfor kappløpet som følger av manglende lås, er en av de vanskeligere feilene i denne kodebasen å reprodusere på bestilling
Hva gjengivelseslåsen faktisk beskytter
PDFiumPas serialiserer gjengivelse fordi PDFiums innlastede side ikke er trygg å lese fra én tråd mens en annen tråd står fritt til å frigjøre den. TPdf eier en TRTLCriticalSection i FRenderLock, initialisert i konstruktøren og vaktet av et FRenderLockReady-flagg slik at et kall som ankommer etter nedrivning, blir en stille no-op i stedet for å gå inn i en slettet kritisk seksjon. EnterRenderLock og LeaveRenderLock er den eneste sanksjonerte veien inn og ut av den seksjonen
procedure TPdf.EnterRenderLock;
begin
if FRenderLockReady then
EnterCriticalSection(FRenderLock);
end;
procedure TPdf.LeaveRenderLock;
begin
if FRenderLockReady then
LeaveCriticalSection(FRenderLock);
end;
TPdf.RenderPage, RenderTile, og RenderPageProgressive fulgte allerede den disiplinen før nettopp denne revisjonen noensinne startet, hver av dem tar låsen før den kaller inn i PDFium og slipper den i en finally-blokk slik at en bakgrunns-forhåndsgjengivelse og en forgrunns-UnloadPage på den samme TPdf-instansen ikke kan overlappe. Gapet PDFiumPas v2.26.0 fant, var ikke i de åpenbare inngangspunktene — det dukket opp i seks metoder som leser som aksessorer snarere enn gjengivelser, selv om hver eneste av dem ber PDFium om å rasterisere piksler før den kan returnere noe som helst
Hvilke seks kall hoppet over gjengivelseslåsen?
TPdf.GetObjectBitmap, TPdf.GetBitmap, og TPdf.GetThumbnail utgjorde halvparten av listen, og TPdfView.GetObjectBitmap, TPdfView.GetBitmap, og TPdfView.GetThumbnail utgjorde den andre halvparten — de samme tre operasjonene, duplisert på tvers av de to komponentklassene som eksponerer den samme underliggende siden. Alle seks kaller til slutt enten FPDFImageObj_GetBitmap eller FPDFPage_GetThumbnailAsBitmap, og begge de PDFium-inngangspunktene rasteriserer på stedet snarere enn å overlevere en referanse til noe allerede gjengitt. Ingenting i noen av de seks metodenavnene sier render, noe som er en rimelig forklaring på hvorfor de ikke ble skrevet mot den samme sjekklisten som RenderPage og RenderTile første gang rundt
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;
Hvorfor TPdfView vokter sitt låsekall med en nil-sjekk
TPdfView eier ikke sin egen kritiske seksjon — hvert eneste av dens seks låsekall videresender til FPdf.EnterRenderLock og FPdf.LeaveRenderLock, pakket inn i en sjekk om at den tilknyttede TPdf-referansen ikke er nil først. Den vakten finnes fordi en TPdfView kan sitte på et skjema ved designtid, eller kort mellom at ett dokument lukkes og det neste åpnes, med ingen TPdf tildelt FPdf ennå. Å hoppe over vakten ville byttet ett krasj mot et annet, ettersom et låsekall mot en nil-referanse feiler ikke mer elegant enn kappløpet låsen finnes for å forhindre
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;
Hvorfor hører RenderPage(HDC) hjemme i den samme revisjonen?
TPdfView.RenderPage mot en enhetskontekst er ikke en av de seks — den dukket opp en utgivelse tidligere, i PDFiumPas v2.25.0, og den fortjener en plass i denne sjekklisten fordi det er den samme defekten i en annen drakt. Den overloaden kalte FPDF_RenderPage rett gjennom uten verken EnterRenderLock eller SetArithmeticMask-kallet som vokter mot FPU-unntak på eldre Delphi-kompilatorer, mens TBitmap-overloaden som satt noen linjer under den i den samme klassen, allerede bar begge. To revisjonspasseringer som fanger den samme feilmodusen en utgivelse fra hverandre, sier mindre om noen enkelt metode og mer om formen på bugen: den gjemmer seg i hvilken som helst overload ingen leser på nytt så snart søsteren dens 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;
Hvorfor er dette kappløpet nesten umulig å reprodusere?
PDFiumPas' gjengivelseslås-gap feiler ikke ved hver kjøring, eller til og med de fleste kjøringer, fordi det trenger to spesifikke ting å lande på den samme TPdf-instansen samtidig: et rasteriseringskall allerede i gang, og en samtidig UnloadPage eller ReloadPage som ankommer innenfor det samme vinduet. Enkelttråd-testing utøver aldri veien i det hele tatt, og selv genuint multi-trådede arbeidsbelastninger utløser det bare når en bakgrunnsgjengivelse og en dokumentlivssyklushendelse tilfeldigvis overlapper innenfor én sides levetid. Den mest realistiske utløseren er bakgrunns-PDF-forhåndsgjengivelse bygget på avbrytbare futures, der en arbeidertråd rasteriserer den neste siden mens UI-tråden laster inn på nytt eller ut den gjeldende ved brukerinndata
FPDFImageObj_GetBitmap og FPDFPage_GetThumbnailAsBitmap går gjennom side-objekt-strukturer UnloadPage står fritt til å frigjøre midt i gjennomgangen, så et kappløp som faktisk utløses, produserer heller ikke alltid et umiddelbart tilgangsbrudd. En struktur lest et øyeblikk for sent kan like gjerne overlevere søppel-piksler, eller korrumpere heap-metadata som først krasjer flere urelaterte allokeringer senere, i en funksjon som aldri rørte en PDF-side. Det er den ærlige grunnen til at denne klassen av bug kan overleve i en kodebase over flere utgivelsessykluser: stack trace-en ved feilpunktet peker sjelden noe sted i nærheten av de seks linjene som faktisk manglet en lås
Hva som endrer seg for kallere
GetBitmap, GetObjectBitmap, GetThumbnail, og HDC-overloaden av RenderPage beholder sine offentlige signaturer nøyaktig som de var, ettersom fiksen er intern låsing lagt til rundt eksisterende kall snarere enn en migrering. Det er verdt å huske at gjengivelseslåsen er avgrenset per TPdf-instans, ikke global for prosessen, så to tråder som gjengir to separat innlastede dokumenter, kjører fortsatt fullt parallelt — låsen serialiserer bare operasjoner mot det ene dokumentet begge trådene tilfeldigvis deler. Hvis låsingen din allerede er sunn og gjengivelser fortsatt føles trege under zoom eller rulling, er det et annet spørsmål, besvart i artikkelen om PDFium-gjengivelsesbuffer og zoom-ytelsestaktikker — korrekthet og hastighet er separate akser her, og denne fiksen rører bare den første
Seks metoder og en søster-overload er en liten brøkdel av PDFium-overflaten PDFiumPas eksponerer, men de var brøkdelen som bare oppførte seg galt under last ingen tilfeldigvis kjørte i en debugger. Selve gjengivelseslåsen, og det fulle settet med gjengivelsesinngangspunkter den nå dekker, følger med som en del av PDFium-komponenten for Delphi, C++Builder, og Lazarus/FPC