Teknik Makale

Delphi'de PDFium DLL Dağıtımı: Yükleme Hatalarını Giderme

Bir müşteri makinesinde pdfium.dll yüklenmeyi reddettiğinde, Delphi ve Lazarus için PDFium Bileşeni karmaşık yerel hatayı belirli bir nedene dönüştürür. CheckLoadLibrary rutini ERROR_BAD_EXE_FORMAT (193) hatasını 32/64-bit mimari uyuşmazlığı olarak okur ve CheckGetProcAddress eksik bir dışa aktarımı, bağlamadan daha eski bir pdfium.dll olarak raporlar. Her iki mesaj da sizi tahmin yürütmek zorunda bırakmak yerine çözümü adlandırır

Bu önemlidir çünkü yerel bir DLL üç farklı şekilde başarısız olur ve ham işletim sistemi metni bunların hepsini birbirine karıştırır. Mimari yanlış olabilir, dağıtılan ikili dosya onu çağıran Pascal bağlaması için çok eski olabilir veya iki iş parçacığı onu aynı anda bağlamak için yarışabilir. Her birinin farklı bir çözümü vardır ve v2.11.0 yükleme yaşam döngüsü çalışmasına eklenen teşhislerin amacı, aslında hangisine baktığınızı size söylemektir

pdfium.dll neden bozuk EXE biçimi hatasıyla yüklenemiyor?

Windows hatası 193, ERROR_BAD_EXE_FORMAT, dosyanın bulunduğunu ve açıldığını ancak PE makine türünün ana bilgisayar işlemiyle eşleşmediğini belirtir. A 64-bit bir yürütülebilir dosya 32-bit bir pdfium.dll yükleyemez ve 32-bit bir yürütülebilir dosya 64-bit bir dosyayı yükleyemez. Dosya mevcuttur, bu nedenle eksik bir DLL aramaya yönelik içgüdü sizi tam olarak yanlış yöne gönderir

PDFium Bileşeni bu durumu CheckLoadLibrary içinde yakalar: LoadLibrary null bir tanıtıcı (handle) döndürdüğünde ve GetLastError 193 olduğunda, DLL'in bulunduğuna ancak mimarisinin ana bilgisayar işlemiyle eşleşmediğine dair bir ipucu ekler ve eşleşen DLLs/Win32 veya DLLs/Win64 derlemesine işaret eder. Alt dizin, IsWin64 true olduğunda Win64, aksi takdirde Win32 döndüren BuildDllSubDir tarafından seçilir. DLL'i, bağlamanın gerçekten aradığı ağacın altına dağıtın; böylece uyuşmazlık ortadan kalkacaktır

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;

Eksik bir PDFium dışa aktarımı size ne anlatır?

İkinci başarısızlık sınıfı sürüm uyumsuzluğudur. PDFium zamanla yeni dışa aktarımlar sunar ve Pascal bağlaması, ihtiyaç duyduğu her işlevi LoadLibrary sırasında CheckGetProcAddress aracılığıyla çözümler. Gerekli bir dışa aktarım eksikse, bağlama diskteki pdfium.dll dosyasından daha yenidir ve dürüst teşhis, dağıtılan DLL'in bozuk olmaktan ziyade güncel olmadığıdır. CheckGetProcAddress işlevi tam olarak bunu belirtir: dağıtılan pdfium.dll dosyasının bu PDFiumPas bağlaması derlemesinden daha eski olduğunu bildiren bir EPdfError hatası oluşturur ve eşleşen bir ikili dosyanın ait olduğu DLLs/<subdir> yolunu adlandırır

İki ayrıntı bu yolu sağlamlaştırır. İlk olarak, CheckGetProcAddress hata vermeden önce UnloadLibrary yöntemini çağırır, böylece kısmi bir bağlama, bir sonraki denemenin takılması için yarım çözümlenmiş işlev işaretçileri bırakmaz. İkinci olarak, rapor ettiği DLL adı, normalde pdfium.dll ve genel EnableV8Engine bayrağı ayarlandığında pdfium.v8.dll döndüren BuildDllName işlevinden gelir. Uygulamanız XFA veya komut dosyası yazılmış formlar için V8 JavaScript motorunu etkinleştiriyorsa, hata metni düz olanı değil V8 derlemesini işaret eder; böylece ilk seferinde doğru dosyayı değiştirirsiniz

PDFium DLL dosyasının arka plan iş parçacığından yüklenmesi güvenli midir?

Kütüphaneyi yüklemek artık herhangi bir iş parçacığından güvenlidir; ancak PDFium API'sini birkaç iş parçacığından çağırmak hala güvenli değildir. Bunlar iki ayrı güvencedir ve onları ayrı tutmak, kararlı bir çalışan havuzu ile aralıklı bir çökme arasındaki farktır. Arka plan işlemesi genellikle her sayfa işlemeyi bir TPdfFuture<T> çalışanı aracılığıyla yürütür ve TPdf nesnesine dokunan ilk future gecikmeli bağlamayı tetikleyen şeydir. Koruma olmadan, iki çalışan da yüklenmemiş bir kütüphane gözlemleyebilir, ikisi de bağlama dizisini çalıştırabilir, modül tanıtıcısının üzerine yazabilir ve ilk yüklemeyi sızdırabilirdi

v2.11.0 çalışması, hem LoadLibrary hem de UnloadLibrary girişlerini saran işlem genelinde bir TRTLCriticalSection olan PDFiumLoadLock ile bu pencereyi kapatır. Kritik bölüm, kontrol et ve yükle dizisini atomik hale getirir; böylece hiçbir iki iş parçacığı da Loaded=False durumunu görüp çift bağlama yapamaz ve bir iş parçacığı bağlama ortasındayken diğeri DLL'i serbest bırakamaz. Kilit, birim initialization bölümünde oluşturulur ve finalization bölümünde parçalanır, bir PDFiumLoadLockReady bayrağı ile korunur, böylece eşleştirme kapatma sırasında bile güvenlidir. İptal etme özellikli tam çalışan ve yanıt şablonuna ihtiyacınız varsa, iptal edilebilir future'lar ile arka plan işleme hakkındaki yardımcı makale bunu uçtan uca anlatmaktadır

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;

Yükleme kilidinin korumadığı şeyler

Buradaki sınırın açıkça belirtilmesinde fayda vardır, çünkü çözümü aşırı yorumlamak kolaydır. PDFiumLoadLock yalnızca genel bağlama durumunu serileştirir: modül tanıtıcısı ve FPDF_* işlev işaretçisi atamaları. Temeldeki PDFium C API'si, yukarı akış başlıklarının beyan ettiği gibi hala iş parçacığı güvenli değildir; bu nedenle FPDF_* işlevlerini çağırmak veya iş parçacıkları arasında tek bir FPDF_DOCUMENT paylaşmak, serileştirmek sizin sorumluluğunuzda kalmaya devam eder. Güvenli şekil yukarıdaki gibidir: her çalışan kendi TPdf nesnesine sahiptir ve çalışan bir belgeyi asla başka bir iş parçacığına vermez. Bu sınırın ve etrafındaki ABI kurallarının daha derin bir analizi için, PDFium VCL bağlamasını ABI ve bellek hatalarına karşı sağlamlaştırma makalesine bakın

finalization ve FPDF_DestroyLibrary neden önemlidir?

Kapatma yolunda daha ince bir boşluk vardı. PDFium biriminin finalization bölümü yoktu, bu da işlem çıkışında FPDF_DestroyLibrary işlevinin asla çalışmadığı anlamına geliyordu; işletim sistemi DLL belleğini geri alıyor ve PDFium'un kendi temizliğini, yani çalışan iş parçacığı birleşimini, EnableV8Engine ayarlandığında V8 yalıtılmış alan (isolate) tasfiyesini ve yazı tipi ile önbellek parçalanmasını atlıyordu. Düz bir işleme iş yükünde bu, parçalama sırasında sessiz bir sızıntıdır; ancak V8 motoru etkinken her çalıştırmada bir JavaScript yalıtılmış alanını sızdırır ve bu durum tekrarlanan otomasyon altında hızla kendini gösterir

finalization bölümü artık UnloadLibrary yöntemini çağırır, bu da FPDF_DestroyLibrary işlevini çağırır ve modülü serbest bırakır. Bu durum birim kapsamında yaşar ve bunun bir nedeni vardır: TPdf.Destroy geçerli belgeyi kapatmak için yalnızca Active := False ayarlar ve genel kütüphaneyi kasıtlı olarak boşaltmaz; böylece çok örnekli bir uygulama, tüm işlem ömrü boyunca paylaşılan bir PDFium bağlamasını hayatta tutar. Parçalamayı finalization bölümüne koymak, paylaşılan kütüphanenin birim boşaltıldığında tam olarak bir kez serbest bırakılması anlamına gelir ve UnloadLibrary önce Loaded bayrağını kontrol eder; böylece PDFium hiç kullanılmamışsa çağrı zararsız bir boş işlemdir (no-op)

Kısa bir dağıtım kontrol listesi

Sahadaki neredeyse her yükleme hatasını üç alışkanlık önler. DLL mimarisini ana bilgisayar işlemiyle eşleştirin og bunu DLLs/Win32 veya DLLs/Win64 altında gönderin; pdfium.dll dosyasını bağlama sürümüyle uyumlu tutun, böylece hiçbir gerekli dışa aktarım eksik kalmaz; ve bağlamanın kendisi artık atomik olsa bile, bir TPdf veya belge tanıtıcısını asla iş parçacıkları arasında paylaşmayın. Aynı kaynak kodunu hem Delphi hem de Lazarus için derliyorsanız, hedefler arası dağıtımı engelleyen paketleme farkları Delphi ve FPC çapraz derleyici tuzakları notlarında ele alınmıştır. Burada açıklanan teşhisler, yükleme kilidi ve sonlandırma temizliğinin tümü, Delphi ve C++Builder için PDFium Bileşeni ile birlikte gelir