Technický článek

Nasazení PDFium DLL v Delphi: Řešení chyb při načítání

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

Diagram tří režimů selhání načtení pdfium.dll v Delphi se signálem a opravou pro každý, od chyby 193 neshody architektur po zastaralou DLL a souběžnou dvojí vazbu
Nativní DLL může selhat na architektuře, rozjezdu verzí, nebo souběžné vazbě a diagnostika pojmenuje, který ze tří se skutečně přihodil

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í

Diagram ukazující BuildDllSubDir volící složku pdfium.dll Win32 nebo Win64 podle bitovosti hostitele, takže aplikace Delphi se vyhne chybě 193 neshody architektur
BuildDllSubDir vybere složku DLL podle bitnosti hostitele a dodání druhého buildu se projeví jako chyba 193, nikoli jako chybějící soubor

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

Diagram dvou pracovních vláken TPdfFuture serializovaných critical section PDFiumLoadLock, takže pdfium.dll se sváže přesně jednou v aplikaci Delphi
PDFiumLoadLock dělá sekvenci zkontroluj-pak-načti atomickou, zatímco každý worker si stále drží vlastní TPdf, protože C API PDFium není thread-safe

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