مقاله فنی

شش فراخوانی PDFium که قفل رندر را در Delphi فراموش کردند

قفل رندر در PDFiumPas یک بخش بحرانی به‌ازای هر سند است — EnterRenderLock و LeaveRenderLock، پشتیبانی‌شده با یک فیلد TRTLCriticalSection روی TPdf — که قصد دارد هر فراخوانی به درون رستری‌ساز PDFium را بپیچد پس یک صفحه نتواند در حین یک رندر در حال پرواز از زیر پا آزاد یا دوباره بارگذاری شود. شش متد به‌طور مساوی بین TPdf و TPdfView تقسیم شده که مستقیم APIهای استخراج بیت‌مپ و بندانگشتی PDFium را فرا می‌خواندند و کاملاً از آن قفل رد می‌شدند، شکافی که PDFiumPas نسخه‌ی v2.26.0 با پیچیدن هر شش تا در همان جفت قفلی که هر نقطه ورود رندر دیگر از پیش استفاده می‌کرد بست

شکاف پوشش‌داده‌شده در اینجا همان پاس تقویت-ABI پوشش‌داده‌شده جای دیگری در این وبلاگ نیست، که یک عدم‌تطابق قرارداد فراخوانی cdecl و یک بریدگی عرض اشاره‌گر FPC Win64 در همان binding از PDFium را طی کرد. آنچه در ادامه می‌آید محدودتر و مکانیکی‌تر است: یک چک‌لیست پوشش-قفل برای شش نقطه‌ی فراخوانی که همگی به درون مسیر رندر PDFium دست دراز می‌کنند، چرا هرکدام آسان بود از قلم بیفتد، و چرا race که از قفل گم‌شده نتیجه می‌شود یکی از سخت‌ترین نقص‌ها در این codebase برای بازتولید در تقاضاست

قفل رندر واقعاً از چه چیزی محافظت می‌کند

PDFiumPas رندر را سریالایز می‌کند چون صفحه‌ی بارشده‌ی PDFium برای خواندن از یک رشته درحالی‌که رشته‌ی دیگری آزاد است آن را آزاد کند، ایمن نیست. TPdf یک TRTLCriticalSection در FRenderLock در اختیار دارد، مقداردهی‌شده در سازنده و پاسداری‌شده با یک پرچم FRenderLockReady پس یک فراخوانی که پس از تخریب برسد به یک no-op خاموش تبدیل می‌شود به‌جای ورود به یک بخش بحرانی حذف‌شده. EnterRenderLock و LeaveRenderLock تنها راه تأییدشده‌ی ورود و خروج از آن بخش هستند

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

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

TPdf.RenderPage، RenderTile، و RenderPageProgressive پیش از اینکه این ممیزی خاص اصلاً شروع شود از پیش آن انضباط را دنبال می‌کردند، هرکدام پیش از فراخوانی به درون PDFium قفل را می‌گرفتند و آن را در یک بلوک finally آزاد می‌کردند پس یک pre-render پس‌زمینه و یک UnloadPage پیش‌زمینه روی همان نمونه‌ی TPdf نمی‌توانند همپوشانی داشته باشند. شکافی که PDFiumPas نسخه‌ی v2.26.0 پیدا کرد در آن نقاط ورود آشکار نبود — در شش متدی نمایان شد که شبیه accessor به‌نظر می‌رسند نه رندر، هرچند هر یک از آن‌ها از PDFium می‌خواهد پیکسل‌ها را پیش از اینکه بتواند چیزی برگرداند رستری کند

کدام شش فراخوانی از قفل رندر رد شدند؟

TPdf.GetObjectBitmap، TPdf.GetBitmap، و TPdf.GetThumbnail نیمی از فهرست را تشکیل می‌دادند، و TPdfView.GetObjectBitmap، TPdfView.GetBitmap، و TPdfView.GetThumbnail نیمه‌ی دیگر را — همان سه عملیات، تکرارشده در سراسر دو کلاس کامپوننتی که همان صفحه‌ی زیرین را در معرض دید می‌گذارند. هر شش تا در نهایت یا FPDFImageObj_GetBitmap یا FPDFPage_GetThumbnailAsBitmap را فرا می‌خوانند، و هر دو آن نقطه ورود PDFium در همان‌جا رستری می‌کنند به‌جای اینکه یک ارجاع به چیزی که از پیش رندر شده پس بدهند. هیچ چیزی در هیچ‌کدام از آن شش نام متد نمی‌گوید رندر، که توضیحی معقول است برای اینکه چرا آن‌ها اولین‌بار در برابر همان چک‌لیست RenderPage و RenderTile نوشته نشدند

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;

چرا TPdfView فراخوانی قفلش را با یک بررسی nil محافظت می‌کند

TPdfView بخش بحرانی خودش را در اختیار ندارد — هر یک از شش فراخوانی قفلش را به FPdf.EnterRenderLock و FPdf.LeaveRenderLock forward می‌کند، پیچیده‌شده در یک بررسی که ابتدا ارجاع TPdf مرتبط nil نباشد. آن نگهبان وجود دارد چون یک TPdfView می‌تواند در زمان طراحی روی یک فرم بنشیند، یا به‌طور مختصر بین بسته‌شدن یک سند و بازشدن سند بعدی، بدون هیچ TPdfای که هنوز به FPdf اختصاص یافته باشد. رد‌کردن این نگهبان یک کرش را با کرش دیگری معامله می‌کرد، چون یک فراخوانی قفل‌کردن در برابر یک ارجاع nil به‌همان‌اندازه‌ی race که قفل برای جلوگیری از آن وجود دارد، نامتناسب شکست می‌خورد

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;

چرا RenderPage(HDC) به همین ممیزی تعلق دارد؟

TPdfView.RenderPage در برابر یک context دستگاه یکی از آن شش تا نیست — این یک انتشار زودتر، در PDFiumPas v2.25.0، نمایان شد، و جایگاهی در این چک‌لیست به‌دست می‌آورد چون همان نقص است که امضای متفاوتی پوشیده. آن overload مستقیم FPDF_RenderPage را فرا می‌خواند بدون EnterRenderLock و بدون فراخوانی SetArithmeticMaskای که در برابر استثناهای FPU روی کامپایلرهای قدیمی‌تر Delphi محافظت می‌کند، درحالی‌که overload از نوع TBitmap که چند خط پایین‌تر در همان کلاس نشسته از پیش هر دو را حمل می‌کرد. اینکه دو پاس ممیزی همان حالت شکست را یک انتشار بعد از هم بگیرند، کمتر درباره‌ی هر متد تکی می‌گوید و بیشتر درباره‌ی شکل باگ: این در هر overloadای پنهان می‌شود که هیچ‌کس آن را به‌محض اینکه خواهر و برادرش درست به‌نظر می‌رسد دوباره نمی‌خواند

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;

چرا این race تقریباً غیرممکن است برای بازتولید؟

شکاف قفل رندر PDFiumPas در هر اجرا، یا حتی در اغلب اجراها، شکست نمی‌خورد، چون به دو چیز مشخص نیاز دارد تا همزمان روی همان نمونه‌ی TPdf فرود بیایند: یک فراخوانی رستری‌سازی از پیش در پرواز، و یک UnloadPage یا ReloadPage همزمان که درون همان پنجره می‌رسد. تست تک‌رشته‌ای هرگز این مسیر را اصلاً اعمال نمی‌کند، و حتی حجم‌کارهای واقعاً چندرشته‌ای فقط زمانی آن را به لغزش می‌اندازند که یک رندر پس‌زمینه و یک رخداد چرخه‌ی عمر سند اتفاقاً درون طول عمر یک صفحه همپوشانی داشته باشند. واقع‌گرایانه‌ترین ماشه پیش‌رندر پس‌زمینه‌ی PDF ساخته‌شده روی future‌های قابل‌لغو است، جایی که یک رشته‌ی کارگر صفحه‌ی بعدی را رستری می‌کند درحالی‌که رشته‌ی UI صفحه‌ی فعلی را با ورودی کاربر دوباره بارگذاری یا آزاد می‌کند

FPDFImageObj_GetBitmap و FPDFPage_GetThumbnailAsBitmap ساختارهای شیء-صفحه را می‌پیمایند که UnloadPage آزاد است در میانه‌ی پیمایش آزاد کند، پس یک race که واقعاً شلیک می‌شود همیشه یک access violation فوری هم تولید نمی‌کند. یک ساختار که یک لحظه دیر خوانده شده به‌همان‌راحتی می‌تواند پیکسل‌های زباله پس بدهد، یا metadata heap را خراب کند که فقط چند تخصیص بی‌ربط بعدتر، در یک تابعی که هرگز یک صفحه‌ی PDF را لمس نکرده، کرش می‌کند. این دلیل صادقانه‌ای است که این رده از باگ می‌تواند در سراسر چند چرخه‌ی انتشار در یک codebase زنده بماند: stack trace در نقطه‌ی شکست به‌ندرت به هرجایی نزدیک آن شش خطی که واقعاً یک قفل را کم داشتند اشاره می‌کند

چه چیزی برای فراخوانندگان تغییر می‌کند

GetBitmap، GetObjectBitmap، GetThumbnail، و overload از نوع HDC در RenderPage امضاهای عمومی‌شان را دقیقاً همان‌طور که بودند نگه می‌دارند، چون این رفع اشکال قفل‌گذاری داخلی افزوده‌شده در اطراف فراخوانی‌های موجود است نه یک مهاجرت. ارزش دارد به‌خاطر داشته باشید که قفل رندر به‌ازای هر نمونه‌ی TPdf محدود است، نه سراسری برای فرآیند، پس دو رشته که دو سند بارشده‌ی جداگانه را رندر می‌کنند همچنان کاملاً به‌موازات اجرا می‌شوند — قفل فقط عملیات‌ها را در برابر همان یک سندی که هر دو رشته اتفاقاً به اشتراک می‌گذارند سریالایز می‌کند. اگر قفل‌گذاری شما از پیش درست است و رندرها همچنان زیر زوم یا اسکرول کند به‌نظر می‌رسند، آن یک سؤال متفاوت است، پاسخ‌داده‌شده در مقاله‌ی کش رندر PDFium و تاکتیک‌های کارایی زوم — درستی و سرعت اینجا محورهای جداگانه‌ای هستند، و این رفع اشکال فقط اولی را لمس می‌کند

شش متد و یک overload خواهر بخش کوچکی از سطح PDFium هستند که PDFiumPas در معرض دید می‌گذارد، اما آن‌ها همان بخشی بودند که فقط زیر باری بدرفتاری می‌کردند که هیچ‌کس اتفاقاً درون یک دیباگر اجرا نمی‌کرد. خودِ قفل رندر، و مجموعه‌ی کامل نقاط ورود رندر که حالا پوشش می‌دهد، به‌عنوان بخشی از کامپوننت PDFium برای Delphi، C++Builder، و Lazarus/FPC عرضه می‌شوند