Twoje pdfium.dll wczytuje się bez problemu, a jedna procedura wciąż jest nieobecna. PDFium Component obsługuje to, dzieląc swoje wiązania na dwie klasy: eksporty wymagane rozwiązywane przez CheckGetProcAddress, które całkowicie przerywają wczytanie, oraz eksporty opcjonalne rozwiązywane przez TryGetProcAddress, które zamiast tego zostawiają wskaźnik nil i sprawdzenie możliwości
To nie jest ten sam problem co DLL, którego nie da się znaleźć. Jeśli Twoja aplikacja umiera z błędem złego formatu EXE, brakującym plikiem albo niezgodnością architektury, ta historia jest opisana w towarzyszącym artykule o wdrażaniu pdfium.dll i diagnozowaniu awarii wczytywania. Tutaj loader się powiódł. Uchwyt modułu jest ważny, setki eksportów zostały rozwiązane, a uruchomienie i tak kończy się, zanim wyrenderuje się Twoja pierwsza strona, ponieważ jeden punkt wejścia, który przybył w nowszym buildzie PDFium, nie znajduje się w binarce na dysku
Dlaczego jeden brakujący eksport psuje całą bibliotekę?
Ponieważ wiązanie wymagane to twardy kontrakt, egzekwowany podczas jednej sekwencji wiązania na zasadzie wszystko-albo-nic. PDFium Component rozwiązuje całą swoją tabelę eksportów wewnątrz LoadLibrary, jedno wywołanie CheckGetProcAddress po drugim. Pierwszy wynik nil zgłasza EPdfError i wcześniej wywołuje UnloadLibrary, co jest celowe: częściowe wiązanie inaczej zostawiłoby już rozwiązane wskaźniki wycelowane w moduł, który zaraz zostanie zwolniony, po cichu pokonując każdą strażnicę Assigned dalej w dole
Konsekwencją jest tryb awarii, który przywodzi ludzi tutaj. Aktualizujesz komponent, wysyłasz to samo pdfium.dll, które wysyłałeś od dwóch lat, a aplikacja się nie uruchamia. Błąd nazywa eksport dla funkcji, której nigdy nie wywołałeś. Nic, co zrobisz w miejscu wywołania, nie pomoże, ponieważ miejsce wywołania nigdy nie działa; awaria zdarzyła się podczas wiązania, zanim otwarto jakikolwiek dokument
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;
Wymagane czy opcjonalne: gdzie faktycznie leży granica
Reguła stosowana przez PDFium Component jest surowa. Eksport jest wymagany, gdy jego brak czyni komponent niezdolnym do wykonania zadania, dla którego istnieje, i opcjonalny, gdy jego brak usuwa tylko jedną funkcję liściową. FPDF_InitLibrary, FPDF_LoadDocument, FPDF_RenderPageBitmap, FPDF_ClosePage są wymagane, i głośna porażka na nich jest właściwa: przeglądarka, która nie potrafi renderować, nie jest zdegradowaną przeglądarką, jest zepsuta
Wszystko osiągane dziś przez tolerancyjny loader jest liściem. FPDFBookmark_GetColor przybyło po M109 i dostarcza wyłącznie opcjonalną tablicę koloru /C wpisu konspektu, więc DLL sprzed tego po prostu zgłasza brak koloru zakładki. Pomocnicy V8 FPDF_GetRecommendedV8Flags i FPDF_GetArrayBufferAllocatorSharedInstance, oraz pomocnicy ciągów XFA FPDF_BStr_Init, FPDF_BStr_Set i FPDF_BStr_Clear, są nieobecne w każdym buildzie nie-V8 z konstrukcji, więc traktowanie ich jako wymaganych uczyniłoby zwykłe pdfium.dll niewczytywalnym. I para, która zmotywowała ten artykuł: FPDFAttachment_SetDescription i FPDFAttachment_GetDescription, dodane w upstreamie 2026-07-13, później niż data buildu wszystkich czterech binariów PDFium, jakie projekt wysyła pod DLLs/Win32 i DLLs/Win64. Ten ostatni przypadek to ogólny kształt problemu, nie jednorazowy: warstwa wiązań śledzi nagłówki upstreamowe, które poruszają się ciągle, podczas gdy DLL w Twoim instalatorze porusza się skokowo, ilekroć ktoś go przebudowuje. Zawsze istnieje okno, w którym strona Pascal wie o eksportach, których wdrożona binarka nie ma, a decydowanie z góry, po której stronie granicy wymagane/opcjonalne wypada każdy nowy eksport, jest jedyną rzeczą, która czyni to okno przeżywalnym
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');
Co powinna zrobić bramka możliwości w miejscu wywołania?
Powinna być asymetryczna, i ta asymetria to cały projekt. Odczyt, który nie może się wykonać, ma uczciwą pustą odpowiedź. Zapis, który nie może się wykonać, nie ma żadnej uczciwej odpowiedzi, więc musi zgłosić wyjątek. PDFium Component dzieli właściwość opisu załącznika dokładnie wzdłuż tej linii, a ten podział jest tym, co powstrzymuje brakujący eksport przed zamianą w cichą utratę danych. TPdf.GetAttachmentDescription testuje Assigned(FPDFAttachment_GetDescription) i wychodzi z pustym WString. To nie jest kłamstwo: na DLL bez tego eksportu komponent naprawdę nie może stwierdzić, czy załącznik niesie wpis /Desc, a pusty opis czyta się tak samo jak załącznik, który nigdy go nie miał. Reszta API załączników, omówiona w artykule o pracy z załącznikami PDF w Delphi, działa dalej nietknięta
TPdf.SetAttachmentDescription obiera przeciwną drogę. Wywołuje Check na tym samym teście Assigned i zgłasza EPdfError z tekstem „Attachment descriptions are not supported by the loaded PDFium DLL”. Ciche zwrócenie wyniku tutaj byłoby najgorszą dostępną opcją: wywołujący ustawiłby opis, nie dostałby błędu, zapisał plik i wysłał PDF, w którym opis jest po prostu nieobecny. Nikt tego nie zauważy, dopóki jakiś odbiorca w dole łańcucha nie zapyta, gdzie się podział
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;
Sondowanie możliwości, zanim zaoferujesz funkcję
Przechwytywanie wyjątku to słaby sposób na odkrycie, co potrafi Twoje wdrożenie, więc PDFium Component udostępnia ten sam test jako nazwaną funkcję. AttachmentDescriptionFeaturesAvailable wywołuje LoadLibrary i zwraca, czy obie połówki pary zostały rozwiązane. Siedzi obok V8FeaturesAvailable, XfaBStrHelpersAvailable i XfaFeaturesAvailable, które podążają za identycznym wzorcem dla własnych grup opcjonalnych. Nazwanie sondy ma większe znaczenie, niż się wydaje: boolean nazwany AttachmentDescriptionFeaturesAvailable mówi kolejnemu utrzymującemu, że ta funkcja jest warunkowa wobec wdrożonej binarki, czego goły test Assigned zagrzebany w setterze właściwości nigdy nie robi. Daje też warstwie UI coś, do czego można się dowiązać, więc pole edycji opisu jest wyłączane z góry zamiast przyjmować dane wejściowe i odrzucać je przy zapisie
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;
Dlaczego pokrycie wiązań musi być udowodnione narzędziem?
Ponieważ liczby są poza punktem, w którym można zaufać im od człowieka. PDFium Component przeaudytował 21 publicznych nagłówków PDFium wobec bazowej linii upstream z 2026-07-29 i znalazł 470 eksportowanych funkcji C ABI. Wiązanie już pokrywało 468 z nich. Nikt nie zlokalizował tej luki dwóch przez czytanie nagłówków; zrobił to skrypt, w sekundę, i zrobi to ponownie przy następnym skoku upstreamowym. tools/audit_pdfium_public_api.py jest celowo mały: dopasowuje regexem FPDF_EXPORT ... FPDF_CALLCONV name( w każdym nagłówku w katalogu publicznym, dopasowuje regexem każde CheckGetProcAddress('Name') i TryGetProcAddress('Name') w PDFium.pas, i wypisuje różnicę dwóch zbiorów: missing dla eksportów bez wiązania, stale dla wiązań, których eksport już nie istnieje upstream. Kończy się kodem niezerowym, gdy którykolwiek zbiór jest niepusty, więc wpina się w krok buildu bez dalszej ceremonii. Bieżący wynik to 470 z 470 związanych, missing 0, stale 0
Kierunek stale zarabia na siebie tak samo jak missing. Eksport, który upstream usuwa, zostawia po sobie linię CheckGetProcAddress, która twardo zawiedzie każde przyszłe wczytanie, a ten rodzaj gnicia jest niewidoczny, dopóki ktoś nie zaktualizuje DLL. Ręczny przegląd znajduje funkcję, o której myślałeś; nie znajduje tej, o której nie myślałeś. Zauważ też, że audyt celowo liczy oba loadery jako pokrycie, co jest właściwą decyzją wobec dryfu API i powodem, dla którego podział wymagane/opcjonalne musi być udokumentowaną decyzją, a nie produktem ubocznym tego, kto dodał linię
Gdzie opcjonalne wiązanie przestaje być uczciwe
Warto jasno wskazać dwie granice, ponieważ ten wzorzec łatwo nadużyć. Pierwsza jest taka, że wskaźnik funkcji nil jest bezpieczny tylko wtedy, gdy dosłownie każda ścieżka, która go dotyka, testuje najpierw Assigned. W module deklarującym setki zmiennych funkcji cdecl pojedyncze niestrzeżone wywołanie to naruszenie dostępu pod adresem, który nic nie znaczy w śladzie stosu. Ta sama dyscyplina rządząca konwencjami wywołań i czasami życia przez granicę C ma tu zastosowanie, i jest tematem artykułu o utwardzaniu wiązania PDFium wobec błędów ABI i bezpieczeństwa pamięci
Druga granica to zakres. Opcjonalne wiązanie nie jest ogólną licencją na uczynienie wszystkiego tolerancyjnym. Gdyby FPDF_RenderPageBitmap było opcjonalne, komponent wczytałby się bez problemu, a potem zawodziłby na każdej stronie, zamieniając jeden jasny błąd startowy w rozproszenie błędów w czasie działania bez oczywistej przyczyny. Wymagane jest właściwym domyślnym. Opcjonalne jest wyjątkiem, po który sięgasz, gdy funkcja jest naprawdę liściem, gdy brak ma obronne zdegradowane zachowanie po stronie odczytu, i gdy strona zapisu może odmówić komunikatem nazywającym powód
Projekt loadera, sondy możliwości i narzędzie audytu opisane tutaj są dostarczane jako część PDFium Component dla Delphi i C++Builder; strona produktu wymienia dołączone binaria PDFium i pełną powierzchnię API, jaką udostępniają