Teknisk artikkel

Seks PDFium-kall som glemte gjengivelseslåsen i Delphi

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