Коли pdfium.dll відмовляється завантажуватися на комп'ютері користувача, компонент PDFium для Delphi та Lazarus перетворює незрозумілу системну помилку на конкретну причину. Процедура CheckLoadLibrary інтерпретує помилку ERROR_BAD_EXE_FORMAT (193) як невідповідність розрядності (32/64 біт), а CheckGetProcAddress повідомляє про відсутність експортованої функції, що свідчить про застарілу версію pdfium.dll. Обидва повідомлення вказують на конкретне вирішення замість того, щоб змушувати вас вгадувати причину.
Це важливо, оскільки нативна бібліотека DLL може давати збій трьома різними способами, хоча операційна система часто об'єднує їх під одним текстом помилки. Може бути неправильною розрядність, розгорнута бібліотека може бути застарілою для обгортки на Pascal, або два потоки можуть одночасно намагатися зв'язатися з нею. Кожен випадок потребує свого вирішення, і метою діагностики, доданої під час оптимізації життєвого циклу завантаження у версії v2.11.0, є точне визначення конкретної причини.
Чому завантаження pdfium.dll завершується помилкою EXE format?
Системна помилка Windows 193, ERROR_BAD_EXE_FORMAT, означає, що файл знайдено та відкрито, але тип платформи PE у файлі не відповідає розрядності поточного процесу. 64-бітний виконуваний файл не може завантажити 32-бітну бібліотеку pdfium.dll, а 32-бітний процес не завантажить 64-бітну. Оскільки файл фізично присутній, спроба шукати відсутню DLL у системі буде помилковою.
Компонент PDFium перехоплює цей випадок у процедурі CheckLoadLibrary: якщо функція LoadLibrary повертає нульовий дескриптор, а GetLastError дорівнює 193, вона додає підказку про те, що бібліотека DLL знайдена, але її архітектура не відповідає розрядності процесу, та вказує на відповідну збірку у папках DLLs/Win32 або DLLs/Win64. Ця підпапка вибирається за допомогою методу BuildDllSubDir, який повертає Win64, якщо властивість IsWin64 дорівнює true, і Win32 у протилежному випадку. Розгорніть потрібну бібліотеку в папці, яку шукає обгортка, і помилка зникне.
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;
Про що свідчить відсутність експортованої функції PDFium?
Другий клас помилок пов'язаний із несумісністю версій. PDFium з часом додає нові експортовані функції, а Pascal-обгортка перевіряє кожну необхідну функцію через CheckGetProcAddress під час завантаження бібліотеки. Якщо потрібна функція відсутня, це означає, що обгортка є новішою за бібліотеку pdfium.dll на диску. Правильним діагнозом у цьому випадку є те, що встановлена DLL застаріла, а не пошкоджена. Функція CheckGetProcAddress повідомляє саме про це: вона генерує виняток EPdfError, вказуючи, що розгорнута бібліотека pdfium.dll є старішою за поточну збірку обгортки PDFiumPas, та виводить шлях DLLs/<subdir>, де має знаходитися сумісний файл.
Дві деталі роблять цей механізм надійним. По-перше, CheckGetProcAddress викликає UnloadLibrary перед генерацією винятку, тому часткове підключення ніколи не залишить напіввирішених покажчиків функцій, на які могла б натрапити наступна спроба завантаження. По-друге, назва DLL, про яку повідомляє система, повертається методом BuildDllName: зазвичай це pdfium.dll, але при встановленому глобальному прапорці EnableV8Engine повертається pdfium.v8.dll. Якщо ваша програма використовує двигун V8 JavaScript для форм XFA або скриптів, повідомлення вкаже саме на збірку V8, дозволяючи вам одразу замінити потрібний файл.
Чи безпечно завантажувати PDFium DLL у фоновому потоці?
Тепер завантаження бібліотеки безпечне з будь-якого потоку, але викликати API PDFium з кількох потоків одночасно все ще не можна. Це дві різні гарантії, і їхнє розділення є критично важливим для стабільності роботи програми. Фоновий рендеринг зазвичай виконує обробку сторінки через фоновий процес TPdfFuture<T>, і перше звернення до TPdf викликає відкладене завантаження бібліотеки. Без відповідного захисту два фонових процеси могли б одночасно виявити незавантажену бібліотеку, запустити процес завантаження, перезаписати дескриптор модуля та спричинити витік пам'яті при першому завантаженні.
Оновлення у версії v2.11.0 усуває цю проблему за допомогою PDFiumLoadLock — глобальної для процесу критичної секції TRTLCriticalSection, яка обгортає виклики як LoadLibrary, так і UnloadLibrary. Критична секція робить перевірку та завантаження атомарними, тому два потоки не зможуть одночасно побачити стан Loaded=False та викликати подвійне завантаження, а також жоден потік не зможе вивантажити DLL, поки інший виконує її підключення. Блокування створюється в розділі initialization модуля та звільняється у finalization із захистом прапорцем PDFiumLoadLockReady для безпечного завершення роботи програми. Якщо вам потрібен повний шаблон фонової обробки з можливістю скасування, див. супутню статтю про фоновий рендеринг із ф'ючерсами (futures) та підтримкою скасування.
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;
Що не захищає блокування завантаження
Це обмеження варто зазначити чітко, оскільки легко переоцінити можливості цього механізму. PDFiumLoadLock серіалізує лише глобальний стан підключення: дескриптор модуля та призначення покажчиків функцій FPDF_*. Сам базовий C-API бібліотеки PDFium не є потокобезпечним, про що прямо вказано в його заголовках, тому виклик функцій FPDF_* або спільне використання одного дескриптора FPDF_DOCUMENT між потоками залишається вашим завданням для серіалізації. Безпечний шаблон наведено вище: кожен фоновий процес володіє власним екземпляром TPdf і ніколи не передає відкритий документ іншому потоку. Детальніше про це обмеження та правила ABI див. у статті про захист обгортки PDFium VCL від збоїв ABI та пам'яті.
Чому важливі розділ finalization та функція FPDF_DestroyLibrary
Більш тонка проблема виникала під час завершення роботи програми. Модуль PDFium не мав секції finalization, через що функція FPDF_DestroyLibrary ніколи не викликалася при виході з процесу. Операційна система просто звільняла пам'ять бібліотеки DLL і пропускала власне очищення ресурсів PDFium (очікування завершення фонових потоків, звільнення ізолятів V8 при увімкненому двигуні EnableV8Engine, очищення кешу та шрифтів). При звичайній роботі це призводило до непомітного витоку пам'яті при виході, но при використанні двигуна V8 відбувався значний витік ізолятів JavaScript при кожному запуску, що швидко ставало помітним в автоматизованих системах.
Тепер розділ 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 Component для Delphi та C++Builder.