Technikai cikk

A PDFium DLL telepítése Delphiben: Betöltési hibák javítása

Ha a pdfium.dll nem hajlandó betöltődni az ügyfél gépén, a Delphihez és Lazarushoz készült PDFium Component a rejtélyes natív hibát konkrét okra vezeti vissza. A CheckLoadLibrary rutin az ERROR_BAD_EXE_FORMAT (193) hibát 32/64 bites architektúra-eltérésként olvassa be, a CheckGetProcAddress pedig a hiányzó exportálást úgy jelenti, mint egy olyan pdfium.dll-t, amely régebbi, mint a Pascal-kötés (binding). Mindkét üzenet megnevezi a javítást, ahelyett, hogy találgatásra kényszerítené

Ez azért fontos, mert egy natív DLL háromféle módon hibásodhat meg, és az operációs rendszer nyers szövege mindhármat összemossa. Lehet rossz az architektúra, a telepített bináris túl régi lehet a Pascal-kötéshez, amely meghívja, vagy két szál versenghet (race) a betöltésért ugyanabban a pillanatban. Mindegyikre más megoldás létezik, és a v2.11.0-s verzió betöltési életciklus-fejlesztéseiben hozzáadott diagnosztika célja pontosan az, hogy megmondja, melyik esettel áll szemben

Miért nem töltődik be a pdfium.dll rossz EXE formátum hibával?

A Windows 193-as hibája, az ERROR_BAD_EXE_FORMAT azt jeleni, hogy a fájl megtalálható és megnyitható volt, de a PE géptípusa (PE machine type) nem egyezik a gazdafolyamat típusával. Egy 64 bites futtatható fájl nem tud betölteni egy 32 bites pdfium.dll-t, egy 32 bites futtatható fájl pedig nem tud betölteni egy 64 biteset. A fájl jelen van, így a hiányzó DLL utáni kutatás ösztöne pontosan a rossz irányba terel

A PDFium Component elcsípi ezt az esetet a CheckLoadLibrary metódusban: ha a LoadLibrary null handle-t ad vissza és a GetLastError értéke 193, hozzáfűz egy utalást, hogy a DLL megtalálható volt, de az architektúrája nem felel meg a gazdafolyamatnak, és rámutat a megfelelő DLLs/Win32 vagy DLLs/Win64 buildre. Az alkönyvtárat a BuildDllSubDir választja ki, amely Win64-et ad vissza, ha az IsWin64 igaz, és Win32-t egyébként. Telepítse a DLL-t abba a könyvtárstruktúrába, amelyet a kötés valójában keres, és az eltérés megszűnik

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;

Mit árul el a hiányzó PDFium export?

A második hibaosztály a verzióeltérés (version skew). A PDFium idővel új exportokat bocsát ki, a Pascal-kötés pedig minden szükséges függvényt felold a CheckGetProcAddress segítségével a LoadLibrary során. Ha a szükséges export hiányzik, a kötés újabb, mint a lemezen lévő pdfium.dll, és az őszinte diagnózis az hogy a telepített DLL elavult, nem pedig sérült. A CheckGetProcAddress függvény pontosan ezt állítja: egy EPdfError kivételt vált ki, jelentve, hogy a telepített pdfium.dll régebbi, mint a PDFiumPas kötés ezen buildje, és megnevezi a DLLs/<subdir> útvonalat, ahová a megfelelő bináris tartozik

Két részlet teszi robusztussá ezt az utat. Először is, a CheckGetProcAddress meghívja az UnloadLibrary metódust, mielőtt kivételt váltana ki, így egy részleges kötés soha nem hagy félig feloldott függvénymutatókat maga után, amelyeken a következő kísérlet elbukhatna. Másodszor, az általa jelentett DLL név a BuildDllName-ből származik, amely normál esetben pdfium.dll-t ad vissza, a globális EnableV8Engine jelző beállításakor pedig pdfium.v8.dll-t. Ha az alkalmazása engedélyezi a V8 JavaScript motort az XFA vagy szkriptelt űrlapokhoz, a hibaszöveg a V8 buildre mutat, nem pedig az egyszerűre, így az első alkalommal a megfelelő fájlt cseréli ki

Biztonságos a PDFium DLL betöltése háttérszálból?

A könyvtár betöltése most már biztonságos bármely szálból; a PDFium API meghívása több szálból azonban továbbra sem az. Ez két különálló garancia, és ezek elkülönítése a különbség a stabil munkavégző készlet (worker pool) és a rendszertelen összeomlások között. A háttér-renderelés jellemzően minden oldalrenderelést egy TPdfFuture<T> munkavégzőn keresztül futtat, és az első jövő (future), amely megérinti a TPdf-et, indítja el a lusta kötést (lazy bind). Védelem nélkül két munkavégző mindegyike észlelhetné a be nem töltött könyvtárat, mindkettő lefuttatná a kötési szekvenciát, felülírná a modul handle-t, és szivárogtatná az első betöltést

A v2.11.0-s fejlesztés bezárja ezt a rést a PDFiumLoadLock segítségével, amely egy folyamat-globális TRTLCriticalSection, és körbeveszi a belépést a LoadLibrary és az UnloadLibrary hívásokhoz. A kritikus szakasz atomivá teszi az ellenőrzés-majd-betöltés szekvenciát, így két szál nem láthatja egyszerre a Loaded=False állapotot és nem végezhet dupla kötést, és egyetlen szál sem szabadíthatja fel a DLL-t, miközben egy másik a betöltés közepén jár. A zár a unit initialization szakaszában jön létre, és a finalization szakaszban bomlik le, egy PDFiumLoadLockReady jelzővel őrizve, így a párosítás még a leállás során is biztonságos. Ha a teljes munkavégző-és-válasz mintára (worker-and-reply pattern) van szüksége megszakítással együtt, a háttér-renderelésről megszakítható jövőkkel (futures) szóló kísérő cikk végigvezeti ezen

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;

Amit a betöltési zár nem véd

A határt érdemes itt nyíltan kimondani, mert könnyű túlértékelni a javítást. A PDFiumLoadLock csak a globális kötési állapotot szerializálja: a modul handle-t és az FPDF_* függvénymutató-hozzárendeléseket. A mögöttes PDFium C API továbbra sem szálbiztos, pontosan úgy, ahogy azt a forrásfejlécek deklarálják, így az FPDF_* függvények meghívása vagy egyetlen FPDF_DOCUMENT megosztása a szálak között továbbra is az Ön felelőssége a szerializálás tekintetében. A biztonságos forma a fenti: minden munkavégző saját TPdf-et birtokol, és soha nem ad át élő dokumentumot egy másik szálnak. E határ és a körülötte lévő ABI szabályok mélyebb tárgyalásához lásd a PDFium VCL kötés megerősítése az ABI és memóriahibák ellen cikket

Miért fontos a finalization és a FPDF_DestroyLibrary

Egy finomabb rés tátongott a leállítási útvonalon. A PDFium egységnek (unit) nem volt finalization szakasza, ami azt jelentette, hogy az FPDF_DestroyLibrary soha nem futott le a folyamatból való kilépéskor; az operációs rendszer visszakövetelte a DLL memóriáját, és kihagyta a PDFium saját tisztítási folyamatait, nevezetesen a munkaszál-csatlakozást (worker-thread join), a V8 izolátum megsemmisítését, ha a EnableV8Engine be van állítva, valamint a betűtípusok és a gyorsítótár lebontását. Egy egyszerű renderelési munkaterhelés esetén ez egy csendes szivárgás a leállításkor, de a V8 motor engedélyezésével minden futáskor szivárogtat egy JavaScript izolátumot, ami ismételt automatizálás esetén gyorsan megmutatkozik

A finalization szakasz most már meghívja az UnloadLibrary metódust, amely meghívja az FPDF_DestroyLibrary-t és felszabadítja a modult. Ez okkal él a unit szintjén: a TPdf.Destroy csak az Active := False értéket állítja be az aktuális dokumentum lezárásához, és szándékosan nem tölti ki a globális könyvtárat, így a több példányt használó alkalmazás egyetlen közös PDFium kötést tart életben a teljes folyamat élettartama alatt. A lebontás elhelyezése a finalization szakaszban azt jelenti, hogy a megosztott könyvtár pontosan egyszer szabadul fel, amikor a unit betöltése megszűnik, az UnloadLibrary pedig először ellenőrzi a Loaded jelzőjét, így a hívás ártalmatlan no-op, ha a PDFium-ot soha nem használták

Rövid telepítési ellenőrzőlista

Három szokás megelőzi szinte az összes helyszíni betöltési hibát. Egyeztesse a DLL architektúráját a gazdafolyamattal, és szállítsa azt a DLLs/Win32 vagy DLLs/Win64 könyvtárba; tartsa a pdfium.dll-t összhangban a kötés verziójával, hogy ne hiányozzon szükséges export; és soha ne ossza meg a TPdf-et vagy egy dokumentum handle-t szálak között, annak ellenére, hogy maga a kötés most már atomi. Ha ugyanazt a forrást építi fel Delphire és Lazarusra is, a kereszt-célpont telepítést megnehezítő csomagolási különbségeket a Delphi és FPC keresztfordító csapdáiról szóló jegyzetek tárgyalják. Az itt leírt diagnosztika, a betöltési zár és a finalization tisztítás mind megtalálható a Delphihez és C++Builderhez készült PDFium Component-ben