قفل الرسم في PDFiumPas قسم حرج لكل مستند — EnterRenderLock وLeaveRenderLock، مدعومان بحقل TRTLCriticalSection في TPdf — يُقصد به تغليف كل استدعاء إلى مُرقِّم PDFium بحيث لا يمكن إلغاء تحميل صفحة أو إعادة تحميلها من تحت رسم قيد التنفيذ. استدعت ست طرائق موزعة بالتساوي بين TPdf وTPdfView واجهات استخراج الصور النقطية والصور المصغّرة الخاصة بـPDFium مباشرة وتخطّت ذلك القفل كليًا، وهي فجوة أغلقها PDFiumPas v2.26.0 بتغليف الستة جميعًا في زوج القفل نفسه الذي استخدمته بالفعل كل نقطة دخول رسم أخرى
الفجوة المغطاة هنا ليست تمريرة تحصين ABI المغطاة في مكان آخر من هذه المدونة، التي استعرضت عدم تطابق اصطلاح استدعاء cdecl واقتطاع عرض مؤشر في FPC Win64 في الربط نفسه بـPDFium. ما يلي أضيق وأكثر ميكانيكية: قائمة تحقق من تغطية القفل لستة مواقع استدعاء تصل جميعها إلى مسار الرسم في PDFium، ولماذا كان من السهل تفويت كل واحد منها، ولماذا يُعد التسابق الناتج عن غياب القفل واحدًا من أصعب العيوب في إعادة إنتاجه عند الطلب في قاعدة الشيفرة هذه
ما الذي يحميه قفل الرسم فعليًا
يُسلسل PDFiumPas الرسم لأن الصفحة المحمَّلة في PDFium ليست آمنة للقراءة من خيط واحد بينما خيط آخر حر لتحريرها. تمتلك TPdf TRTLCriticalSection في FRenderLock، مُهيَّأ في المنشئ ومحروس بعلم FRenderLockReady بحيث يصبح استدعاء يصل بعد التفكيك عملية عدم فعل شيء صامتة بدلًا من دخول قسم حرج محذوف. 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 بحيث لا يمكن لرسم مسبق في الخلفية واستدعاء UnloadPage في المقدمة على مثيل TPdf نفسه أن يتداخلا. الفجوة التي وجدها PDFiumPas v2.26.0 لم تكن في نقاط الدخول الواضحة تلك — ظهرت في ست طرائق تُقرأ كواصلات (accessors) لا كعمليات رسم، رغم أن كل واحدة منها تطلب من 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، مغلَّفًا في فحص أن مرجع TPdf المرتبط ليس nil أولًا. يوجد ذلك الحارس لأن TPdfView يمكن أن تجلس على نموذج وقت التصميم، أو بإيجاز بين إغلاق مستند وفتح التالي، دون أي TPdf مُعيَّن لـFPdf بعد. تخطي الحارس كان سيبادل عطلًا واحدًا بآخر، بما أن استدعاء قفل مقابل مرجع nil يفشل بلا لطف أكثر من التسابق الذي وُجد القفل لمنعه
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 مقابل سياق جهاز ليست واحدة من الست — ظهرت في إصدار سابق، PDFiumPas v2.25.0، وتستحق مكانًا في قائمة التحقق هذه لأنها العيب نفسه مرتديًا توقيعًا مختلفًا. استدعى ذلك التحميل الزائد FPDF_RenderPage مباشرة دون EnterRenderLock ولا استدعاء SetArithmeticMask الذي يحرس ضد استثناءات وحدة الفاصلة العائمة على مترجمات Delphi الأقدم، بينما التحميل الزائد لـTBitmap الجالس على بعد بضعة أسطر أسفله في الفئة نفسها كان يحمل الاثنين بالفعل. تمريرتا تدقيق تلتقطان نمط الفشل نفسه بفارق إصدار واحد يقولان أقل عن أي طريقة منفردة وأكثر عن شكل الخلل: إنه يختبئ في أيًّا كان التحميل الزائد الذي لا يعيد أحد قراءته بمجرد أن يبدو شقيقه صحيحًا
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;
لماذا هذا التسابق شبه مستحيل إعادة إنتاجه؟
لا تفشل فجوة قفل الرسم في PDFiumPas في كل تشغيل، ولا حتى في معظم التشغيلات، لأنها تحتاج شيئين محددين ليقعا على مثيل TPdf نفسه في آن واحد: استدعاء ترقيم قيد التنفيذ بالفعل، وUnloadPage أو ReloadPage متنافس يصل داخل النافذة نفسها. لا يمارس الاختبار أحادي الخيط المسار على الإطلاق، وحتى أعباء العمل متعددة الخيوط حقًا لا تُفعّله إلا عندما يصادف أن يتداخل رسم في الخلفية وحدث دورة حياة مستند ضمن عمر صفحة واحدة. المُحفِّز الأكثر واقعية هو الرسم المسبق لـPDF في الخلفية المبني على مستقبلات (futures) قابلة للإلغاء، حيث يُرقّم خيط عامل الصفحة التالية بينما يعيد خيط الواجهة تحميل أو إلغاء تحميل الصفحة الحالية استجابة لإدخال المستخدم
تجتاز FPDFImageObj_GetBitmap وFPDFPage_GetThumbnailAsBitmap بنى كائنات صفحة حرة UnloadPage في تحريرها في منتصف الاجتياز، بحيث لا ينتج تسابق يُطلَق فعليًا دائمًا خرق وصول فوري أيضًا. يمكن لبنية تُقرأ متأخرة لحظة بسهولة أن تعيد بكسلات عبثية، أو تفسد بيانات كومة وصفية لا تُعطِّل إلا عدة تخصيصات غير مرتبطة لاحقًا، في دالة لم تلمس صفحة PDF قط. هذا هو السبب الصادق وراء إمكانية بقاء هذه الفئة من الأخطاء في قاعدة شيفرة عبر عدة دورات إصدار: تتبع المكدس عند نقطة الفشل نادرًا ما يشير إلى أي مكان قرب الأسطر الستة التي كانت تفتقد قفلًا فعليًا
ما الذي يتغير للمستدعين
تحتفظ GetBitmap، وGetObjectBitmap، وGetThumbnail، والتحميل الزائد HDC لـRenderPage، بتوقيعاتها العامة تمامًا كما كانت، بما أن الإصلاح قفل داخلي أُضيف حول استدعاءات موجودة لا ترحيلًا. يستحق الأمر التذكر أن قفل الرسم محصور بنطاق كل مثيل TPdf، لا عام على العملية، بحيث لا يزال خيطان يرسمان مستندين محمَّلين بشكل منفصل يعملان بالتوازي الكامل — يُسلسل القفل فقط العمليات مقابل المستند الواحد الذي يصادف أن يتشاركه الخيطان. إذا كان قفلك سليمًا بالفعل ولا تزال عمليات الرسم تبدو بطيئة تحت التكبير أو التمرير، فهذا سؤال مختلف، تُجاب عنه في مقالة تكتيكات ذاكرة التخزين المؤقت للرسم وأداء التكبير في PDFium — الصحة والسرعة محوران منفصلان هنا، ولا يمسّ هذا الإصلاح سوى الأول
ست طرائق وتحميل زائد شقيق واحد جزء صغير من سطح PDFium الذي يعرضه PDFiumPas، لكنها كانت الجزء الذي أساء التصرف فقط تحت حمل لم يصادف أن يشغّله أحد في مُصحح أخطاء. يُشحن قفل الرسم نفسه، والمجموعة الكاملة من نقاط دخول الرسم التي يغطيها الآن، كجزء من مكوّن PDFium لـDelphi وC++Builder وLazarus/FPC