مقاله فنی

استقرار فایل DLL پی‌دی‌اف‌یوم (PDFium DLL) در دلفی: رفع مشکلات بارگذاری

هنگامی که فایل pdfium.dll روی سیستم مشتری بارگذاری نمی‌شود، کامپوننت PDFium برای دلفی و لازاروس خطای بومی نامفهوم سیستم‌عامل را به یک علت مشخص تبدیل می‌کند. متد CheckLoadLibrary خطای ERROR_BAD_EXE_FORMAT (193) را به عنوان عدم تطابق معماری ۳۲/۶۴ بیتی تشخیص می‌دهد، و متد CheckGetProcAddress فقدان یک تابع صادرشده (export) را به عنوان نشانه‌ای از قدیمی‌تر بودن فایل pdfium.dll نسبت به اتصال (binding) فعلی گزارش می‌کند. هر دو پیغام به جای اینکه شما را به حدس زدن واگذار کنند، راه‌حل رفع مشکل را نام می‌برند

این موضوع از آنجا اهمیت دارد که یک DLL بومی به سه روش مجزا با شکست مواجه می‌شود، و متن خام سیستم‌عامل همه آنها را با هم ترکیب می‌کند. معماری ممکن است اشتباه باشد، باینری مستقرشده ممکن است برای اتصال پاسکال که آن را فراخوانی می‌کند بسیار قدیمی باشد، یا دو ریسمان (thread) ممکن است در یک لحظه برای اتصال به آن رقابت کنند (race condition). هر کدام راه‌حل متفاوتی دارند، و هدف کل سیستم عیب‌یابی که در کارهای چرخه حیات بارگذاری نسخه v2.11.0 اضافه شده است، این است که به شما بگوید در حال حاضر با کدام‌یک از این مشکلات مواجه هستید

چرا بارگذاری pdfium.dll با خطای فرمت EXE نامعتبر (bad EXE format) با شکست مواجه می‌شود؟

خطای ۱۹۳ ویندوز، یعنی ERROR_BAD_EXE_FORMAT، به این معنی است که فایل پیدا و باز شده است اما نوع ماشین PE آن با پروسه میزبان مطابقت ندارد. یک فایل اجرایی ۶۴ بیتی نمی‌تواند یک فایل pdfium.dll ۳۲ بیتی را بارگذاری کند، و یک فایل اجرایی ۳۲ بیتی نیز نمی‌تواند نسخه ۶۴ بیتی آن را بارگذاری نماید. از آنجا که فایل وجود دارد، تلاش برای جستجوی یک DLL مفقودشده، شما را دقیقاً در مسیر اشتباهی قرار می‌دهد

کامپوننت PDFium این حالت را در متد CheckLoadLibrary شناسایی می‌کند: هنگامی که LoadLibrary یک هندل null برمی‌گرداند و مقدار GetLastError برابر با ۱۹۳ است، پیامی اضافه می‌کند مبنی بر اینکه DLL پیدا شد اما معماری آن با فرآیند میزبان مطابقت ندارد، و به نسخه منطبق در مسیر DLLs/Win32 یا DLLs/Win64 اشاره می‌نماید. این پوشه فرعی توسط BuildDllSubDir انتخاب می‌شود، که وقتی IsWin64 مقدار true داشته باشد Win64 را برمی‌گرداند و در غیر این صورت Win32. فایل DLL را در درختی مستقر کنید که اتصال واقعاً آن را جستجو می‌کند تا عدم تطابق از بین برود

uses
  SysUtils, PDFium;

procedure OpenDocument(const AFileName: string);
var
  Pdf: TPdf;
begin
  // Deploy pdfium.dll next to the executable, matched to the build target:
  //   <AppDir>\DLLs\Win32\pdfium.dll   for a 32-bit host process
  //   <AppDir>\DLLs\Win64\pdfium.dll   for a 64-bit host process
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := AFileName;
    try
      Pdf.Active := True;   // first use lazily binds pdfium.dll
    except
      on E: EPdfError do
        // The message already names the real cause: a 32/64-bit mismatch
        // (ERROR_BAD_EXE_FORMAT) or a pdfium.dll older than this binding
        raise Exception.CreateFmt('PDF engine did not start: %s', [E.Message]);
    end;
    if Pdf.Active then
      RenderFirstPage(Pdf);
  finally
    Pdf.Free;
  end;
end;

نبود تابع صادرشده (export) در PDFium نشان‌دهنده چیست؟

کلاس دوم شکست، ناهمخوانی نسخه است. پی‌دی‌اف‌یوم در طول زمان توابع صادرشده (exports) جدیدی را ارائه می‌دهد، و اتصال پاسکال هر تابعی را که نیاز دارد از طریق متد CheckGetProcAddress در حین اجرای LoadLibrary حل می‌کند. اگر یک تابع صادرشده مورد نیاز وجود نداشته باشد، به این معنی است که اتصال از فایل pdfium.dll روی دیسک جدیدتر است، و تشخیص دقیق این است که DLL مستقرشده قدیمی است تا اینکه خراب باشد. متد CheckGetProcAddress دقیقاً همین را بیان می‌کند: یک خطای EPdfError ایجاد می‌کند و گزارش می‌دهد که فایل pdfium.dll مستقرشده قدیمی‌تر از این نسخه از اتصال PDFiumPas است، و مسیر DLLs/<subdir> را که باینری منطبق در آن قرار دارد نام می‌برد

دو ویژگی این مسیر را پایدار می‌کنند. اول اینکه، متد CheckGetProcAddress قبل از ایجاد خطا، UnloadLibrary را فراخوانی می‌کند، بنابراین یک اتصال ناقص هرگز اشاره‌گرهای تابع نیمه‌حل‌شده را برای تلاش بعدی باقی نمی‌گذارد تا با خطا مواجه شوند. دوم اینکه، نام DLL که گزارش می‌دهد از BuildDllName می‌آید، که در حالت عادی pdfium.dll و در زمان فعال بودن فلگ سراسری EnableV8Engine نام pdfium.v8.dll را برمی‌گرداند. اگر برنامه شما موتور جاوااسکریپت V8 را برای فرم‌های XFA یا اسکریپت‌شده فعال کرده باشد، متن خطا به نسخه V8 اشاره می‌کند، نه نسخه معمولی، بنابراین شما در همان تلاش اول فایل صحیح را جایگزین می‌کنید

آیا بارگذاری DLL پی‌دی‌اف‌یوم از یک ریسمان پس‌زمینه (background thread) ایمن است؟

بارگذاری کتابخانه اکنون از هر ریسمانی ایمن است؛ اما فراخوانی API پی‌دی‌اف‌یوم از چندین ریسمان همچنان ایمن نیست. این‌ها دو تضمین مجزا هستند، و مجزا نگه داشتن آنها تفاوت بین یک گروه کاری پایدار و یک کرش تصادفی است. رندر پس‌زمینه معمولاً هر رندر صفحه را از طریق یک کلاس کاری TPdfFuture<T> اجرا می‌کند، و اولین پردازشی که با TPdf ارتباط برقرار کند، اتصال تنبل (lazy bind) را فعال می‌نماید. بدون محافظت، دو پروسه کاری می‌توانند یک کتابخانه بارگذاری‌نشده را مشاهده کنند، هر دو فرآیند اتصال را اجرا نمایند، هندل ماژول را بازنویسی کنند و بارگذاری اول را لو دهند

تغییرات نسخه v2.11.0 این مشکل را با PDFiumLoadLock برطرف می‌کند، یک TRTLCriticalSection سراسری در پروسه که ورودی‌های هر دو متد LoadLibrary و UnloadLibrary را در بر می‌گیرد. این بخش بحرانی (critical section) فرآیند بررسی-سپس-بارگذاری را به صورت اتمیک (atomic) در می‌آورد، بنابراین هیچ دو ریسمانی نمی‌توانند به طور همزمان وضعیت Loaded=False را مشاهده کرده و دوباره متصل شوند، و هیچ ریسمانی نمی‌تواند DLL را در حالی که ریسمان دیگری در میانه اتصال است، آزاد کند. قفل در بخش initialization یونیت ایجاد و در بخش finalization از بین می‌رود، و توسط فلگ PDFiumLoadLockReady محافظت می‌شود تا این جفت‌سازی حتی در زمان خاموش شدن برنامه نیز ایمن باشد. اگر به الگوی کامل کاری-و-پاسخ به همراه قابلیت لغو نیاز دارید، مقاله همراه با موضوع رندر پس‌زمینه با قابلیت لغو کارها آن را به طور کامل توضیح می‌دهد

uses
  PDFium, FPdfAsync;

// Worker method runs on a background thread. The first future to bind
// pdfium.dll is serialised by PDFiumLoadLock, so a second concurrent
// worker cannot double-bind or leak the module handle.
function TReportForm.RenderThumbnail(
  const AToken: IPdfCancellationToken): TBitmap;
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Active := True;         // safe concurrent bind
    AToken.ThrowIfCancelled;
    Result := Pdf.Thumbnail;    // this TPdf is 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;

آنچه قفل بارگذاری (load lock) از آن محافظت نمی‌کند

مرز در اینجا ارزش بیان صریح دارد، زیرا به راحتی می‌توان درباره این راه حل زیاده‌روی کرد. PDFiumLoadLock تنها وضعیت اتصال سراسری را سریال‌سازی (serialises) می‌کند: یعنی هندل ماژول و انتساب‌های اشاره‌گر تابع FPDF_*. ساختار C API پی‌دی‌اف‌یوم پایه‌ای همچنان thread-safe نیست، دقیقاً همان‌گونه که هدرهای اصلی اعلام می‌کنند، بنابراین فراخوانی توابع FPDF_* یا اشتراک‌گذاری یک FPDF_DOCUMENT در میان ریسمان‌ها همچنان بر عهده شماست تا آنها را سریال‌سازی کنید. روش ایمن همان روشی است که در بالا ذکر شد: هر بخش کاری مالک TPdf خود است و هرگز یک سند زنده را به ریسمان دیگری تحویل نمی‌دهد. برای بررسی دقیق‌تر این مرز و قوانین ABI پیرامون آن، بخش مقاوم‌سازی اتصال VCL پی‌دی‌اف‌یوم در برابر خطاهای ABI و حافظه را ببینید

چرا بخش finalization و متد FPDF_DestroyLibrary اهمیت دارند

یک شکاف ظریف‌تر در مسیر خاموش شدن برنامه وجود داشت. یونیت PDFium فاقد بخش finalization بود، به این معنی که متد FPDF_DestroyLibrary هرگز در زمان خروج از پروسه اجرا نمی‌شد؛ سیستم‌عامل حافظه DLL را پس می‌گرفت و از پاک‌سازی خود پی‌دی‌اف‌یوم چشم‌پوشی می‌کرد، یعنی مواردی مانند الحاق ریسمان کاری (worker-thread join)، حذف ایزوله V8 در زمان فعال بودن EnableV8Engine، و از بین بردن فونت و کش. در یک بار کاری رندرینگ معمولی، این موضوع یک نشتی بی‌صدا در زمان بستن برنامه است، اما با فعال بودن موتور V8، در هر اجرا یک ایزوله جاوااسکریپت نشت می‌کند، که در اتوماسیون‌های تکراری به سرعت خود را نشان می‌دهد

بخش finalization اکنون متد UnloadLibrary را فراخوانی می‌کند، که متد FPDF_DestroyLibrary را اجرا کرده و ماژول را آزاد می‌نماید. این کار به دلیلی در دامنه یونیت قرار دارد: متد TPdf.Destroy تنها وضعیت Active := False را برای بستن سند فعلی تنظیم می‌کند، و عمداً کتابخانه سراسری را بارگذاری‌زدایی نمی‌کند، بنابراین برنامه‌ای با چند نمونه فعال، یک اتصال مشترک پی‌دی‌اف‌یوم را در کل طول عمر پروسه زنده نگه می‌دارد. قرار دادن پاک‌سازی در بخش finalization به این معنی است که کتابخانه مشترک دقیقاً یک بار و در زمان بارگذاری‌زدایی یونیت آزاد می‌شود، و متد UnloadLibrary ابتدا فلگ Loaded خود را بررسی می‌کند تا در صورتی که پی‌دی‌اف‌یوم هرگز استفاده نشده باشد، این فراخوانی بدون هیچ عملیاتی (no-op) و بی‌ضرر باشد

یک چک‌لیست کوتاه برای استقرار

سه عادت، تقریباً از تمام شکست‌های بارگذاری در محیط واقعی جلوگیری می‌کنند. معماری DLL را با فرآیند میزبان مطابقت دهید و آن را در پوشه DLLs/Win32 یا DLLs/Win64 قرار دهید؛ فایل pdfium.dll را با نسخه اتصال هماهنگ نگه دارید تا هیچ تابع صادرشده مورد نیازی مفقود نشود؛ و هرگز یک TPdf یا هندل سند را در میان ریسمان‌ها به اشتراک نگذارید، هرچند که خود فرآیند اتصال اکنون اتمیک (atomic) است. اگر همان کد را برای هر دو ابزار دلفی و لازاروس می‌سازید، تفاوت‌های بسته‌بندی که استقرار چندهدفه را با مشکل مواجه می‌کنند در یادداشت‌های مربوط به مشکلات کامپایلر چندگانه دلفی و FPC پوشش داده شده‌اند. سیستم عیب‌یابی، قفل بارگذاری، و پاک‌سازی نهایی توصیف‌شده در اینجا همگی همراه با PDFium Component برای دلفی و سی‌پلاس‌پلاس‌بیلدر ارائه می‌شوند