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

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í

uses
  SysUtils, PDFium;

procedure OpenDocument(const AFileName: string);
var
  Pdf: TPdf;
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
      Pdf.Active := True;   // první použití líně naváže pdfium.dll
    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
        raise Exception.CreateFmt('PDF engine did not start: %s', [E.Message]);
    end;
    if Pdf.Active then
      RenderFirstPage(Pdf);
  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/`, 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