مقاله فنی

exportهای اختیاری PDFium: دروازه‌های قابلیت در Delphi

pdfium.dll شما درست بارگذاری می‌شود و همچنان یک رویه گم است. PDFium Component این را با تقسیم binding های خود به دو دسته مدیریت می‌کند: exportهای اجباری که از طریق CheckGetProcAddress حل می‌شوند، که بارگذاری را کاملاً لغو می‌کنند، و exportهای اختیاری که از طریق TryGetProcAddress حل می‌شوند، که به‌جای آن یک pointer nil و یک بررسی قابلیت برجای می‌گذارند

این همان مشکل یک DLL که پیدا نمی‌شود نیست. اگر برنامه شما با یک خطای فرمت EXE بد، یک فایل گمشده، یا یک عدم‌تطابق معماری می‌میرد، آن داستان در مقاله همراه درباره استقرار pdfium.dll و تشخیص شکست‌های بارگذاری گفته شده. اینجا loader موفق شده. handle ماژول معتبر است، صدها export حل شده‌اند، و اجرا همچنان پیش از رندر شدن اولین صفحه شما پایان می‌یابد چون یک نقطه ورود که در یک build جدیدتر PDFium رسیده در باینری روی دیسک نیست

چرا یک export گمشده کل کتابخانه را می‌شکند؟

چون یک binding اجباری یک قرارداد سخت است، و در طول یک دنباله bind همه‌یا-هیچ واحد اعمال می‌شود. PDFium Component کل جدول export خودش را داخل LoadLibrary، یک فراخوانی CheckGetProcAddress پس از دیگری، حل می‌کند. اولین نتیجه nil یک EPdfError بلند می‌کند و پیش از آن UnloadLibrary را فراخوانی می‌کند، که عمدی است: در غیر این صورت یک bind جزئی pointerهای از قبل حل‌شده‌ای را باقی می‌گذاشت که به یک ماژول در آستانه آزاد شدن اشاره می‌کردند، و هر نگهبان Assigned پایین‌دستی را خاموش شکست می‌داد

پیامد آن حالت شکستی است که مردم را اینجا می‌آورد. کامپوننت را ارتقا می‌دهید، همان pdfium.dllای را که دو سال است ارسال کرده‌اید ارسال می‌کنید، و برنامه شروع نمی‌شود. خطا یک export را برای ویژگی‌ای نام می‌برد که هرگز فراخوانی نکرده‌اید. هیچ کاری در محل فراخوانی کمک نمی‌کند، چون محل فراخوانی هرگز اجرا نمی‌شود؛ شکست در طول binding رخ داد، پیش از باز شدن هر سندی

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 اعمال می‌کند صریح است. یک export وقتی اجباری است که غیبتش کامپوننت را از انجام کاری که برای آن وجود دارد ناتوان می‌کند، و اختیاری است وقتی غیبتش فقط یک ویژگی برگ را حذف می‌کند. FPDF_InitLibrary، FPDF_LoadDocument، FPDF_RenderPageBitmap، FPDF_ClosePage اجباری‌اند، و شکست بلند روی آن‌ها درست است: یک viewer که نمی‌تواند رندر کند یک viewer تنزل‌یافته نیست، یک viewer خراب است

هرچیزی که امروز از طریق loader تسامح‌آمیز رسیده یک برگ است. FPDFBookmark_GetColor پس از M109 رسید و فقط آرایه رنگ /C اختیاری یک entry outline را تأمین می‌کند، پس یک DLL که از آن قدیمی‌تر است صرفاً هیچ رنگ bookmark گزارش نمی‌کند. کمک‌کننده‌های V8 FPDF_GetRecommendedV8Flags و FPDF_GetArrayBufferAllocatorSharedInstance، و کمک‌کننده‌های رشته XFA FPDF_BStr_Init، FPDF_BStr_Set و FPDF_BStr_Clear، طبق ساخت از هر build غیر-V8 غایب‌اند، پس رفتار با آن‌ها به‌عنوان اجباری pdfium.dll ساده را بارگذاری‌ناپذیر می‌کرد. و جفتی که این مقاله را برانگیخت: FPDFAttachment_SetDescription و FPDFAttachment_GetDescription، اضافه‌شده بالادستی در تاریخ 2026-07-13، دیرتر از تاریخ build هر چهار باینری PDFium‌ای که پروژه زیر DLLs/Win32 و DLLs/Win64 ارسال می‌کند. آن مورد آخر شکل عمومی مسئله است، نه یک مورد یک‌باره: یک لایه binding هدرهای بالادستی را ردیابی می‌کند، که مداوماً حرکت می‌کنند، درحالی‌که DLL در installer شما هر وقت کسی آن را دوباره می‌سازد در پرش‌های گسسته حرکت می‌کند. همیشه یک پنجره وجود دارد که در آن سمت Pascal درباره exportهایی می‌داند که باینری مستقر ندارد، و تصمیم‌گیری از پیش درباره اینکه کدام سمت خط اجباری/اختیاری هر export جدید می‌افتد تنها چیزی است که آن پنجره را قابل‌بقا نگه می‌دارد

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 ویژگی توصیف پیوست را دقیقاً روی آن خط تقسیم می‌کند، و آن تقسیم چیزی است که یک export گمشده را از تبدیل شدن به از‌دست‌رفتن خاموش داده متوقف می‌کند. TPdf.GetAttachmentDescription Assigned(FPDFAttachment_GetDescription) را آزمایش می‌کند و با یک WString خالی خارج می‌شود. آن یک دروغ نیست: روی یک DLL بدون export، کامپوننت واقعاً نمی‌تواند بگوید آیا پیوست یک entry /Desc حمل می‌کند یا نه، و یک توصیف خالی همان‌طور خوانده می‌شود که یک پیوست که هرگز یکی نداشته. باقی API پیوست، پوشش‌داده‌شده در مقاله کار با پیوست‌های PDF در Delphi، بدون‌لمس کار کردن را ادامه می‌دهد

TPdf.SetAttachmentDescription مسیر مخالف را می‌گیرد. روی همان آزمون Assigned Check را فراخوانی می‌کند و EPdfError را با متن "Attachment descriptions are not supported by the loaded PDFium DLL" بلند می‌کند. آرام برگشتن اینجا بدترین گزینه در دسترس می‌بود: فراخواننده یک توصیف تنظیم می‌کرد، هیچ خطایی نمی‌گرفت، فایل را save می‌کرد، و یک 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;

کاوش قابلیت پیش از ارائه ویژگی

گرفتن یک exception راه ضعیفی برای کشف کاری است که استقرار شما می‌تواند انجام دهد، پس PDFium Component همان آزمون را به‌عنوان یک تابع نام‌گذاری‌شده ارائه می‌دهد. AttachmentDescriptionFeaturesAvailable LoadLibrary را فراخوانی می‌کند و برمی‌گرداند آیا هر دو نیمه جفت حل شدند. کنار V8FeaturesAvailable، XfaBStrHelpersAvailable و XfaFeaturesAvailable می‌نشیند، که همان الگو را برای گروه‌های اختیاری خودشان دنبال می‌کنند. نام‌گذاری کاوش بیشتر از آنچه به‌نظر می‌رسد اهمیت دارد: یک boolean به‌نام AttachmentDescriptionFeaturesAvailable به نگهدارنده بعدی می‌گوید این ویژگی روی باینری مستقر مشروط است، که یک آزمون Assigned ساده مدفون در یک setter ویژگی هرگز نمی‌گوید. همچنین به لایه UI چیزی برای اتصال می‌دهد، پس جعبه ویرایش توصیف از ابتدا غیرفعال است به‌جای پذیرفتن ورودی و رد کردن آن هنگام save

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;

چرا پوشش binding باید با یک ابزار اثبات شود؟

چون اعداد از نقطه‌ای گذشته‌اند که یک انسان می‌تواند به آن‌ها اعتماد شود. PDFium Component 21 هدر عمومی PDFium را در برابر یک baseline بالادستی 2026-07-29 ممیزی کرد و 470 تابع C ABI export‌شده پیدا کرد. binding از قبل 468 تای آن‌ها را پوشش می‌داد. هیچ‌کس آن شکاف دو‌تایی را با خواندن هدرها پیدا نکرد؛ یک اسکریپت در یک ثانیه پیدا کرد، و دوباره در پرش بالادستی بعدی این کار را خواهد کرد. tools/audit_pdfium_public_api.py عمداً کوچک است: FPDF_EXPORT ... FPDF_CALLCONV name( را در سراسر هر هدر در دایرکتوری عمومی regex-match می‌کند، هر CheckGetProcAddress('Name') و TryGetProcAddress('Name') در PDFium.pas را regex-match می‌کند، و دو تفاوت مجموعه را چاپ می‌کند: missing برای exportهایی بدون binding، stale برای bindingهایی که export آن‌ها دیگر بالادستی وجود ندارد. وقتی هرکدام از مجموعه‌ها غیرخالی باشند با کد غیرصفر خارج می‌شود، پس بدون تشریفات بیشتر داخل یک گام build می‌افتد. نتیجه فعلی 470 از 470 bound، 0 missing، 0 stale است

جهت stale به همان اندازه missing ارزش خودش را ثابت می‌کند. یک export که بالادستی حذف می‌کند یک خط CheckGetProcAddress برجای می‌گذارد که هر بارگذاری آینده را به‌سختی شکست می‌دهد، و آن نوع پوسیدگی تا روزی که کسی DLL را به‌روز کند نامرئی است. بررسی دستی تابعی را که به آن فکر می‌کردید پیدا می‌کند؛ آنی را که فکر نمی‌کردید پیدا نمی‌کند. توجه کنید ممیزی عمداً هر دو loader را به‌عنوان پوشش می‌شمارد، که تصمیم درستی برای drift API است و دلیلی است که تقسیم اجباری/اختیاری باید یک تصمیم مستند باشد نه یک محصول جانبی از هرکسی که آن خط را اضافه کرده

جایی که binding اختیاری صادق بودن را متوقف می‌کند

دو مرز ارزش گفتن صریح دارند، چون الگو به‌راحتی بیش‌ازحد اعمال می‌شود. اول این است که یک pointer تابع nil فقط زمانی ایمن است که واقعاً هر مسیری که آن را لمس می‌کند ابتدا Assigned را آزمایش کند. در یک واحد که صدها متغیر تابع cdecl اعلام می‌کند، یک فراخوانی محافظت‌نشده واحد یک access violation در آدرسی است که در یک stack trace هیچ معنایی ندارد. همان انضباطی که calling conventionها و طول عمرها را در سراسر مرز C اداره می‌کند اینجا اعمال می‌شود، و موضوع مقاله سخت‌کردن binding PDFium در برابر خطاهای ABI و ایمنی حافظه است

مرز دوم دامنه است. binding اختیاری یک مجوز عمومی برای تسامح‌آمیز کردن همه‌چیز نیست. اگر FPDF_RenderPageBitmap اختیاری بود، کامپوننت با خوشحالی بارگذاری می‌شد و سپس روی هر صفحه شکست می‌خورد، و یک خطای شروع واضح را به یک پراکندگی از خطاهای زمان‌اجرا بدون علت واضح تبدیل می‌کرد. اجباری پیش‌فرض درست است. اختیاری استثنایی است که به آن دست می‌زنید وقتی یک ویژگی واقعاً یک برگ است، وقتی غیبت رفتار تنزل‌یافته قابل‌دفاعی در سمت خواندن دارد، و وقتی سمت نوشتن می‌تواند با پیامی که دلیل را نام می‌برد امتناع کند

طراحی loader، کاوش‌های قابلیت و ابزار ممیزی که اینجا شرح داده شد به‌عنوان بخشی از PDFium Component برای Delphi و C++Builder عرضه می‌شوند؛ صفحه محصول باینری‌های PDFium همراه‌شده و سطح کامل API را که ارائه می‌دهند فهرست می‌کند