Tehnički članak

Implementacija PDFium DLL-a u Delphiju: rješavanje neuspjeha učitavanja

Kada se pdfium.dll odbije učitati na korisničkom računalu, PDFium komponenta za Delphi i Lazarus pretvara zagonetnu izvornu pogrešku u specifičan uzrok. Rutina CheckLoadLibrary čita ERROR_BAD_EXE_FORMAT (193) kao neslaganje 32/64-bitne arhitekture, a CheckGetProcAddress prijavljuje nedostajući izvoz kao znak da je pdfium.dll stariji od povezivanja. Obje poruke imenuju rješenje umjesto da vas ostave da nagađate

To je važno jer izvorni DLL može zakazati na tri različita načina, a sirovi tekst operacijskog sustava ih sve spaja. Arhitektura može biti pogrešna, implementirani binarni kôd može biti prestar za Pascal povezivanje koje ga poziva, ili se dvije dretve (threads) mogu utrkivati da ga povežu u istom trenutku. Svaki problem ima drugačije rješenje, a cijeli smisao dijagnostike dodane u verziji v2.11.0 u radu s životnim ciklusom učitavanja jest reći vam što zapravo gledate

Zašto se pdfium.dll ne uspijeva učitati uz pogrešku lošeg EXE formata?

Windows pogreška 193, ERROR_BAD_EXE_FORMAT, znači da je datoteka pronađena i otvorena, ali njezin tip stroja u PE zaglavlju ne odgovara procesu domaćinu. 64-bitna izvršna datoteka ne može učitati 32-bitni pdfium.dll, a 32-bitna izvršna datoteka ne može učitati 64-bitni. Datoteka je prisutna, pa vas instinkt da tražite nedostajući DLL šalje u potpuno pogrešnom smjeru

PDFium komponenta presreće ovaj slučaj u CheckLoadLibrary: kada LoadLibrary vrati nul-pokazivač (null handle), a GetLastError je 193, ona dodaje napomenu da je DLL pronađen, ali da njegova arhitektura ne odgovara procesu domaćinu, te upućuje na odgovarajuću DLLs/Win32 ili DLLs/Win64 verziju. Poddirektorij odabire funkcija BuildDllSubDir, koja vraća Win64 kada je IsWin64 istinito, a u suprotnom Win32. Implementirajte DLL u stablo koje povezivanje zapravo pretražuje i neslaganje će nestati

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;

Što vam govori nedostajući PDFium izvoz?

Druga klasa neuspjeha je neslaganje verzija. PDFium s vremenom isporučuje nove izvoze (exports), a Pascal povezivanje razrješava svaku funkciju koja mu je potrebna kroz CheckGetProcAddress tijekom poziva LoadLibrary. Ako potreban izvoz nedostaje, povezivanje je novije od datoteke pdfium.dll na disku, a prava dijagnoza je da je implementirani DLL zastario, a ne oštećen. Funkcija CheckGetProcAddress navodi točno to: podiže pogrešku EPdfError prijavljujući da je implementirani pdfium.dll stariji od ove verzije PDFiumPas povezivanja i navodi putanju DLLs/<subdir> kamo pripada odgovarajući binarni kôd

Dva detalja čine tu putanju robusnom. Prvo, CheckGetProcAddress poziva UnloadLibrary prije nego što podigne iznimku, tako da djelomično povezivanje nikada ne ostavlja polovično razriješene pokazivače funkcija s kojima bi se sljedeći pokušaj mogao sudariti. Drugo, naziv DLL-a koji prijavljuje dolazi iz funkcije BuildDllName, koja uobičajeno vraća pdfium.dll, a pdfium.v8.dll kada je postavljena globalna zastavica EnableV8Engine. Ako vaša aplikacija omogućuje V8 JavaScript pokretač za XFA or skriptirane obrasce, tekst pogreške upućivat će na V8 verziju, a ne na običnu, tako da odmah zamijenite ispravnu datoteku

Je li PDFium DLL siguran za učitavanje iz pozadinske dretve?

Učitavanje knjižnice sada je sigurno iz bilo koje dretve; pozivanje PDFium API-ja iz nekoliko dretvi i dalje nije. To su dva odvojena jamstva, a njihovo držanje odvojenima čini razliku između stabilnog skupa radnika (worker pool) i povremenih rušenja. Pozadinsko iscrtavanje obično pokreće iscrtavanje svake stranice kroz radnika TPdfFuture<T>, a prva budućnost (future) koja dotakne TPdf pokreće lijeno povezivanje. Bez zaštite, dva bi radnika mogla uočiti neučitanu knjižnicu, oba pokrenuti sekvencu povezivanja, prepisati pokazivač modula i izgubiti referencu na prvo učitavanje

Rad u verziji v2.11.0 zatvara taj prozor s PDFiumLoadLock, kritičnom sekcijom TRTLCriticalSection globalnom na razini procesa koja obavija ulaz u LoadLibrary i UnloadLibrary. Kritična sekcija čini slijed provjere i učitavanja atomarnim, tako da nijedna dretva ne može istovremeno vidjeti Loaded=False i napraviti dvostruko povezivanje, niti jedna dretva može osloboditi DLL dok je druga u procesu povezivanja. Brava se stvara u odjeljku initialization jedinice (unit) i uklanja u odjeljku finalization, zaštićena zastavicom PDFiumLoadLockReady kako bi uparivanje bilo sigurno čak i tijekom gašenja. Ako vam je potreban puni obrazac radnika i odgovora s otkazivanjem, popratni članak o pozadinskom iscrtavanju s mogućnošću otkazivanja budućnosti vodi vas kroz to od početka do kraja

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;

Što brava za učitavanje ne štiti

Granicu ovdje vrijedi jasno istaknuti, jer je lako preuveličati ovo rješenje. PDFiumLoadLock serijalizira samo globalno stanje povezivanja: pokazivač modula i dodjele pokazivača funkcija FPDF_*. Temeljni PDFium C API i dalje nije siguran za rad s dretvama (thread-safe), točno onako kako to deklariraju izvorišna zaglavlja, pa pozivanje funkcija FPDF_* ili dijeljenje jednog FPDF_DOCUMENT-a kroz dretve ostaje vaša odgovornost za serijalizaciju. Siguran oblik je onaj gore prikazan: svaki radnik posjeduje vlastiti TPdf i nikada ne predaje aktivni dokument drugoj dretvi. Za detaljniji prikaz te granice i ABI pravila oko nje, pogledajte članak o učvršćivanju PDFium VCL povezivanja protiv ABI pogrešaka i pogrešaka memorijske sigurnosti

Zašto su finalization i FPDF_DestroyLibrary važne

Suptilniji jaz nalazio se na putanji gašenja. PDFium jedinica (unit) nije imala odjeljak finalization, što je značilo da se FPDF_DestroyLibrary nikada nije pokrenuo pri izlasku iz procesa; operacijski sustav bi povratio memoriju DLL-a i preskočio PDFiumovo vlastito čišćenje, točnije spajanje radnih dretvi, odlaganje V8 izolata kada je EnableV8Engine postavljen, te uklanjanje fontova i predmemorije. Kod običnog iscrtavanja to je tiho curenje memorije pri gašenju, ali s omogućenim V8 pokretačem curi JavaScript izolat pri svakom pokretanju, što se brzo očituje pod opetovanom automatizacijom

Odjeljak finalization sada poziva UnloadLibrary, što pokreće FPDF_DestroyLibrary i oslobađa modul. To se nalazi na razini jedinice s razlogom: TPdf.Destroy samo postavlja Active := False kako bi zatvorio trenutni dokument i namjerno ne uklanja globalnu knjižnicu, tako da aplikacija s više instanci održava jedno zajedničko PDFium povezivanje aktivnim tijekom cijelog životnog vijeka procesa. Postavljanje uklanjanja u odjeljak finalization znači da se zajednička knjižnica oslobađa točno jednom, kada se jedinica ukloni iz memorije, a UnloadLibrary najprije provjerava svoju zastavicu Loaded kako bi poziv bio bezopasan ako se PDFium uopće nije koristio

Kratak kontrolni popis za implementaciju

Tri navike sprječavaju gotovo svaki neuspjeh učitavanja na terenu. Uskladite arhitekturu DLL-a s procesom domaćinom i isporučite ga pod DLLs/Win32 ili DLLs/Win64; držite pdfium.dll usklađenim s verzijom povezivanja kako nijedan potreban izvoz ne bi nedostajao; i nikada ne dijelite TPdf ili pokazivač dokumenta kroz dretve, iako je samo povezivanje sada atomarno. Ako gradite isti izvor za Delphi i Lazarus, razlike u pakiranju koje ometaju implementaciju na više platformi pokrivene su u bilješkama o zamkama među-prevoditelja za Delphi i FPC. Dijagnostika, brava učitavanja i završno čišćenje (finalization) opisani ovdje dolaze u sklopu komponente PDFium Component za Delphi i C++Builder