Gdy biblioteka pdfium.dll nie chce się załadować na maszynie klienta, komponent PDFium dla Delphi i Lazarus zamienia niejasny błąd natywny na konkretną przyczynę. Procedura CheckLoadLibrary odczytuje błąd ERROR_BAD_EXE_FORMAT (193) as a 32/64-bit architecture mismatch, and CheckGetProcAddress reports a missing export as a pdfium.dll that is older than the binding. Oba komunikaty wskazują rozwiązanie, zamiast pozostawiać Cię z domysłami
Ma to znaczenie, ponieważ natywna biblioteka DLL może ulec awarii na trzy różne sposoby, a surowy tekst systemu operacyjnego łączy je wszystkie. Architektura może być niepoprawna, wdrożony plik binarny może być zbyt stary dla wywołującego go powiązania Pascala lub dwa wątki mogą próbować powiązać go w tym samym momencie. Każda z tych sytuacji wymaga innego rozwiązania, a celem diagnostyki dodanej w pracach nad cyklem życia ładowania w wersji v2.11.0 jest wskazanie, na który przypadek faktycznie patrzysz
Dlaczego pdfium.dll nie ładuje się z błędem nieprawidłowego formatu EXE?
Błąd systemu Windows 193, ERROR_BAD_EXE_FORMAT, oznacza, że plik został znaleziony i otwarty, ale typ maszyny PE nie pasuje do procesu hosta. 64-bitowy plik wykonywalny nie może załadować 32-bitowej biblioteki pdfium.dll, a 32-bitowy plik wykonywalny nie może załadować 64-bitowej. Plik jest obecny, więc instynkt poszukiwania brakującej biblioteki DLL kieruje Cię w zupełnie złym kierunku
Komponent PDFium przechwytuje ten przypadek w CheckLoadLibrary: gdy LoadLibrary zwraca pusty uchwyt, a GetLastError to 193, dołącza wskazówkę, że biblioteka DLL została znaleziona, ale jej architektura nie pasuje do procesu hosta, i wskazuje na pasującą wersję z DLLs/Win32 lub DLLs/Win64. Podkatalog jest wybierany przez BuildDllSubDir, który zwraca Win64, gdy IsWin64 ma wartość true, i Win32 w przeciwnym razie. Wdróż bibliotekę DLL w drzewie katalogów, które powiązanie faktycznie przeszukuje, a niezgodność zniknie
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;
O czym informuje brak eksportu PDFium?
Drugą klasą błędów jest rozbieżność wersji. PDFium z czasem udostępnia nowe eksporty, a powiązanie Pascala rozwiązuje każdą potrzebną funkcję za pomocą CheckGetProcAddress podczas LoadLibrary. Jeśli wymagany eksport jest nieobecny, powiązanie jest nowsze niż pdfium.dll na dysku, a uczciwa diagnoza brzmi, że wdrożona biblioteka DLL jest przestarzała, a nie uszkodzona. Funkcja CheckGetProcAddress stwierdza dokładnie to: zgłasza błąd EPdfError informujący, że wdrożona biblioteka pdfium.dll jest starsza niż ta kompilacja powiązania PDFiumPas, i podaje ścieżkę DLLs/<subdir>, do której należy pasujący plik binarny
Dwa szczegóły sprawiają, że ta ścieżka jest solidna. Po pierwsze, CheckGetProcAddress wywołuje UnloadLibrary przed zgłoszeniem błędu, więc częściowe powiązanie nigdy nie pozostawia na wpół rozwiązanych wskaźników funkcji dla kolejnej próby. Po drugie, nazwa biblioteki DLL, którą zgłasza, pochodzi z BuildDllName, która normalnie zwraca pdfium.dll, a pdfium.v8.dll, gdy ustawiona jest globalna flaga EnableV8Engine. Jeśli Twoja aplikacja włącza silnik JavaScript V8 dla formularzy XFA lub skryptowych, tekst błędu wskazuje na kompilację V8, a nie zwykłą, dzięki czemu od razu wymieniasz właściwy plik
Czy ładowanie biblioteki DLL PDFium z wątku pobocznego jest bezpieczne?
Ładowanie biblioteki jest teraz bezpieczne z dowolnego wątku; wywoływanie interfejsu API PDFium z kilku wątków nadal jednak nie jest bezpieczne. Są to dwie oddzielne gwarancje, a utrzymanie tego podziału decyduje o stabilności puli wątków roboczych i zapobiega sporadycznym awariom. Renderowanie w tle zazwyczaj wykonuje każdy render strony przez wątek roboczy TPdfFuture<T>, a pierwsze wywołanie, które dotyka TPdf, uruchamia leniwe powiązanie. Bez ochrony dwa wątki robocze mogłyby jednocześnie zauważyć niezaładowaną bibliotekę, oba uruchomić sekwencję powiązania, nadpisać uchwyt modułu i doprowadzić do wycieku przy pierwszym załadowaniu
Prace w wersji v2.11.0 zamykają to okno za pomocą PDFiumLoadLock, globalną dla procesu sekcją krytyczną TRTLCriticalSection, która otacza wejście zarówno do LoadLibrary, jak i UnloadLibrary. Sekcja krytyczna sprawia, że sekwencja sprawdzania i ładowania jest atomowa, więc żadne dwa wątki nie mogą jednocześnie zobaczyć statusu Loaded=False i wykonać podwójnego powiązania, a żaden wątek nie może zwolnić biblioteki DLL, gdy inny jest w trakcie jej wiązania. Blokada jest tworzona w sekcji initialization modułu i usuwana w finalization, chroniona flagą PDFiumLoadLockReady, dzięki czemu parowanie jest bezpieczne nawet podczas zamykania systemu. Jeśli potrzebujesz pełnego wzorca wątku roboczego i odpowiedzi z możliwością anulowania, powiązany artykuł na temat renderowania w tle z możliwością anulowania zadań przyszłych (futures) omawia to od początku do końca
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;
Czego nie chroni blokada ładowania
Granica jest tutaj warta jasnego przedstawienia, ponieważ łatwo przecenić to rozwiązanie. PDFiumLoadLock szereguje jedynie globalny stan powiązania: uchwyt modułu i przypisania wskaźników funkcji FPDF_*. Podstawowy interfejs API C biblioteki PDFium nadal nie jest bezpieczny wątkowo, dokładnie tak, jak deklarują nagłówki projektu, więc wywoływanie funkcji FPDF_* lub udostępnianie jednego obiektu FPDF_DOCUMENT w różnych wątkach pozostaje w Twojej odpowiedzialności pod kątem synchronizacji. Bezpiecznym podejściem jest to przedstawione powyżej: każdy wątek roboczy posiada własny obiekt TPdf i nigdy nie przekazuje działającego dokumentu do innego wątku. Więcej informacji na temat tej granicy i zasad ABI wokół niej można znaleźć w artykule na temat wzmacniania powiązania PDFium VCL przed błędami ABI i pamięci
Dlaczego sekcja finalization i funkcja FPDF_DestroyLibrary mają znaczenie
Subtelniejsza luka znajdowała się w ścieżce zamykania aplikacji. Moduł PDFium nie posiadał sekcji finalization, co oznaczało, że funkcja FPDF_DestroyLibrary nigdy nie była wywoływana przy wyjściu z procesu; system operacyjny odzyskiwał pamięć biblioteki DLL i pomijał własne czyszczenie PDFium, mianowicie łączenie wątków roboczych, usuwanie izolatu V8 przy ustawionym EnableV8Engine oraz zwalnianie czcionek i pamięci podręcznej. W przypadku prostego renderowania jest to cichy wyciek przy zamykaniu, ale przy włączonym silniku V8 powoduje to wyciek izolatu JavaScript przy każdym uruchomieniu, co szybko ujawnia się przy wielokrotnej automatyzacji
Sekcja finalization wywołuje teraz UnloadLibrary, co uruchamia funkcję FPDF_DestroyLibrary i zwalnia moduł. Umieszczenie tego w zakresie modułu ma swój powód: metoda TPdf.Destroy ustawia jedynie Active := False, aby zamknąć bieżący dokument, i celowo nie ładuje globalnej biblioteki, więc aplikacja wieloinstancyjna utrzymuje jedno współdzielone powiązanie PDFium przy życiu przez cały okres eksploatacji procesu. Umieszczenie zwalniania w finalization oznacza, że współdzielona biblioteka jest zwalniana dokładnie raz, przy zwalnianiu modułu, a UnloadLibrary najpierw sprawdza flagę Loaded, dzięki czemu wywołanie jest nieszkodliwą operacją pustą, jeśli PDFium nigdy nie był używany
Krótka lista kontrolna wdrożenia
Trzy nawyki zapobiegają prawie każdemu błędowi ładowania w terenie. Dopasuj architekturę biblioteki DLL do procesu hosta i dostarcz ją w folderze DLLs/Win32 lub DLLs/Win64; utrzymuj spójność pdfium.dll z wersją powiązania, aby nie zabrakło żadnego wymaganego eksportu; i nigdy nie udostępniaj obiektu TPdf ani uchwytu dokumentu między wątkami, mimo że samo powiązanie jest teraz atomowe. Jeśli budujesz ten sam kod źródłowy dla Delphi i Lazarus, różnice w pakowaniu, które mogą utrudnić wdrożenie między różnymi platformami docelowymi, omówiono w uwagach na temat pułapek kompilatora krzyżowego Delphi i FPC. Diagnostyka, blokada ładowania i czyszczenie w sekcji finalizacji opisane tutaj są dostarczane w komponencie PDFium dla Delphi i C++Builder