Teknisk artikkel

Distribusjon av PDFium DLL i Delphi: Retting av lastingsfeil

Når pdfium.dll nekter å laste på en kundemaskin, gjør PDFium-komponenten for Delphi og Lazarus den kryptiske innfødte feilen om til en spesifikk årsak. Rutinen CheckLoadLibrary leser ERROR_BAD_EXE_FORMAT (193) som en 32/64-biters arkitekturkonflikt, og CheckGetProcAddress rapporterer en manglende eksport som en pdfium.dll som er eldre enn bindingen. Begge meldingene navngir løsningen i stedet for å la deg gjette

Dette har betydning fordi en innfødt DLL kan feile på tre forskjellige måter, og den rå operativsystemteksten slår dem alle sammen. Arkitekturen kan være feil, den distribuerte binærfilen kan være for gammel for Pascal-bindingen som kaller den, eller to tråder kan kjempe om å binde den på samme tidspunkt. Hver av dem har en egen løsning, og hele poenget med diagnostikken som ble lagt til i v2.11.0-lastingslivssyklusen, er å fortelle deg hvilken du faktisk ser på

Hvorfor feiler lasting av pdfium.dll med en ugyldig EXE-formatfeil?

Windows-feil 193, ERROR_BAD_EXE_FORMAT, betyr at filen ble funnet og åpnet, men dens PE-maskintype stemmer ikke overens med vertsprosessen. En 64-biters kjørbar fil kan ikke laste en 32-biters pdfium.dll, og en 32-biters kjørbar fil kan ikke laste en 64-biters. Filen er til stede, så instinktet til å lete etter en manglende DLL sender deg i nøyaktig feil retning

PDFium-komponenten fanger opp dette tilfellet i CheckLoadLibrary: når LoadLibrary returnerer et nullhåndtak og GetLastError is 193, legger den til et hint om at DLL-en ble funnet, men at arkitekturen ikke samsvarer med vertsprosessen, og den peker på den samsvarende DLLs/Win32- eller DLLs/Win64-versjonen. Underkatalogen velges av BuildDllSubDir, som returnerer Win64 når IsWin64 er sann, og Win32 ellers. Distribuer DLL-en under treet som bindingen faktisk søker i, så forsvinner misforholdet

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;

Hva forteller en manglende PDFium-eksport deg?

Den andre feilkategorien er versjonsavvik. PDFium leverer nye eksporter over tid, og Pascal-bindingen løser hver funksjon den trenger gjennom CheckGetProcAddress under LoadLibrary. Hvis en nødvendig eksport mangler, er bindingen nyere enn pdfium.dll på disken, og den ærlige diagnosen er at den distribuerte DLL-en er utdatert snarere enn korrupt. Funksjonen CheckGetProcAddress oppgir nøyaktig dette: den utløser en EPdfError som rapporterer at den distribuerte pdfium.dll er eldre enn denne versjonen av PDFiumPas-bindingen, og den navngir DLLs/<subdir>-banen der en samsvarende binærfil hører hjemme

To detaljer gjør denne banen robust. For det første kaller CheckGetProcAddress UnloadLibrary før den kaster feilen, slik at en delvis binding aldri etterlater halvveis løste funksjonspekere som neste forsøk kan snuble i. For det andre kommer DLL-navnet den rapporterer fra BuildDllName, som normalt returnerer pdfium.dll, og pdfium.v8.dll når det globale flagget EnableV8Engine er satt. Hvis applikasjonen din aktiverer V8 JavaScript-motoren for XFA eller skriptede skjemaer, peker feilteksten på V8-versjonen, ikke den vanlige, slik at du erstatter riktig fil på første forsøk

Er PDFium DLL-en trygg å laste fra en bakgrunnstråd?

Lasting av biblioteket er nå trygt fra enhver tråd; å kalle PDFium-API-et fra flere tråder er fremdeles ikke det. Dette er to separate garantier, og å holde dem adskilt er forskjellen mellom en stabil arbeidstrådpool og periodiske krasj. Bakgrunnsrendring kjører vanligvis hver siderendring gjennom en TPdfFuture<T>-arbeider, og den første fremtiden som berører TPdf er det som utløser den forsinkede bindingen. Uten beskyttelse kunne to arbeidere begge observere et ulastet bibliotek, begge kjøre bindingssekvensen, overskrive modulhåndtaket og lekke den første lastingen

Arbeidet i v2.11.0 lukker det vinduet med PDFiumLoadLock, en prosessglobal TRTLCriticalSection som omslutter inngangen to både LoadLibrary og UnloadLibrary. Den kritiske seksjonen gjør sjekk-og-deretter-last-sekvensen atomær, slik at ingen tråder kan se Loaded=False og dobbeltbinde samtidig, og ingen tråd kan frigjøre DLL-en mens en annen er midt i bindingen. Låsen opprettes i enhetens initialization-seksjon og avvikles i finalization, beskyttet av et PDFiumLoadLockReady-flagg slik at paringen er trygg selv under nedstengning. Hvis du trenger det fulle mønsteret med arbeider-og-svar med kansellering, går den tilhørende artikkelen om bakgrunnsrendring med kansellerbare futures gjennom det fra start til slutt

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;

Hva lastelåsen ikke beskytter mot

Grensene her er verdt å slå fast tydelig, fordi det er lett å feiltolke løsningen. PDFiumLoadLock serialiserer bare den globale bindingstilstanden: modulhåndtaket og FPDF_*-funksjonspeker-tilordningene. Det underliggende PDFium C-API-et er fremdeles ikke trådsikkert, akkurat som oppstrømshodene erklærer, så det å kalle FPDF_*-funksjoner eller dele et enkelt FPDF_DOCUMENT på tvers av tråder forblir ditt ansvar å serialisere. Den trygge formen er den ovenfor: hver arbeider eier sin egen TPdf og overleverer aldri et aktivt dokument til en annen tråd. For en dypere behandling av den grensen og ABI-reglene rundt den, se herding av PDFium VCL-bindingen mot ABI- og minnefeil

Hvorfor finalization og FPDF_DestroyLibrary betyr noe

Et mer subtilt gap fantes i nedstengningsbanen. PDFium-enheten hadde ingen finalization-seksjon, noe som betydde at FPDF_DestroyLibrary aldri ble kjørt ved prosessavslutning; operativsystemet gjenvant DLL-minnet og hoppet over PDFiums egen opprydding, nemlig avslutning av arbeidstråder, frigjøring av V8-isolater når EnableV8Engine er satt, og nedstengning av skrifttyper og hurtigbuffer. På en vanlig rendringsarbeidsmengde er dette en stille lekkasje ved avvikling, men med V8-motoren aktivert lekker den et JavaScript-isolat for hver kjøring, noe som merkes raskt under gjentatt automatisering

Seksjonen finalization kaller nå UnloadLibrary, som kaller FPDF_DestroyLibrary og frigjør modulen. Dette lever på enhetsnivå av en grunn: TPdf.Destroy setter bare Active := False for å lukke det gjeldende dokumentet, og den unnlater bevisst å avlaste det globale biblioteket, slik at en applikasjon med flere forekomster holder en felles PDFium-binding i live under hele prosessens levetid. Å plassere avviklingen i finalization betyr at det delte biblioteket frigjøres nøyaktig én gang, når enheten avlastes, og UnloadLibrary sjekker sitt Loaded-flagg først, slik at kallet er en ufarlig tom operasjon hvis PDFium aldri ble brukt

En kort sjekkliste for distribusjon

Tre vaner forhindrer nesten alle lastingsfeil i feltet. Tilpass DLL-arkitekturen to vertsprosessen og lever den under DLLs/Win32 eller DLLs/Win64; hold pdfium.dll i samsvar med bindingsversjonen slik at ingen nødvendig eksport mangler; og del aldri en TPdf eller et dokumenthåndtak på tvers av tråder, selv om selve bindingen nå er atomær. Hvis du bygger samme kildekode for både Delphi og Lazarus, er emballasjeforskjellene som skaper problemer for distribusjon på tvers av målplattformer, dekket i notatene om Delphi og FPC kryssprogrammeringsfallgruver. Diagnostikken, lastelåsen og finalization-oppryddingen som er beskrevet her, leveres alle i PDFium Component for Delphi og C++Builder