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

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

Вашият pdfium.dll се зарежда добре, а една процедура все още липсва. PDFium Component се справя с това, разделяйки свързванията си на два класа: задължителни експорти, разрешавани чрез CheckGetProcAddress, които прекратяват зареждането направо, и незадължителни експорти, разрешавани чрез TryGetProcAddress, които оставят зад себе си nil указател и проверка за възможност вместо това

Това не е същият проблем като DLL, който не може да бъде намерен. Ако приложението ви умира с грешка за лош формат на EXE, липсващ файл, или несъответствие в архитектурата, тази история е разказана в придружаващата статия за разгръщане на pdfium.dll и диагностициране на провали при зареждане. Тук зареждащият модул е успял. Манипулаторът на модула е валиден, стотици експорти са разрешени, а изпълнението все пак завършва преди първата ви страница да се рендира, защото една входна точка, пристигнала в по-нов билд на PDFium, не е в бинарния файл на диска

Защо липсващ единствен експорт чупи цялата библиотека?

Защото задължително свързване е твърд договор, и се налага по време на единствена последователност на свързване по принципа всичко-или-нищо. PDFium Component разрешава цялата си таблица с експорти вътре в LoadLibrary, едно извикване на CheckGetProcAddress след друго. Първият резултат nil хвърля EPdfError и извиква UnloadLibrary преди това, което е умишлено: частично свързване иначе би оставило вече разрешени указатели, насочени в модул, който е на път да бъде освободен, тихо побеждавайки всяка защита Assigned надолу по веригата

Последствието е режимът на провал, който води хората тук. Обновявате компонента, доставяте същия pdfium.dll, доставян от две години, а приложението не стартира. Грешката назовава експорт за функция, която никога не сте извиквали. Нищо, което направите на мястото на извикването, не помага, защото мястото на извикването никога не се изпълнява; провалът се е случил по време на свързването, преди какъвто и да е документ да е бил отворен

PDFium Component свързва своята Delphi таблица с експорти в един проход, където CheckGetProcAddress прекъсва зареждането при липсващ задължителен експорт, докато TryGetProcAddress безопасно деградира незадължителен
Задължителни експорти се вързват all-or-nothing и прекъсват зареждането на първия nil, докато незадължителни експорти оставят nil указател зад Assigned проверка за възможност
function CheckGetProcAddress(const Name: string): Pointer;
begin
  Result := GetProcAddress(PDFiumLibrary, PChar(Name));
  if Result = nil then
  begin
    // Липсващ задължителен експорт означава, че внеденият pdfium.dll е по-стар
    // от тази компилация на свързването. Изхвърлете всеки вече развързан указател,
    // за да не може никакъв извикващ да достигне модула, който ще освободим.
    UnloadLibrary;
    raise EPdfError.Create('Required PDFium export not found: ' + Name);
  end;
end;

function TryGetProcAddress(const Name: string): Pointer;
begin
  // Незадължителен експорт. nil е законен отговор тук; всеки извикващ е
  // длъжен да тества Assigned(), преди да дереферира променливата.
  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');
// Описанията на прикачени файлове бяха добавени след версията на пакетирания DLL.
// Запазете ги незадължителни, за да могат по-старите разгръщания да продължат да се зареждат.
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, в който описанието просто отсъства. Никой не забелязва, докато консуматор надолу по веригата не попита къде е отишло

Липсващ PDFium експорт за описание на прикачен файл в Delphi връща празно четене през TPdf.GetAttachmentDescription и вдига изключение при запис, ограничено от AttachmentDescriptionFeaturesAvailable
Четната страна деградира до празен отговор, пишещата страна вдига с назована причина, а назована проба позволява на UI да изключи функцията предварително
function TPdf.GetAttachmentDescription(Index: Integer): WString;
begin
  CheckActive;
  Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
  Result := '';

  // Страната за четене деградира: стар DLL не може да отчете /Desc, а '' е
  // неразличимо от прикачен файл без описание.
  if not Assigned(FPDFAttachment_GetDescription) then
    Exit;
  // ... двупроходно оразмеряване на буфера срещу FPDFAttachment_GetDescription ...
end;

procedure TPdf.SetAttachmentDescription(Index: Integer; const Value: WString);
begin
  CheckActive;
  Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
  // Страната за запис отказва: тихото изпускане на стойността би създало файл,
  // за който извикващият смята, че носи описание, а той не носи.
  Check(Assigned(FPDFAttachment_SetDescription),
    'Attachment descriptions are not supported by the loaded PDFium DLL');
  // ... FPDFDoc_GetAttachment, а после FPDFAttachment_SetDescription ...
end;

Сондиране на възможността, преди да предложите функцията

Улавянето на изключение е слаб начин да откриете какво може разгръщането ви, така че PDFium Component излага същия тест като именувана функция. AttachmentDescriptionFeaturesAvailable извиква LoadLibrary и връща дали и двете половини на двойката са разрешени. Тя седи до V8FeaturesAvailable, XfaBStrHelpersAvailable и XfaFeaturesAvailable, които следват идентичния модел за собствените си незадължителни групи. Именуването на сондата има значение повече, отколкото изглежда: булев тип, наречен AttachmentDescriptionFeaturesAvailable, казва на следващия поддържащ, че тази функция е условна спрямо разгърнатия бинарен файл, което гол тест Assigned, заровен в сетер на свойство, никога не прави. Дава и на UI слоя нещо, към което да се обвърже, така че полето за редактиране на описание е деактивирано предварително, вместо да приема вход и да го отхвърля при запазване

procedure TAttachmentFrame.SyncCapabilities;
begin
  // Попитайте веднъж, при настройката на формата, вместо да откривате ограничението при запис.
  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-то, която излагат