عندما ترفض مكتبة pdfium.dll التحميل على جهاز العميل، يحول مكون PDFium لـ Delphi و Lazarus الخطأ الأصلي الغامض إلى سبب محدد؛ حيث تقرأ الدالة الروتينية CheckLoadLibrary الخطأ ERROR_BAD_EXE_FORMAT (193) كعدم تطابق في بنية المعالج ٣٢/٦٤ بت، وتبلغ CheckGetProcAddress عن تصدير مفقود كـ pdfium.dll أقدم من الربط (binding)؛ وتحدد كلا الرسالتين الإصلاح بدلاً من تركك للتخمين
ويهمنا ذلك لأن مكتبة DLL الأصلية تفشل بثلاث طرق متميزة، ويدمج النص الخام لنظام التشغيل بينها جميعاً؛ فقد تكون البنية خاطئة، أو قد يكون الملف الثنائي المنشور قديماً جداً بالنسبة لربط Pascal الذي يستدعيه، أو قد يتسابق خيطان (threads) لربطه في نفس اللحظة؛ ولكل منها إصلاح مختلف، والهدف الكامل من التشخيصات المضافة في عمل دورة حياة التحميل للإصدار v2.11.0 هو إخبارك بأي منها تواجه بالفعل
لماذا يفشل تحميل pdfium.dll مع خطأ تنسيق EXE غير صالح؟
يعني خطأ ويندوز 193، ERROR_BAD_EXE_FORMAT، أنه تم العثور على الملف وفتحه ولكن نوع آلة PE الخاصة به لا يطابق العملية المضيفة؛ فلا يمكن لملف تنفيذي بقوة ٦٤ بت تحميل مكتبة pdfium.dll بقوة ٣٢ بت، ولا يمكن لملف تنفيذي بقوة ٣٢ بت تحميل مكتبة بقوة ٦٤ بت؛ والملف موجود، ولذلك فإن غريزة البحث عن ملف DLL مفقود ترسلك في الاتجاه الخاطئ تماماً
ويعترض مكون PDFium هذه الحالة في CheckLoadLibrary: فعندما تعيد LoadLibrary مقبضاً فارغاً (null handle) وتكون قيمة GetLastError هي 193، يلحق المكون تلميحاً بأنه تم العثور على ملف DLL ولكن بنيته لا تطابق العملية المضيفة، ويشير إلى بناء DLLs/Win32 أو DLLs/Win64 المطابق؛ ويتم اختيار الدليل الفرعي بواسطة BuildDllSubDir، والتي ترجع Win64 عندما يكون IsWin64 صحيحاً ويرجع Win32 خلاف ذلك؛ وانشر ملف DLL تحت الشجرة التي يبحث عنها الربط بالفعل وسيختفي عدم التطابق
uses
SysUtils, PDFium;
procedure OpenDocument(const AFileName: string);
var
Pdf: TPdf;
begin
// انشر pdfium.dll بجوار الملف التنفيذي، متوافقاً مع هدف البناء:
// <AppDir>\DLLs\Win32\pdfium.dll لعملية مضيفة بقوة ٣٢ بت
// <AppDir>\DLLs\Win64\pdfium.dll لعملية مضيفة بقوة ٦٤ بت
Pdf := TPdf.Create(nil);
try
Pdf.FileName := AFileName;
try
Pdf.Active := True; // يربط الاستخدام الأول pdfium.dll بشكل كسول
except
on E: EPdfError do
// تذكر الرسالة بالفعل السبب الحقيقي: عدم تطابق ٣٢/٦٤ بت
// (ERROR_BAD_EXE_FORMAT) أو ملف pdfium.dll أقدم من هذا الربط
raise Exception.CreateFmt('PDF engine did not start: %s', [E.Message]);
end;
if Pdf.Active then
RenderFirstPage(Pdf);
finally
Pdf.Free;
end;
end;
ما الذي يخبرك به تصدير PDFium المفقود؟
فئة الفشل الثانية هي انحراف الإصدار؛ حيث تشحن PDFium عمليات تصدير جديدة بمرور الوقت، ويحل ربط Pascal كل دالة يحتاجها من خلال CheckGetProcAddress أثناء LoadLibrary؛ فإذا غاب تصدير مطلوب، يكون الربط أحدث من ملف pdfium.dll الموجود على القرص، والتشخيص الصادق هو أن ملف DLL المنشور قديم وليس تالفاً؛ وتذكر الدالة CheckGetProcAddress ذلك بدقة: فهي تثير الخطأ EPdfError مبلغة أن ملف pdfium.dll المنشور أقدم من هذا البناء لربط PDFiumPas، وتسمي مسار DLLs/<subdir> الذي ينتمي إليه الملف الثنائي المطابق
وهناك تفصيلان يجعلان هذا المسار قويًا؛ أولاً، تستدعي CheckGetProcAddress الدالة UnloadLibrary قبل أن تثير الخطأ، ولذلك لا يترك الربط الجزئي أبدًا مؤشرات دوال محلولة جزئيًا خلفه لتتعثر فيها المحاولة التالية؛ ثانياً، اسم DLL الذي تبلغ عنه يأتي من BuildDllName، والتي ترجع pdfium.dll بشكل طبيعي وترجع pdfium.v8.dll عندما يتم تعيين علم EnableV8Engine العالمي؛ فإذا قام تطبيقك بتمكين محرك جافا سكريبت V8 للنماذج XFA أو المكتوبة ببرامج نصية، يشير نص الخطأ إلى بناء V8، وليس البناء العادي، وبذلك تستبدل الملف الصحيح من المرة الأولى
هل تحميل مكتبة PDFium DLL آمن من خيط خلفي؟
تحميل المكتبة الآن آمن من أي خيط (thread)، ولكن استدعاء واجهة برمجة تطبيقات PDFium من عدة خيوط لا يزال غير آمن؛ وهذان ضمانان منفصلان، وإبقاؤهما منفصلين يمثل الفرق بين تجمع عمال مستقر وانهيار متقطع؛ وعادة ما يقوم العرض الخلفي بتشغيل عرض كل صفحة من خلال عامل TPdfFuture<T>، والمستقبل الأول الذي يلمس TPdf هو ما يطلق الربط الكسول؛ وبدون حماية، يمكن لعاملين كلاهما مراقبة مكتبة غير محملة، وتشغيل تسلسل الربط، وتجاوز مقبض الوحدة، وتسريب التحميل الأول
ويغلق العمل في الإصدار v2.11.0 هذه النافذة باستخدام PDFiumLoadLock، وهو قسم حرج على مستوى العملية TRTLCriticalSection يغلف الدخول لكل من LoadLibrary و UnloadLibrary؛ ويجعل القسم الحرج تسلسل الفحص ثم التحميل ذرياً (atomic)، وبذلك لا يمكن لخيطين كلاهما رؤية Loaded=False والربط المزدوج، ولا يمكن لأي خيط تحرير ملف DLL بينما يكون خيط آخر في منتصف الربط؛ ويتم إنشاء القفل في قسم initialization للوحدة وهدمه في قسم finalization، محميًا بعلم PDFiumLoadLockReady بحيث تكون التوأمة آمنة حتى أثناء إيقاف التشغيل؛ وإذا كنت بحاجة إلى نمط العامل والرد الكامل مع الإلغاء، فإن المقال المصاحب حول العرض الخلفي مع المستقبلات القابلة للإلغاء يمر عبره من البداية إلى النهاية
uses
PDFium, FPdfAsync;
// تعمل طريقة العامل على خيط خلفي. المستقبل الأول الذي يربط
// pdfium.dll يتم تسلسله بواسطة PDFiumLoadLock، ولذلك فإن عاملاً متزامناً ثانياً
// لا يمكنه الربط المزدوج أو تسريب مقبض الوحدة.
function TReportForm.RenderThumbnail(
const AToken: IPdfCancellationToken): TBitmap;
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.Active := True; // ربط متزامن آمن
AToken.ThrowIfCancelled;
Result := Pdf.Thumbnail; // هذا الـ TPdf must be owned by one thread only
finally
Pdf.Free;
end;
end;
procedure TReportForm.StartRender;
begin
TPdfFuture<TBitmap>.Run(RenderThumbnail, ThumbnailReady);
end;
procedure TReportForm.ThumbnailReady(
const AResult: TPdfFutureResult<TBitmap>);
begin
if AResult.IsSuccess then
Preview.Picture.Assign(AResult.Value);
end;
ما لا يحميه قفل التحميل
إن الحد هنا يستحق الذكر بوضوح، لأنه من السهل تفسير الإصلاح بأكثر مما يحتمل؛ حيث يقوم PDFiumLoadLock بتسلسل حالة الربط العالمية فقط: مقبض الوحدة وتعيينات مؤشرات دالة FPDF_*؛ ولا تزال واجهة برمجة تطبيقات PDFium C الأساسية غير آمنة للخيوط، تماماً كما تعلن العناوين الرئيسية، ولذلك فإن استدعاء دوال FPDF_* أو مشاركة مستند FPDF_DOCUMENT واحد عبر الخيوط يظل مسؤوليتك في التسلسل؛ والشكل الآمن هو الموضح أعلاه: يمتلك كل عامل كائن TPdf الخاص به ولا يسلم مستنداً حياً إلى خيط آخر أبداً؛ ولمعاملة أعمق لهذا الحد وقواعد ABI حولها، انظر المقال المصاحب حول تقوية ربط PDFium VCL ضد أخطاء ABI والذاكرة
لماذا تهم أقسام finalization و FPDF_DestroyLibrary
كانت هناك فجوة أكثر دقة تقع في مسار إيقاف التشغيل؛ فلم تكن وحدة PDFium تحتوي على قسم finalization، مما يعني أن دالة FPDF_DestroyLibrary لم يتم تشغيلها أبدًا عند خروج العملية؛ وقام نظام التشغيل باستصلاح ذاكرة DLL وتخطي تنظيف PDFium الخاص، وتحديدًا انضمام خيط العامل، والتخلص من عزل V8 (isolate) عند تعيين EnableV8Engine، وهدم الخطوط والتخزين المؤقت؛ وفي عبء عمل العرض العادي، يعد هذا تسريبًا صامتًا عند الهدم، ولكن مع تمكين محرك V8 فإنه يسرب عزل جافا سكريبت في كل تشغيل، وهو ما يظهر سريعًا تحت الأتمتة المتكررة
ويستدعي قسم finalization الآن الدالة UnloadLibrary، والتي تستدعي FPDF_DestroyLibrary وتفرغ الوحدة؛ ويعيش هذا في نطاق الوحدة لسبب: فدالة TPdf.Destroy تضبط فقط Active := False لإغلاق المستند الحالي، ولا تقوم عمدًا بتفريغ المكتبة العالمية، وبذلك يحتفظ التطبيق متعدد المثيلات بربط PDFium مشترك واحد حياً طوال فترة حياة العملية؛ ووضع الهدم في finalization يعني تحرير المكتبة المشتركة مرة واحدة بالضبط، عند تفريغ الوحدة، وتفحص UnloadLibrary علم Loaded الخاص بها أولاً بحيث يكون الاستدعاء عديم التأثير إذا لم يتم استخدام PDFium أبداً
قائمة مراجعة قصيرة للنشر
تمنع ثلاث عادات كل فشل في التحميل تقريبًا في الميدان: طابق بنية DLL مع العملية المضيفة واشحنها تحت DLLs/Win32 أو DLLs/Win64؛ وحافظ على بقاء pdfium.dll متزامناً مع إصدار الربط حتى لا يغيب أي تصدير مطلوب؛ ولا تشارك أبداً كائن TPdf أو مقبض مستند عبر الخيوط، على الرغم من أن الربط نفسه أصبح ذرياً الآن؛ وإذا قمت ببناء نفس المصدر لكل من Delphi و Lazarus، فإن فروق التعبئة التي تعرقل النشر عبر الأهداف المختلفة مغطاة في المقال المصاحب حول مزالق المترجمين المتقاطعين Delphi و FPC؛ ويتم شحن التشخيصات، وقفل التحميل، وتنظيف الإنهاء الموصوف هنا في مكون PDFium لـ Delphi و C++Builder