Техническа статия

Незадължителни PDFium експорти: портали за възможности в Delphi

Вашият 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, а празно описание се чете по същия начин като прикачен файл, който никога не е имал такова. Остатъкът от 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 е нарочно малък: прави regex съвпадение на FPDF_EXPORT ... FPDF_CALLCONV name( през всеки заглавен файл в публичната директория, regex съвпадение на всеки CheckGetProcAddress('Name') и TryGetProcAddress('Name') в PDFium.pas, и отпечатва двете разлики на множества: missing за експорти без свързване, stale за свързвания, чийто експорт вече не съществува нагоре по веригата. Излиза с ненулев код, когато което и да е от множествата е непразно, така че се вписва в стъпка на билда без допълнителна церемония. Текущият резултат е 470 от 470 свързани, 0 липсващи, 0 остарели

Посоката stale си заслужава мястото толкова, колкото и missing. Експорт, който бива премахнат нагоре по веригата, оставя зад себе си ред CheckGetProcAddress, който ще проваля твърдо всяко бъдещо зареждане, а този вид разпад е невидим до деня, в който някой обнови DLL-а. Ръчен преглед намира функцията, за която сте мислили; не намира тази, за която не сте. Отбележете също, че одитът нарочно брои и двата зареждащи модула като покритие, което е правилното решение за отклонение на API-то и причината разделянето задължително/незадължително да трябва да бъде документирано решение, а не страничен продукт на това кой е добавил реда

Къде незадължителното свързване спира да бъде честно

Две граници заслужават да бъдат казани ясно, защото моделът е лесен за прекомерно прилагане. Първата е, че nil указател на функция е безопасен само ако буквално всеки път, докосващ го, тества Assigned първо. В модул, декларираш стотици променливи на функции cdecl, едно единствено незащитено извикване е нарушение на достъп на адрес, който не значи нищо в trace на стека. Същата дисциплина, управляваща конвенции за извикване и времена на живот през C границата, се прилага тук, и е темата на статията за закаляване на PDFium свързването срещу грешки в ABI и безопасността на паметта

Втората граница е обхватът. Незадължителното свързване не е обща лицензия да направите всичко толерантно. Ако FPDF_RenderPageBitmap беше незадължителна, компонентът би се заредил с готовност и после би се провалял на всяка страница, превръщайки една ясна грешка при стартиране в разпръснатост от грешки по време на изпълнение без очевидна причина. Задължително е правилната стойност по подразбиране. Незадължително е изключението, към което посягате, когато функция наистина е листо, когато отсъствието има защитимо деградирало поведение от страната на четенето, и когато страната на записа може да откаже със съобщение, назоваващо причината

Дизайнът на зареждащия модул, сондите за възможности и инструментът за одит, описани тук, се доставят като част от PDFium Component за Delphi и C++Builder; страницата на продукта изброява включените PDFium бинарни файлове и пълната повърхност на API-то, която излагат