Když se pdfium.dll odmítne načíst na klientském počítači, komponenta PDFium pro Delphi a Lazarus promění kryptickou nativní chybu v konkrétní příčinu. Rutina CheckLoadLibrary vyhodnotí chybu ERROR_BAD_EXE_FORMAT (193) jako nesoulad 32/64bitové architektury a CheckGetProcAddress nahlásí chybějící export jako stav, kdy je knihovna pdfium.dll starší než vazba (binding). Obě zprávy přímo pojmenovávají řešení, místo aby vás nechaly hádat
Na tom záleží, protože nativní DLL selhává třemi odlišnými způsoby a čistý text z operačního systému je všechny spojuje dohromady. Architektura může být špatná, nasazený binární soubor může být příliš starý pro vazbu v Pascalu, která jej volá, nebo se dvě vlákna mohou pokusit jej navázat ve stejném okamžiku. Každý případ má jiné řešení a smyslem diagnostiky přidané v rámci prací na životním cyklu načítání ve verzi v2.11.0 je sdělit vám, na co se vlastně díváte
Proč načítání pdfium.dll selhává s chybou špatného formátu EXE?
Chyba Windows 193, ERROR_BAD_EXE_FORMAT, znamená, že soubor byl nalezen a otevřen, ale typ jeho stroje PE neodpovídá hostitelskému procesu. 64bitový spustitelný soubor nemůže načíst 32bitovou knihovnu pdfium.dll a 32bitový spustitelný soubor nemůže načíst 64bitovou. Soubor je přítomen, takže instinktivní hledání chybějící DLL vás pošle přesně opačným směrem
Komponenta PDFium zachycuje tento případ v CheckLoadLibrary: když LoadLibrary vrátí nulový handle a GetLastError is 193, připojí nápovědu, že DLL byla nalezena, ale její architektura neodpovídá hostitelskému procesu, a ukáže na odpovídající sestavení v DLLs/Win32 nebo DLLs/Win64. Podadresář je vybrán funkcí BuildDllSubDir, která vrací Win64, pokud je IsWin64 true, a Win32 v opačném případě. Nasaďte DLL do stromu, který vazba skutečně prohledává, a nesoulad zmizí. Jedna past ovšem sedí ve volajícím kódu, nikoli v nasazení: TPdf.SetActive spolkne každé selhání načtení, takže Active := True nechá komponentu neaktivní bez vyhození výjimky a blok except kolem ní se nikdy nespustí. Chcete-li vidět zprávu, zavolejte LoadDocument se záznamem TPdfLoadOptions a s out parametrem TPdfLoadReport, který vyhodí skutečnou výjimku EPdfError a zapíše do reportu plsFailed s textem chyby. Od PDFiumPas v3.122.1 navíc neúspěšné Active := True nahradí LastLoadReport takovým reportem o selhání, takže kód, který u property cesty zůstává, si může přečíst příčinu z LastLoadReport.ErrorMessage místo hádání
uses
SysUtils, PDFium;
procedure OpenDocument(const AFileName: string);
var
Pdf: TPdf;
Report: TPdfLoadReport;
begin
// Nasadit pdfium.dll vedle spustitelného souboru v závislosti na cíli sestavení:
// <AppDir>\DLLs\Win32\pdfium.dll pro 32bitový hostitelský proces
// <AppDir>\DLLs\Win64\pdfium.dll pro 64bitový hostitelský proces
Pdf := TPdf.Create(nil);
try
Pdf.FileName := AFileName;
try
// Active := True by selhání spolklo a zůstalo False; toto
// přetížení líně naváže pdfium.dll a vyhodí skutečnou výjimku
Pdf.LoadDocument(TPdfLoadOptions.Default(plmCompatible), Report);
except
on E: EPdfError do
// Zpráva již pojmenovává skutečnou příčinu: nesoulad 32/64bitové architektury
// (ERROR_BAD_EXE_FORMAT) nebo knihovna pdfium.dll starší než tato vazba;
// Report.Status je plsFailed a Report.ErrorMessage drží text
raise Exception.CreateFmt('PDF engine did not start: %s', [E.Message]);
end;
RenderFirstPage(Pdf); // Pdf.Active je True, jakmile se LoadDocument vrátí
finally
Pdf.Free;
end;
end;
Co vám sdělí chybějící export z PDFium?
Druhou třídou selhání je verze skew. PDFium postupem času přináší nové exporty a vazba v Pascalu řeší každou funkci, kterou potřebuje, pomocí CheckGetProcAddress během LoadLibrary. Pokud požadovaný export chybí, vazba je novější než pdfium.dll na disku a upřímná diagnóza zní, že nasazená DLL je zastaralá, nikoli poškozená. Funkce CheckGetProcAddress uvádí přesně toto: vyvolá výjimku EPdfError s hlášením, že nasazená pdfium.dll je starší než toto sestavení vazby PDFiumPas, a pojmenuje cestu DLLs/<subdir>, kam odpovídající binární soubor patří
Dva detaily činí tuto cestu robustní. Za prvé, CheckGetProcAddress volá UnloadLibrary předtím, než vyvolá výjimku, takže částečné navázání nikdy neponechá napůl vyřešené ukazatele na funkce, o které by mohl zakopnout další pokus. Za druhé, název DLL, který hlásí, pochází z BuildDllName, což vrací standardně pdfium.dll a pdfium.v8.dll, když je nastaven globální příznak EnableV8Engine. Pokud vaše aplikace povoluje JavaScript engine V8 pro XFA nebo skriptované formuláře, text chyby ukazuje na sestavení s V8, nikoli na obyčejné, takže hned napoprvé nahradíte správný soubor
Je bezpečné načítat PDFium DLL z vlákna na pozadí?
Načítání knihovny je nyní bezpečné z jakéhooli vlákna; volání API PDFium z více vláken stále není. Jedná se o dvě samostatné záruky a jejich oddělování představuje rozdíl mezi stabilním fondem pracovních vláken a občasným pádem. Vykreslování na pozadí obvykle spouští vykreslení každé stránky prostřednictvím pracovního vlákna TPdfFuture<T> a první future, která se dotkne TPdf, vyvolá líné navázání. Bez ochrany by oba pracovníci mohli zaznamenat nenačtenou knihovnu, oba spustit sekvenci navázání, přepsat handle modulu a způsobit únik prvního načtení
Práce ve verzi v2.11.0 toto okno zavírají pomocí PDFiumLoadLock, což je procesně globální TRTLCriticalSection, která obaluje vstup do LoadLibrary i UnloadLibrary. Kritická sekce činí sekvenci kontroly a následného načtení atomickou, takže žádná dvě vlákna nemohou obě vidět Loaded=False a provést dvojité navázání, a žádné vlákno nemůže uvolnit DLL, zatímco jiné je uprostřed navazování. Zámek se vytváří v sekci initialization jednotky a uvolňuje se v sekci finalization, chráněn příznakem PDFiumLoadLockReady, takže párování je bezpečné i během vypínání. Pokud potřebujete kompletní vzor typu pracovník-a-odpověď se zrušením, doprovodný článek o vykreslování na pozadí se zrušitelnými futures jej popisuje od začátku do konce
uses
PDFium, FPdfAsync;
// Pracovní metoda běží na vlákně na pozadí. První future, která naváže
// pdfium.dll, je serializována pomocí PDFiumLoadLock, takže druhý souběžný
// pracovník nemůže provést dvojité navázání ani způsobit únik handle modulu.
function TReportForm.RenderThumbnail(
const AToken: IPdfCancellationToken): TBitmap;
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.Active := True; // bezpečné souběžné navázání
AToken.ThrowIfCancelled;
Result := Pdf.Thumbnail; // tento 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;
Co zámek načtení nechrání
Hranici zde stojí za to uvést na rovinu, protože je snadné si opravu špatně vyložit. PDFiumLoadLock serializuje pouze globální stav navázání: handle modulu a přiřazení ukazatelů na funkce FPDF_*. Podkladové C API PDFium stále není thread-safe, přesně jak deklarují hlavičky upstreamu, takže volání funkcí FPDF_* nebo sdílení jednoho FPDF_DOCUMENT napříč vlákny zůstává na vaší odpovědnosti za serializaci. Bezpečný tvar je ten výše: každý pracovník vlastní svůj vlastní TPdf a nikdy nepředává živý dokument jinému vláknu. Pro hlubší rozbor této hranice a pravidel ABI kolem ní viz zabezpečení vazby PDFium VCL proti chybám ABI a paměti
Proč na sekci finalization a FPDF_DestroyLibrary záleží
Jemnější mezera byla v cestě ukončení. Jednotka PDFium neměla sekci finalization, což znamenalo, že se při ukončení procesu nikdy nespustilo FPDF_DestroyLibrary; operační systém získal zpět paměť DLL a přeskočil vlastní čištění PDFium, konkrétně připojení pracovního vlákna, uvolnění izolátu V8, když je nastaveno EnableV8Engine, a zrušení písma a mezipaměti. U běžné zátěže vykreslování se jedná o tichý únik při ukončení, ale s povoleným enginem V8 uniká JavaScript izolát při každém spuštění, což se při opakované automatizaci rychle projeví
Sekce finalization nyní volá UnloadLibrary, což vyvolá FPDF_DestroyLibrary a uvolní modul. To žije na úrovni jednotky z nějakého důvodu: TPdf.Destroy pouze nastavuje Active := False pro zavření aktuálního dokumentu a záměrně neuvolňuje globální knihovnu, takže víceinstanční aplikace udržuje jedno sdílené navázání PDFium naživu po celou dobu životnosti procesu. Umístění čištění do sekce finalization znamená, že sdílená knihovna se uvolní přesně jednou, když se jednotka uvolní, a UnloadLibrary nejprve zkontroluje svůj příznak Loaded, takže volání je neškodný no-op, pokud PDFium nebylo nikdy použito
Krátký kontrolní seznam pro nasazení
Tři návyky předcházejí téměř každému selhání načtení v praxi. Slaďte architekturu DLL s hostitelským procesem a expedujte ji pod DLLs/Win32 nebo DLLs/Win64; udržujte pdfium.dll v souladu s verzí vazby, aby žádný požadovaný export nechyběl; a nikdy nesdílejte TPdf nebo handle dokumentu napříč vlákny, i když je samotné navázání nyní atomické. Pokud sestavujete stejný zdrojový kód pro Delphi i Lazarus, rozdíly v balení, které komplikují nasazení napříč cíli, jsou popsány v poznámkách o úskalích křížového překladače Delphi a FPC. Diagnostika, zámek načtení a čištění ve finalization popsané v tomto článku se dodávají v komponentě PDFium Component pro Delphi a C++Builder