ملف pdfium.dll الخاص بك يُحمَّل بشكل جيد وإجراء واحد لا يزال مفقودًا. يتعامل PDFium Component مع هذا بتقسيم روابطه إلى فئتين: تصديرات مطلوبة تُحلّ عبر CheckGetProcAddress، التي تُلغي التحميل تمامًا، وتصديرات اختيارية تُحلّ عبر TryGetProcAddress، التي تترك مؤشر nil وفحص قدرة خلفها بدلًا من ذلك
هذه ليست نفس مشكلة DLL لا يمكن العثور عليها. إذا كان تطبيقك يموت بخطأ تنسيق EXE سيء، أو ملف مفقود، أو عدم تطابق معماري، تلك القصة مشروحة في المقال المرافق عن نشر pdfium.dll وتشخيص فشل التحميل. هنا نجح المُحمِّل. مقبض الوحدة صالح، وتصديرات المئات حُلّت، ولا يزال التشغيل ينتهي قبل عرض صفحتك الأولى لأن نقطة دخول واحدة وصلت في بناء PDFium أحدث ليست في الملف الثنائي على القرص
لماذا يُعطّل تصدير واحد مفقود المكتبة بأكملها؟
لأن الربط المطلوب عقد صارم، ويُفرَض خلال سلسلة ربط واحدة الكل-أو-لا-شيء. يحلّ PDFium Component جدول تصديره بأكمله داخل LoadLibrary، استدعاء CheckGetProcAddress تلو الآخر. أول نتيجة nil تُثير EPdfError وتستدعي UnloadLibrary قبل ذلك، وهذا متعمَّد: ربط جزئي كان سيترك بخلاف ذلك مؤشرات مُحلَّلة بالفعل تشير إلى وحدة على وشك التحرير، ما يُبطل بهدوء كل حارس Assigned في المسار
النتيجة هي نمط الفشل الذي يجلب الناس إلى هنا. تُرقّي المكوّن، وتشحن نفس ملف pdfium.dll الذي شحنته لسنتين، ولن يبدأ التطبيق. يُسمّي الخطأ تصديرًا لميزة لم تستدعها أبدًا. لا شيء تفعله عند موقع الاستدعاء يساعد، لأن موقع الاستدعاء لا يعمل أبدًا؛ حدث الفشل أثناء الربط، قبل فتح أي مستند
function CheckGetProcAddress(const Name: string): Pointer;
begin
Result := GetProcAddress(PDFiumLibrary, PChar(Name));
if Result = nil then
begin
// A missing required export means the deployed pdfium.dll is older
// than this build of the binding. Drop every pointer resolved so far
// so no caller can reach into the module we are about to free.
UnloadLibrary;
raise EPdfError.Create('Required PDFium export not found: ' + Name);
end;
end;
function TryGetProcAddress(const Name: string): Pointer;
begin
// Optional export. nil is a legitimate answer here; every caller is
// required to test Assigned() before dereferencing the variable.
Result := GetProcAddress(PDFiumLibrary, PChar(Name));
end;
مطلوب أم اختياري: أين يقع الخط فعليًا
القاعدة التي يُطبّقها PDFium Component صريحة. التصدير مطلوب عندما يجعل غيابه المكوّن غير قادر على أداء العمل الذي وُجد من أجله، واختياري عندما يُزيل غيابه فقط ميزة ورقية واحدة. FPDF_InitLibrary، وFPDF_LoadDocument، وFPDF_RenderPageBitmap، وFPDF_ClosePage مطلوبة، والفشل بصوت عالٍ فيها صحيح: عارض لا يستطيع العرض ليس عارضًا مُتدهورًا، إنه معطوب
كل شيء يصل إليه المُحمِّل المتسامح اليوم ورقة. وصلت FPDFBookmark_GetColor بعد M109 وتوفّر فقط مصفوفة اللون /C الاختيارية لمدخل مخطط تفصيلي، لذا DLL أقدم منها يُبلغ ببساطة عن عدم وجود لون إشارة مرجعية. مساعدو V8، FPDF_GetRecommendedV8Flags وFPDF_GetArrayBufferAllocatorSharedInstance، ومساعدو سلسلة XFA، FPDF_BStr_Init وFPDF_BStr_Set وFPDF_BStr_Clear، غائبون عن أي بناء بلا V8 بالتصميم، لذا معاملتهم كمطلوبين سيجعل ملف pdfium.dll العادي غير قابل للتحميل. والزوج الذي حفّز هذا المقال: FPDFAttachment_SetDescription وFPDFAttachment_GetDescription، المُضافان في المنبع بتاريخ 2026-07-13، بعد تاريخ بناء الملفات الثنائية الأربعة لـPDFium التي يشحنها المشروع تحت DLLs/Win32 وDLLs/Win64. تلك الحالة الأخيرة هي الشكل العام للمشكلة، لا حادثة منفردة: طبقة ربط تتتبع ترويسات المنبع، التي تتحرك باستمرار، بينما DLL في مُثبِّتك يتحرك بقفزات منفصلة كلما أعاد أحدهم بناءه. توجد دائمًا نافذة يعرف فيها جانب Pascal تصديرات لا يملكها الملف الثنائي المنشور، وتحديد أي جانب من خط المطلوب/الاختياري يقع فيه كل تصدير جديد مقدمًا هو الشيء الوحيد الذي يُبقي تلك النافذة قابلة للنجاة
FPDFDoc_GetAttachmentCount := CheckGetProcAddress('FPDFDoc_GetAttachmentCount');
FPDFDoc_AddAttachment := CheckGetProcAddress('FPDFDoc_AddAttachment');
FPDFAttachment_GetName := CheckGetProcAddress('FPDFAttachment_GetName');
FPDFAttachment_GetStringValue := CheckGetProcAddress('FPDFAttachment_GetStringValue');
// Attachment descriptions were added after the bundled DLL revision.
// Keep them optional so older deployments continue to load.
FPDFAttachment_SetDescription := TryGetProcAddress('FPDFAttachment_SetDescription');
FPDFAttachment_GetDescription := TryGetProcAddress('FPDFAttachment_GetDescription');
FPDFAttachment_SetFile := CheckGetProcAddress('FPDFAttachment_SetFile');
FPDFAttachment_GetFile := CheckGetProcAddress('FPDFAttachment_GetFile');
ماذا يجب أن تفعل بوابة قدرة عند موقع الاستدعاء؟
يجب أن تكون غير متناظرة، وذلك عدم التناظر هو التصميم بأكمله. القراءة التي لا تستطيع العمل لها إجابة فارغة صادقة. الكتابة التي لا تستطيع العمل ليس لها إجابة صادقة إطلاقًا، لذا يجب أن تُثير استثناءً. يُقسّم PDFium Component خاصية وصف المرفق بالضبط على ذلك الخط، والانقسام هو ما يمنع تصديرًا مفقودًا من التحوّل إلى فقدان بيانات صامت. تختبر TPdf.GetAttachmentDescription Assigned(FPDFAttachment_GetDescription) وتخرج بـWString فارغة. هذا ليس كذبًا: على DLL بلا التصدير، لا يستطيع المكوّن حقًا معرفة ما إذا كان المرفق يحمل مدخل /Desc، ووصف فارغ يُقرأ بنفس طريقة مرفق لم يحمل واحدًا قط. بقية واجهة المرفقات البرمجية، المشروحة في مقال العمل مع مرفقات PDF في Delphi، تستمر بالعمل دون مساس
تسلك TPdf.SetAttachmentDescription الطريق المعاكس. تستدعي Check على نفس اختبار Assigned وتُثير EPdfError بالنص "Attachment descriptions are not supported by the loaded PDFium DLL". العودة بهدوء هنا سيكون أسوأ خيار متاح: سيُعيّن المستدعي وصفًا، ولا يحصل على خطأ، ويحفظ الملف، ويشحن ملف PDF حيث الوصف غائب ببساطة. لا أحد يلاحظ حتى يسأل مستهلك لاحق عن مكانه
function TPdf.GetAttachmentDescription(Index: Integer): WString;
begin
CheckActive;
Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
Result := '';
// Read side degrades: an old DLL cannot report /Desc, and '' is
// indistinguishable from an attachment that carries no description.
if not Assigned(FPDFAttachment_GetDescription) then
Exit;
// ... two-pass buffer sizing against FPDFAttachment_GetDescription ...
end;
procedure TPdf.SetAttachmentDescription(Index: Integer; const Value: WString);
begin
CheckActive;
Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
// Write side refuses: silently dropping the value would produce a file
// the caller believes carries a description and does not.
Check(Assigned(FPDFAttachment_SetDescription),
'Attachment descriptions are not supported by the loaded PDFium DLL');
// ... FPDFDoc_GetAttachment, then FPDFAttachment_SetDescription ...
end;
فحص القدرة قبل عرض الميزة
التقاط استثناء طريقة سيئة لاكتشاف ما يستطيعه نشرك، لذا يعرض PDFium Component نفس الفحص كدالة مُسمّاة. تستدعي AttachmentDescriptionFeaturesAvailable LoadLibrary وتُعيد ما إذا كان نصفا الزوج قد حُلّا. تقع بجانب V8FeaturesAvailable وXfaBStrHelpersAvailable وXfaFeaturesAvailable، التي تتبع نفس النمط تمامًا لمجموعاتها الاختيارية الخاصة. تسمية الفحص أهم مما يبدو: قيمة منطقية تُسمّى AttachmentDescriptionFeaturesAvailable تُخبر المُصان التالي أن هذه الميزة مشروطة بالملف الثنائي المنشور، وهو ما لا يفعله أبدًا اختبار Assigned مجرد مدفون في مُعيِّن خاصية. كما يُعطي طبقة الواجهة شيئًا لتربطه، بحيث يُعطَّل مربع تحرير الوصف مقدمًا بدلًا من قبول المدخل ورفضه عند الحفظ
procedure TAttachmentFrame.SyncCapabilities;
begin
// Ask once, at form setup, instead of discovering the limit on save.
DescriptionEdit.Enabled := AttachmentDescriptionFeaturesAvailable;
if not DescriptionEdit.Enabled then
DescriptionEdit.TextHint := 'Requires a newer pdfium.dll';
end;
procedure TAttachmentFrame.SaveDescription(Pdf: TPdf; Index: Integer);
begin
if not AttachmentDescriptionFeaturesAvailable then
Exit;
Pdf.AttachmentDescription[Index] := DescriptionEdit.Text;
end;
لماذا يجب إثبات تغطية الربط بأداة؟
لأن الأرقام تجاوزت النقطة التي يمكن فيها الوثوق بإنسان معها. دقّق PDFium Component 21 ترويسة PDFium عامة مقابل خط أساس من المنبع بتاريخ 2026-07-29 ووجد 470 دالة ABI بلغة C مُصدَّرة. الربط غطى بالفعل 468 منها. لم يحدد أحد تلك الفجوة من اثنتين بقراءة الترويسات؛ فعل ذلك سكريبت، في ثانية، وسيفعله مرة أخرى عند رفعة المنبع القادمة. tools/audit_pdfium_public_api.py صغير عمدًا: يُطابق بالتعبير النمطي FPDF_EXPORT ... FPDF_CALLCONV name( عبر كل ترويسة في الدليل العام، ويُطابق بالتعبير النمطي كل CheckGetProcAddress('Name') وTryGetProcAddress('Name') في PDFium.pas، ويطبع فرقي المجموعتين: missing للتصديرات بلا ربط، وstale للروابط التي لم يعد تصديرها موجودًا في المنبع. يخرج بقيمة غير صفرية عندما تكون أي مجموعة غير فارغة، بحيث ينسدل في خطوة بناء دون مراسم إضافية. النتيجة الحالية 470 من 470 مربوطة، مفقود 0، بائت 0
اتجاه البائت يكسب حصته بقدر اتجاه المفقود. تصدير يُزيله المنبع يترك سطر CheckGetProcAddress خلفه سيفشل بشدة في كل تحميل مستقبلي، وذلك النوع من التعفن غير مرئي حتى يُحدّث أحدهم DLL يومًا. المراجعة اليدوية تجد الدالة التي كنت تفكر فيها؛ لا تجد التي لم تكن تفكر فيها. لاحظ أيضًا أن التدقيق يحتسب متعمدًا كلا المُحمِّلين كتغطية، وهو القرار الصحيح لانحراف الواجهة البرمجية والسبب في أن انقسام المطلوب/الاختياري يجب أن يكون قرارًا موثَّقًا لا نتاجًا ثانويًا لمن أضاف السطر
أين يتوقف الربط الاختياري عن كونه صادقًا
حدّان يستحقان الذكر الصريح، لأن النمط سهل الإفراط في تطبيقه. الأول أن مؤشر دالة nil آمن فقط إذا كان حرفيًا كل مسار يلمسه يختبر Assigned أولًا. في وحدة تُعلن مئات متغيرات دالة cdecl، استدعاء واحد غير محروس انتهاك وصول عند عنوان لا يعني شيئًا في تتبع مكدس. نفس الانضباط الذي يحكم اصطلاحات الاستدعاء والأعمار عبر حدود C ينطبق هنا، وهو موضوع مقال تحصين ربط PDFium ضد أعطال ABI وسلامة الذاكرة
الحد الثاني هو النطاق. الربط الاختياري ليس رخصة عامة لجعل كل شيء متسامحًا. لو كانت FPDF_RenderPageBitmap اختيارية، سيُحمَّل المكوّن بسعادة ثم يفشل في كل صفحة، محوّلًا خطأ بدء تشغيل واضحًا واحدًا إلى بعثرة من أخطاء وقت التشغيل بلا سبب واضح. المطلوب هو الافتراضي الصحيح. الاختياري هو الاستثناء الذي تلجأ إليه عندما تكون ميزة ورقة حقًا، وعندما يكون للغياب سلوك تدهور قابل للدفاع عنه على جانب القراءة، وعندما يستطيع جانب الكتابة الرفض برسالة تُسمّي السبب
تصميم المُحمِّل، وفحوصات القدرة، وأداة التدقيق الموصوفة هنا تُشحن كجزء من PDFium Component لـDelphi وC++Builder؛ صفحة المنتج تُدرِج الملفات الثنائية لـPDFium المُرفَقة وسطح الواجهة البرمجية الكامل الذي تعرضه