Техническа статия

Внедряване на PDFium DLL в Delphi: Отстраняване на грешки при зареждане

Когато pdfium.dll откаже да се зареди на клиентска машина, компонентът PDFium за Delphi и Lazarus превръща неразбираемата нативна грешка в конкретна причина. Подпрограмата CheckLoadLibrary разчита ERROR_BAD_EXE_FORMAT (193) като несъответствие между 32/64-битовите архитектури, а CheckGetProcAddress докладва липсваща експортирана функция, указвайки че pdfium.dll е по-стара от свързващата библиотека. И двете съобщения посочват решението, вместо да ви оставят да гадаете

Това е важно, тъй като нативната DLL библиотека може да се провали по три различни начина, а необработеният текст от операционната система ги смесва в едно. Архитектурата може да е грешна, внедреният двоичен файл може да е твърде стар за Pascal свързването, което го извиква, или две нишки могат да се състезават за свързването му в един и същ момент. Всяко състояние има различно решение, и целта на диагностиката, добавена при оптимизацията на жизнения цикъл на зареждане във v2.11.0, е да ви каже точно какво виждате

Защо pdfium.dll не се зарежда с грешка за лош EXE формат?

Грешката в Windows 193, ERROR_BAD_EXE_FORMAT, означава, че файлът е намерен и отворен, но неговият тип PE (Portable Executable) архитектура не съвпада с хост процеса. 64-битов изпълним файл не може да зареди 32-битова библиотека pdfium.dll, а 32-битов изпълним файл не може да зареди 64-битова. Файлът присъства, така че инстинктът да търсите липсваща DLL библиотека ще ви насочи в напълно грешна посока

Компонентът PDFium прихваща този случай в CheckLoadLibrary: когато LoadLibrary върне нулев манипулатор и GetLastError е 193, то добавя подсказка, че библиотеката е намерена, но архитектурата ѝ не съответства на хост процеса, и препраща към съответната версия в DLLs/Win32 или DLLs/Win64. Поддиректорията се избира от BuildDllSubDir, което връща Win64, когато IsWin64 е True, и Win32 в противен случай. Внедрете DLL библиотеката в дървото от директории, което свързващият код реално търси, и несъответствието ще изчезне

uses
  SysUtils, PDFium;

procedure OpenDocument(const AFileName: string);
var
  Pdf: TPdf;
begin
  // Внедрете pdfium.dll до изпълнимия файл, съобразен с целта на компилацията:
  //   <AppDir>\DLLs\Win32\pdfium.dll   за 32-битов хост процес
  //   <AppDir>\DLLs\Win64\pdfium.dll   за 64-битов хост процес
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := AFileName;
    try
      Pdf.Active := True;   // първото използване латентно (мързеливо) свързва pdfium.dll
    except
      on E: EPdfError do
        // Съобщението вече посочва реалната причина: несъответствие между 32/64-битови версии
        // (ERROR_BAD_EXE_FORMAT) или pdfium.dll, която е по-стара от това свързване
        raise Exception.CreateFmt('PDF engine did not start: %s', [E.Message]);
    end;
    if Pdf.Active then
      RenderFirstPage(Pdf);
  finally
    Pdf.Free;
  end;
end;

Какво ви казва липсваща експортирана функция в PDFium?

Вторият клас повреди се дължи на несъответствие във версиите. PDFium добавя нови експортирани функции с течение на времето, а свързването на Pascal намира всяка функция, от която се нуждае, чрез CheckGetProcAddress по време на LoadLibrary. Ако изисквана функция липсва, свързващият код е по-нов от библиотеката pdfium.dll на диска, и реалната диагноза е, че внедрената DLL библиотека е остаряла, а не повредена. Функцията CheckGetProcAddress съобщава точно това: тя задейства EPdfError, указваща че внедрената pdfium.dll е по-стара от тази версия на свързването PDFiumPas, и посочва пътя DLLs/<subdir>, където трябва да се намира съвпадащият файл

Две подробности правят този път стабилен. Първо, CheckGetProcAddress извиква UnloadLibrary преди да задейства изключението, така че частично свързване никога не оставя наполовина разрешени указатели към функции, в които следващият опит би се спънал. Второ, името на DLL файла, което се докладва, идва от BuildDllName, което връща pdfium.dll при нормални обстоятелства и pdfium.v8.dll, когато е зададен глобалният флаг EnableV8Engine. Ако приложението ви активира JavaScript енджина V8 за XFA или скриптирани формуляри, текстът на грешката ще сочи към V8 компилацията, а не към обикновената, така че да подмените правилния файл още от първия път

Безопасно ли е зареждането на PDFium DLL от фонова нишка?

Зареждането на библиотеката вече е безопасно от всяка нишка; извикването на API-то на PDFium от няколко нишки едновременно обаче все още не е. Това са две отделни гаранции и разграничаването им определя дали ще имате стабилен пул от работни нишки или случайни сривове. Фоновото рендериране обикновено изпълнява всяко рендериране на страница чрез работна нишка TPdfFuture<T>, и първият future обект, който докосне TPdf, задейства латентното свързване. Без защита, два работника биха могли да видят незаредена библиотека, и двата да стартират процеса на свързване, да презапишат манипулатора на модула и да изтече памет от първото зареждане

Оптимизациите във v2.11.0 затварят тази пролука с PDFiumLoadLock — глобална за процеса критична секция TRTLCriticalSection, която обвива входовете към LoadLibrary и UnloadLibrary. Критичната секция прави последователността от проверка и зареждане атомна, така че да няма две нишки, които едновременно да видят Loaded=False и да направят двойно свързване, както и нито една нишка да не може да освободи библиотеката, докато друга е в процес на свързване. Заключването се създава в секцията initialization на модула и се премахва в finalization, защитено от флаг PDFiumLoadLockReady, така че сдвояването да е безопасно даже при спиране на програмата. Ако се нуждаете от пълен модел с работни нишки, отговори и отказване, придружаващата статия за фоново рендериране с отменяеми future обекти описва това от край до край

uses
  PDFium, FPdfAsync;

// Работният метод се изпълнява във фонова нишка. Първият future обект, който се свърже
// с pdfium.dll, се сериализира от PDFiumLoadLock, така че втори едновременен
// работник не може да направи двойно свързване или да изпусне манипулатора на модула.
function TReportForm.RenderThumbnail(
  const AToken: IPdfCancellationToken): TBitmap;
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Active := True;         // безопасно едновременно свързване
    AToken.ThrowIfCancelled;
    Result := Pdf.Thumbnail;    // този обект TPdf е притежание само на една нишка
  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;

Какво не защитава заключването при зареждане (load lock)

Границата тук трябва да бъде ясно назована, тъй като лесно може да се преувеличи ефектът от тази корекция. PDFiumLoadLock сериализира единствено глобалното състояние на свързване: манипулатора на модула и присвояванията на указателите към функции FPDF_*. Базовото C API на PDFium все още не е нишково безопасно, точно както декларират оригиналните заглавни файлове, така че извикването на FPDF_* функции или споделянето на един FPDF_DOCUMENT между нишки остава ваша отговорност за сериализиране. Безопасната структура е показаната по-горе: всеки работник притежава собствен TPdf обект и никога не предава работещ документ на друга нишка. За по-подробен анализ на тази граница и правилата за ABI около нея, вижте статията за защита на свързването на PDFium VCL срещу ABI и паметови дефекти

Защо finalization и FPDF_DestroyLibrary са важни

По-незабележима пролука имаше в процеса на спиране. Модулът PDFium нямаше секция finalization, което означаваше, че FPDF_DestroyLibrary никога не се изпълняваше при изход от процеса; операционната система си връщаше паметта на DLL библиотеката и пропускаше почистването на самия PDFium — а именно изчакването на работните нишки, освобождаването на V8 изолатите (когато е зададено EnableV8Engine) и премахването на шрифтовете и кеша. При обикновена задача за рендериране това е тихо изтичане на памет при затваряне, но с активиран V8 енджин се изпуска JavaScript изолат при всяко изпълнение, което бързо се проявява при многократно автоматизирано изпълнение

Секцията finalization сега извиква UnloadLibrary, което задейства FPDF_DestroyLibrary и освобождава модула. Това се намира на ниво модул с причина: TPdf.Destroy само задава Active := False, за да затвори текущия документ, и умишлено не изтегля глобалната библиотека от паметта, така че приложение с множество инстанции поддържа едно споделено свързване на PDFium живо за целия жизнен цикъл на процеса. Поставянето на почистването във finalization означава, че споделената библиотека се освобождава точно веднъж, когато модулът се изтегли, а UnloadLibrary първо проверява флага Loaded, така че извикването е безвредно и без допълнителни действия, ако PDFium изобщо не е бил използван

Кратък контролен списък за внедряване

Три навика предотвратяват почти всеки проблем със зареждането на място. Съобразете архитектурата на DLL с хост процеса и я доставете в DLLs/Win32 или DLLs/Win64; поддържайте версията на pdfium.dll в съответствие със свързващия код, за да не липсват необходими функции; и никога не споделяйте TPdf или манипулатор на документ между нишки, въпреки че самото свързване вече е атомно. Ако компилирате един и същ сорс код за Delphi и Lazarus, разликите в пакетирането, които могат да объркат внедряването между платформи, са разгледани в бележките за Delphi и FPC капани при крос-компилация. Диагностиката, заключването при зареждане и почистването в края, описани тук, се доставят в PDFium Component за Delphi и C++Builder