Ваш 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 у вашому інсталяторі рухається дискретними стрибками щоразу, коли хтось перезбирає його. Завжди є вікно, в якому паскалевий бік знає про експорти, яких розгорнутий бінарник не має, і рішення заздалегідь, на який бік лінії обов'язкового/опційного потрапляє кожен новий експорт, — єдине, що робить це вікно пережиттєвим
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, а порожній опис читається так само, як вкладення, що ніколи його не мало. Решта API вкладень, охопленого в статті про роботу з вкладеннями 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, похований у сеттері властивості, ніколи не робить. Це також дає шару UI щось для прив'язки, тож поле редагування опису вимикається заздалегідь, а не приймає введення й відхиляє його при збереженні
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 експортованих функцій C ABI. Прив'язка вже покривала 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. Ручний огляд знаходить функцію, про яку ви думали; він не знаходить ту, про яку ви не думали. Зауважте також, що аудит навмисно рахує обидва завантажувачі як покриття, що правильне рішення для дрейфу API і причина, чому розподіл обов'язкового/опційного має бути задокументованим рішенням, а не побічним продуктом того, хто б не додав рядок
Де опційна прив'язка перестає бути чесною
Дві межі варто прямо сформулювати, бо схему легко надуживати. Перша в тому, що nil-покажчик функції безпечний лише якщо буквально кожен шлях, що торкається його, перевіряє Assigned спершу. У модулі, що декларує сотні змінних функцій cdecl, один неохоронений виклик — порушення доступу за адресою, що нічого не означає у стек-трейсі. Та сама дисципліна, що керує угодами виклику й часом життя через межу C, застосовується тут, і це предмет статті про зміцнення прив'язки PDFium проти помилок ABI та безпеки пам'яті
Друга межа — обсяг. Опційна прив'язка — не загальна ліцензія робити все толерантним. Якби FPDF_RenderPageBitmap був опційним, компонент з радістю завантажився б, а потім зазнавав невдачі на кожній сторінці, перетворюючи одну чітку помилку запуску на розсип помилок під час виконання без очевидної причини. Обов'язковість — правильне значення за замовчуванням. Опційність — виняток, до якого ви сягаєте, коли функція справді листок, коли відсутність має захисну деградовану поведінку на боці читання, і коли бік запису може відмовити з повідомленням, що називає причину
Дизайн завантажувача, зонди можливостей та інструмент аудиту, описані тут, постачаються як частина PDFium Component для Delphi та C++Builder; сторінка продукту перелічує пакетовані бінарники PDFium та повну поверхню API, яку вони надають