Tehnički članak

Distribucija PDFium DLL-a u Delphi-ju: Rešavanje problema sa učitavanjem

Kada se pdfium.dll ne učitava na klijentskoj mašini, PDFium komponenta za Delphi i Lazarus pretvara zagonetnu izvornu grešku u specifičan uzrok. Rutina CheckLoadLibrary čita ERROR_BAD_EXE_FORMAT (193) kao nesklad između 32-bitne i 64-bitne arhitekture, a CheckGetProcAddress prijavljuje nedostatak izvoznog simbola (export) jer je datoteka pdfium.dll starija od same komponente. Obe poruke ukazuju na rešenje umesto da vas ostavljaju da nagađate

To je važno jer izvorna DLL datoteka može da otkaže na tri različita načina, a sirovi tekst operativnog sistema ih sve spaja u jedno. Arhitektura može biti pogrešna, distribuirana binarna datoteka može biti prestara za Pascal vezu (binding) koja je poziva, ili dve niti mogu da se utrkuju ko će je povezati u istom trenutku. Svaki od ovih problema ima drugačije rešenje, a cela svrha dijagnostike dodate u verziji v2.11.0 u okviru ciklusa učitavanja jeste da vam kaže sa kojim se tačno problemom suočavate

Zašto učitavanje pdfium.dll ne uspeva uz grešku o lošem EXE formatu?

Windows greška 193, ERROR_BAD_EXE_FORMAT, znači da je datoteka pronađena i otvorena, ali njen PE tip mašine ne odgovara procesu domaćina (host process). 64-bitna izvršna datoteka ne može da učita 32-bitni pdfium.dll, a 32-bitna izvršna datoteka ne može da učita 64-bitni. Datoteka je prisutna, tako da vas instinkt da tražite nedostajući DLL šalje u potpuno pogrešnom smeru

PDFium komponenta presreće ovaj slučaj u metodi CheckLoadLibrary: kada LoadLibrary vrati null pokazivač (handle) i GetLastError bude 193, ona dodaje napomenu da je DLL pronađen ali da njegova arhitektura ne odgovara procesu domaćina, i ukazuje na odgovarajuću DLLs/Win32 ili DLLs/Win64 verziju. Poddirektorijum bira funkcija BuildDllSubDir, koja vraća Win64 kada je IsWin64 tačno, a u suprotnom Win32. Distribuirajte DLL u direktorijumu koji veza zapravo pretražuje i nesklad ć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;

Šta vam govori nedostajući izvozni simbol (export) u PDFium-u?

Druga klasa neuspeha je nesklad u verzijama. PDFium vremenom uvodi nove izvozne funkcije, a Pascal veza razrešava svaku funkciju koja joj je potrebna kroz CheckGetProcAddress tokom rada LoadLibrary. Ako potreban izvozni simbol nedostaje, veza je novija od datoteke pdfium.dll na disku, a iskrena dijagnoza je da je distribuirani DLL zastareo, a ne oštećen. Funkcija CheckGetProcAddress saopštava upravo to: podiže izuzetak EPdfError prijavljujući da je distribuirani pdfium.dll stariji od ove verzije PDFiumPas veze i navodi putanju DLLs/<subdir> gde pripada odgovarajuća binarna datoteka

Dva detalja čine ovu putanju robusnom. Prvo, CheckGetProcAddress poziva UnloadLibrary pre nego što podigne izuzetak, tako da delimično povezivanje nikada ne ostavlja polu-razrešene pokazivače funkcija da bi sledeći pokušaj naišao na njih. Drugo, naziv DLL-a koji prijavljuje dolazi iz funkcije BuildDllName, koja podrazumevano vraća pdfium.dll, a pdfium.v8.dll kada je postavljena globalna zastavica EnableV8Engine. Ako vaša aplikacija omogućava V8 JavaScript motor za XFA ili skriptovane obrasce, tekst greške ukazuje na V8 verziju, a ne na običnu, tako da odmah iz prvog puta zamenjujete pravu datoteku

Da li je PDFium DLL bezbedan za učitavanje iz pozadinske niti?

Učitavanje biblioteke je sada bezbedno iz bilo koje niti; pozivanje PDFium API-ja iz više niti i dalje nije. To su dve odvojene garancije, i njihovo razlikovanje čini razliku između stabilnog skupa radnih niti i povremenih rušenja programa. Pozadinsko renderovanje obično pokreće renderovanje svake stranice kroz radnu nit TPdfFuture<T>, a prva buduća radnja (future) koja dodirne TPdf je ona koja pokreće lenjo povezivanje (lazy bind). Bez zaštite, dva radnika bi mogla da uoče neučitanu biblioteku, oba bi pokrenula sekvencu povezivanja, prepisala pokazivač modula i dovela do curenja prvog učitavanja

Rad u verziji v2.11.0 zatvara taj prozor pomoću PDFiumLoadLock, kritične sekcije na nivou procesa (TRTLCriticalSection) koja obmotava ulaz u LoadLibrary and UnloadLibrary. Kritična sekcija čini sekvencu "proveri-pa-učitaj" atomskom, tako da nijedna od dve niti ne može istovremeno videti Loaded=False i izvršiti dvostruko povezivanje, niti ijedna nit može osloboditi DLL dok je druga usred povezivanja. Brava (lock) se kreira u sekciji initialization jedinice (unit) i uklanja u sekciji finalization, zaštićena zastavicom PDFiumLoadLockReady tako da je uparivanje bezbedno čak i tokom gašenja aplikacije. Ako vam je potreban kompletan šablon "radnik-i-odgovor" sa otkazivanjem, prateći članak o pozadinskom renderovanju sa mogućnošću otkazivanja futures-a prolazi 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;

Šta brava za učitavanje (load lock) ne štiti

Granicu ovde vredi jasno navesti, jer je lako pogrešno protumačiti ovo rešenje. PDFiumLoadLock serijalizuje samo globalno stanje veze: pokazivač modula i dodele pokazivača funkcija FPDF_*. Osnovni PDFium C API i dalje nije bezbedan za niti (thread-safe), baš kao što deklarišu zaglavlja izvornog projekta, tako da pozivanje FPDF_* funkcija ili deljenje jednog FPDF_DOCUMENT kroz niti ostaje vaša odgovornost za serijalizaciju. Bezbedan oblik je onaj gore: svaki radnik poseduje sopstveni TPdf i nikada ne predaje aktivni dokument drugoj niti. Za detaljniji tretman te granice i ABI pravila oko nje, pogledajte članak o učvršćivanju PDFium VCL veze protiv grešaka u ABI-ju i memoriji

Zašto su finalization i FPDF_DestroyLibrary važni

Suptilniji nedostatak se nalazio u putanji za gašenje. PDFium jedinica nije imala sekciju finalization, što je značilo da se FPDF_DestroyLibrary nikada nije pokretala pri izlazu iz procesa; operativni sistem bi povratio memoriju DLL-a i preskočio sopstveno čišćenje PDFium-a, konkretno spajanje radnih niti, uklanjanje V8 izolata kada je EnableV8Engine postavljeno, i gašenje fontova i keša. Na običnom zadatku renderovanja to je tiho curenje memorije pri gašenju, ali sa omogućenim V8 motorom to curi JavaScript izolat pri svakom pokretanju, što se brzo primećuje pod ponovljenom automatizacijom

Sekcija finalization sada poziva UnloadLibrary, koja pokreće FPDF_DestroyLibrary i oslobađa modul. Ovo živi na nivou jedinice (unit scope) sa razlogom: destruktor TPdf.Destroy samo postavlja Active := False da bi zatvorio trenutni dokument, i namerno ne učitava globalnu biblioteku, tako da aplikacija sa više instanci održava jednu zajedničku PDFium vezu tokom celog životnog veka procesa. Smeštanje gašenja u finalization znači da se zajednička biblioteka oslobađa tačno jednom, kada se jedinica ukloni iz memorije, a UnloadLibrary najpre proverava zastavicu Loaded, tako da je poziv bezopasan ako se PDFium uopšte nije koristio

Kratka lista za proveru distribucije

Tri navike sprečavaju skoro svaki neuspeh učitavanja na terenu. Uskladite arhitekturu DLL-a sa procesom domaćina i isporučite je pod DLLs/Win32 ili DLLs/Win64; držite pdfium.dll u koraku sa verzijom veze tako da nijedan potreban izvozni simbol ne nedostaje; i nikada ne delite TPdf ili pokazivač dokumenta kroz niti, iako je samo povezivanje sada atomsko. Ako gradite isti izvor za Delphi i Lazarus, razlike u pakovanju koje mogu otežati distribuciju na različite ciljeve pokrivene su u beleškama o zamkama unakrsnog kompajliranja između Delphi-ja i FPC-a. Dijagnostika, brava za učitavanje i čišćenje u finalizaciji opisani ovde se isporučuju u komponente PDFium Component za Delphi i C++Builder