Teknisk artikel

Udrulning af PDFium-DLL i Delphi: løs indlæsningsfejl

Når pdfium.dll nægter at indlæse på en kundemaskine, forvandler PDFium Component til Delphi og Lazarus den kryptiske native fejl til en konkret årsag. Rutinen CheckLoadLibrary læser ERROR_BAD_EXE_FORMAT (193) som et arkitekturmismatch mellem 32 og 64 bit, og CheckGetProcAddress melder en manglende eksport som en pdfium.dll, der er ældre end bindingen. Begge beskeder navngiver løsningen i stedet for at overlade dig til gætværk

Det betyder noget, fordi en native DLL fejler på tre forskellige måder, og operativsystemets rå tekst blander dem alle sammen. Arkitekturen kan være forkert, den udrullede binærfil kan være for gammel til den Pascal-binding, der kalder den, eller to tråde kan kappes om at binde den i samme øjeblik. Hver af dem har sin egen løsning, og hele pointen med den diagnostik, der kom med arbejdet omkring indlæsningslivscyklussen i v2.11.0, er at fortælle dig, hvilken af dem du rent faktisk står med

Diagram over de tre indlæsningsfejltilstande for pdfium.dll i Delphi med signal og løsning for hver, fra arkitekturmismatch med fejl 193 til en forældet DLL og en samtidig dobbeltbinding
En native DLL kan fejle på arkitektur, versionsforskydning eller en samtidig binding, og diagnostikken navngiver, hvilken af de tre der rent faktisk indtraf

Hvorfor fejler pdfium.dll med en fejl om ugyldigt EXE-format?

Windows-fejl 193, ERROR_BAD_EXE_FORMAT, betyder, at filen blev fundet og åbnet, men at dens PE-maskintype ikke matcher værtsprocessen. En 64-bit eksekverbar fil kan ikke indlæse en 32-bit pdfium.dll, og en 32-bit eksekverbar fil kan ikke indlæse en 64-bit. Filen er til stede, så instinktet om at lede efter en manglende DLL sender dig i præcis den forkerte retning

PDFium Component opfanger dette tilfælde i CheckLoadLibrary: når LoadLibrary returnerer et null-handle, og GetLastError er 193, føjer den et fingerpeg til om, at DLL-filen blev fundet, men at dens arkitektur ikke matcher værtsprocessen, og den peger på den tilsvarende DLLs/Win32- eller DLLs/Win64-build. Undermappen vælges af BuildDllSubDir, som returnerer Win64, når IsWin64 er true, og ellers Win32. Udrul DLL-filen under det træ, bindingen faktisk søger i, og mismatchet forsvinder. Én fælde sidder i kaldkoden frem for i udrulningen: TPdf.SetActive sluger enhver indlæsningsfejl, så Active := True efterlader komponenten inaktiv uden at rejse noget, og en except-blok omkring den kommer aldrig til at køre. For at se beskeden skal du kalde LoadDocument med en TPdfLoadOptions-record og en out TPdfLoadReport, som rejser den reelle EPdfError og noterer plsFailed med fejlteksten i rapporten. Siden PDFiumPas v3.122.1 erstatter en fejlet Active := True også LastLoadReport med en sådan fejlrapport, så kode, der bliver ved egenskabsvejen, kan læse årsagen fra LastLoadReport.ErrorMessage i stedet for at gætte

Diagram, der viser BuildDllSubDir vælge mappen med Win32- eller Win64-udgaven af pdfium.dll ud fra værtens bitbredde, så en Delphi-applikation undgår arkitekturmismatch med fejl 193
BuildDllSubDir vælger DLLs-mappen ud fra værtens bitbredde, og at levere den anden build viser sig som fejl 193 frem for en manglende fil
uses
  SysUtils, PDFium;

procedure OpenDocument(const AFileName: string);
var
  Pdf: TPdf;
  Report: TPdfLoadReport;
begin
  // Udrul pdfium.dll ved siden af den eksekverbare fil, matchet til build-målet:
  //   <AppDir>\DLLs\Win32\pdfium.dll   til en 32-bit værtsproces
  //   <AppDir>\DLLs\Win64\pdfium.dll   til en 64-bit værtsproces
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := AFileName;
    try
      // Active := True ville sluga fejlen og forblive False; denne
      // overload binder pdfium.dll dovent og rejser den reelle exception
      Pdf.LoadDocument(TPdfLoadOptions.Default(plmCompatible), Report);
    except
      on E: EPdfError do
        // Beskeden navngiver allerede den reelle årsag: et 32/64-bit-mismatch
        // (ERROR_BAD_EXE_FORMAT) eller en pdfium.dll ældre end denne binding;
        // Report.Status er plsFailed, og Report.ErrorMessage indeholder teksten
        raise Exception.CreateFmt('PDF engine did not start: %s', [E.Message]);
    end;
    RenderFirstPage(Pdf);   // Pdf.Active er True, når LoadDocument returnerer
  finally
    Pdf.Free;
  end;
end;

Hvad fortæller en manglende PDFium-eksport dig?

Den anden fejlklasse er versionsforskydning. PDFium leverer nye eksporter over tid, og Pascal-bindingen slår hver funktion, den har brug for, op gennem CheckGetProcAddress under LoadLibrary. Hvis en påkrævet eksport mangler, er bindingen nyere end den pdfium.dll, der ligger på disken, og den ærlige diagnose er, at den udrullede DLL er forældet snarere end beskadiget. Funktionen CheckGetProcAddress siger præcis det: den rejser en EPdfError, der melder, at den udrullede pdfium.dll er ældre end denne build af PDFiumPas-bindingen, og den navngiver stien DLLs/<subdir>, hvor en matchende binærfil hører hjemme

To detaljer gør den vej robust. For det første kalder CheckGetProcAddress UnloadLibrary, før den rejser fejlen, så en delvis binding aldrig efterlader halvt opløste funktionspegere, som næste forsøg kan snuble over. For det andet kommer det DLL-navn, den melder, fra BuildDllName, som normalt returnerer pdfium.dll og pdfium.v8.dll, når det globale flag EnableV8Engine er sat. Hvis din applikation aktiverer V8-JavaScript-motoren til XFA eller scriptede formularer, peger fejlteksten på V8-builden, ikke den almindelige, så du udskifter den rigtige fil i første forsøg

Er PDFium-DLL-filen sikker at indlæse fra en baggrundstråd?

At indlæse biblioteket er nu sikkert fra enhver tråd; at kalde PDFium-API'et fra flere tråde er det stadig ikke. Det er to adskilte garantier, og at holde dem adskilt er forskellen mellem en stabil arbejderpulje og et sporadisk nedbrud. Baggrundsgengivelse kører typisk hver sidegengivelse gennem en TPdfFuture<T>-arbejder, og den første future, der rører TPdf, er det, der udløser den dovne binding. Uden beskyttelse kunne to arbejdere begge observere et uindlæst bibliotek, begge køre bindingssekvensen, overskrive modulhandlet og lække den første indlæsning

Arbejdet i v2.11.0 lukker det vindue med PDFiumLoadLock, en procesglobal TRTLCriticalSection, der omslutter indgangen til både LoadLibrary og UnloadLibrary. Den kritiske sektion gør tjek-så-indlæs-sekvensen atomisk, så to tråde ikke begge kan se Loaded=False og dobbeltbinde, og ingen tråd kan frigive DLL-filen, mens en anden er midt i en binding. Låsen oprettes i unitens initialization-sektion og rives ned i finalization, bevogtet af et PDFiumLoadLockReady-flag, så parringen er sikker selv under nedlukning. Har du brug for det fulde arbejder-og-svar-mønster med annullering, gennemgår den ledsagende artikel om baggrundsgengivelse med annullerbare futures det fra ende til anden

Diagram over to TPdfFuture-arbejdertråde serialiseret af den kritiske sektion PDFiumLoadLock, så pdfium.dll bindes præcis én gang i en Delphi-applikation
PDFiumLoadLock gør tjek-så-indlæs-sekvensen atomisk, mens hver arbejder stadig ejer sin egen TPdf, fordi PDFium C-API'et ikke er trådsikkert
uses
  PDFium, FPdfAsync;

// Arbejdermetoden kører på en baggrundstråd. Den første future, der binder
// pdfium.dll, serialiseres af PDFiumLoadLock, så en anden samtidig
// arbejder ikke kan dobbeltbinde eller lække modulhandlet.
function TReportForm.RenderThumbnail(
  const AToken: IPdfCancellationToken): TBitmap;
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Active := True;         // sikker samtidig binding
    AToken.ThrowIfCancelled;
    Result := Pdf.Thumbnail;    // denne TPdf ejes kun af én tråd
  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;

Hvad indlæsningslåsen ikke beskytter

Grænsen her er værd at sige ligeud, for det er let at overtolke rettelsen. PDFiumLoadLock serialiserer kun den globale bindingstilstand: modulhandlet og tildelingerne af FPDF_*-funktionspegere. Det underliggende PDFium C-API er stadig ikke trådsikkert, præcis som opstrømsheaderne erklærer, så at kalde FPDF_*-funktioner eller dele ét FPDF_DOCUMENT på tværs af tråde er fortsat dit eget ansvar at serialisere. Den sikre form er den ovenfor: hver arbejder ejer sin egen TPdf og rækker aldrig et levende dokument videre til en anden tråd. For en dybere behandling af den grænse og ABI-reglerne omkring den, se hærdning af PDFium VCL-bindingen mod ABI- og hukommelsesfejl

Hvorfor finalization og FPDF_DestroyLibrary betyder noget

Et mere subtilt hul sad i nedlukningsstien. PDFium-uniten havde ingen finalization-sektion, hvilket betød, at FPDF_DestroyLibrary aldrig kørte ved procesafslutning; operativsystemet inddrog DLL-hukommelsen og sprang PDFiums egen oprydning over, nemlig sammenføjningen af arbejdertråde, bortskaffelsen af V8-isolatet når EnableV8Engine er sat, og nedrivningen af skrifttyper og cache. På en almindelig gengivelsesarbejdsbyrde er det en stille lækage ved nedrivning, men med V8-motoren aktiveret lækker den et JavaScript-isolat hver eneste kørsel, hvilket viser sig hurtigt under gentagen automatisering

finalization-sektionen kalder nu UnloadLibrary, som kalder FPDF_DestroyLibrary og frigiver modulet. Det ligger på unit-niveau af en grund: TPdf.Destroy sætter kun Active := False for at lukke det aktuelle dokument, og den aflæsser bevidst ikke det globale bibliotek, så en applikation med flere instanser holder én delt PDFium-binding i live i hele processens levetid. At lægge nedrivningen i finalization betyder, at det delte bibliotek frigives præcis én gang, når uniten aflæsses, og UnloadLibrary tjekker sit Loaded-flag først, så kaldet er en harmløs no-op, hvis PDFium aldrig blev brugt

En kort udrulningstjekliste

Tre vaner forhindrer stort set enhver indlæsningsfejl ude i marken. Match DLL-arkitekturen til værtsprocessen og lever den under DLLs/Win32 eller DLLs/Win64; hold pdfium.dll i trit med bindingens version, så ingen påkrævet eksport mangler; og del aldrig en TPdf eller et dokumenthandle på tværs af tråde, selv om selve bindingen nu er atomisk. Bygger du den samme kildekode til både Delphi og Lazarus, er de pakkeforskelle, der spænder ben for udrulning på tværs af mål, dækket i noterne om faldgruber ved krydskompilering med Delphi og FPC. Diagnostikken, indlæsningslåsen og finalization-oprydningen, der er beskrevet her, leveres alle i PDFium Component til Delphi og C++Builder