Teknisk artikel

Indlæsning af PDFium native-biblioteket på ethvert mål

PDFium-komponenten finder sit native-bibliotek gennem en fast, ordnet søgekæde i stedet for at overlade det til operativsystemets loader, for et udrulningstræ, der er eksplicit, er et udrulningstræ, du kan fejlfinde. På Windows kigger den kæde efter en Win32- eller Win64-undermappe, som installereren allerede udsender. På andre mål bygger den undermappens navn ud fra Free Pascal target-makroerne, som <cpu>-<os>, så udrulningstræet læses præcis som det kompilerede unit-træ. Den sidste beslutning indførte en fejl, der er hele artiklen værd, for årsagen var et versal, og symptomet var stilhed

Kæden, i rækkefølge

Fire placeringer, prøvet i sekvens, derefter platformloaderen som sidste udvej. Først det foretrukne layout, en DLLs-mappe ved siden af den eksekverbare med én undermappe pr. mål. Dernæst et alternativt layout med mål-undermappen direkte ved siden af den eksekverbare. Tredje det flade legacy-layout, biblioteket liggende ved siden af den eksekverbare uden nogen undermappe. Fjerde, kun på Windows, systemmappen, som behøver omhu, for en 32-bit-proces må kigge i SysWOW64 og en 64-bit-proces i System32, og på 32-bit Windows findes den førstnævnte ikke, så opslaget må falde tilbage. Først efter alt det bliver loaderen bedt om at søge på egen hånd

Diagram over PDFium native-bibliotekets søgekæde til Delphi, fra DLLs mål-undermappen gennem alternativt, fladt og Windows systemmappe-layout til platformloaderen
Fire eksplicitte placeringer afsøges i rækkefølge, før OS-loaderen bedes søge på egen hånd

Der er bevidst intet systemmappe-trin uden for Windows. Platformloaderens egen søgesti, drevet af runtime linker-konfigurationen og biblioteksstiens miljø, dækker allerede det område, og at duplikere det i Pascal ville betyde at re-implementere regler, der varierer efter distribution. Fejlfinding i Windows-kæden er dækket separat i udrulning af PDFium DLL og diagnosticering af indlæsningsfejl

Hvor undermappens navn kommer fra

På Windows er det Win32 eller Win64, afgjort af den kørende proces' bitvidde snarere end operativsystemets, for det er dét, der afgør, hvilken binær der kan indlæses. Alle andre steder bygges navnet ud fra compilerens target-makroer, så en maskine, der bygger til to arkitekturer, producerer to klart adskilte træer, og så mappen, der rummer det native bibliotek, ligger ved siden af mappen, der rummer de kompilerede units, med samme navn

function BuildDllSubDir(UseV8: Boolean): string;
begin
{$IFDEF MSWINDOWS}
  if IsWin64 then
    Result := 'Win64'
  else
    Result := 'Win32';
{$ELSE}
  // Compilermakroerne versaler OS'et ("Linux", "Darwin"), mens package
  // unit-outputmappen ikke gør, så de to er enige først efter folding.
  // På et filsystem, der skelner store og små bogstaver, er den forskel
  // hele opslaget
  Result := LowerCase({$I %FPCTARGETCPU%} + '-' + {$I %FPCTARGETOS%});
{$ENDIF}
end;

Hvorfor ét versal knækkede hele kæden

Compilermakroen staver mål-operativsystemet med begyndende versal: Win64, Linux, Darwin. Lazarus-pakken skriver sit unit-output ind i en mappe opkaldt efter sin egen target-variabel, som er med små bogstaver: win64, linux, darwin. To stavemåder af det samme, og ingen måde at lægge mærke til det på Windows, hvor filsystemet ikke skelner dem

På Linux er de to forskellige mapper. En udrulning, der lægger shared object i DLLs/x86_64-linux, er usynlig for en loader, der leder efter DLLs/x86_64-Linux, så alle kædens fire eksplicitte trin misser, og koden falder igennem til at lade platformloaderen søge. Nogle gange virker det, hvis biblioteket tilfældigvis er installeret systembredt, og nogle gange gør det ikke, og uanset hvad bidrager det omhyggeligt arrangerede udrulningstræ med ingenting. Fejlen har ingen fejlmeddelelse, for intet fejlede: hvert trin rapporterede korrekt, at filen ikke var der, hvor den kiggede

Hvordan ét versal i FPC target-makroen knækker PDFium DLL-opslaget på Linux: loaderen søger DLLs/x86_64-Linux, mens den udrullede mappe er DLLs/x86_64-linux, hvilket kun matcher på versal-ufølsomme Windows
Samme mål stavet på to måder matcher på Windows og misser i stilhed på et filsystem, der skelner store og små bogstaver

Probe-programmet, kompileret og kørt

Denne klasse af fejl kan ikke findes ved at læse, og den kan heller ikke findes ved at kompilere. Den sædvanlige teknik til at verificere en platformsgren, der aldrig kompilerer på udviklingsmaskinen, er at kopiere uniten til en midlertidig mappe, omdøbe den, erstatte den betingede platformskompilering med et symbol, der aldrig defineres, og kompilere kopien; kompilerer den, er uses-klausulen og kaldsignaturerne på den vej i det mindste selvkonsistente. Det virker fint for en selvkørende unit

Det virker ikke her. Hovedbindingsuniten er meget stor og trækker LCL ind, så den kan ikke blot kopieres og kompileres med Windows-symbolet slået fra. Så i stedet transskriberedes den håndfuld funktioner, ændringen rørte, ordret ind i et lille selvkørende program, og det program blev kørt. Det udskrev x86_64-Win64, og uoverensstemmelsen var synlig i én outputlinje. At kompilere samme program ville ikke have fortalt dig noget, for strengen er fuldstændig gyldig; kun dens værdi er forkert

program ProbeSubDir;
{$MODE DELPHI}
uses
  SysUtils;
begin
  // Udskriv, assert ikke. Pointen er at se på den værdi, en makro
  // reelt ekspanderer til på denne toolchain
  Writeln('raw:    ', {$I %FPCTARGETCPU%}, '-', {$I %FPCTARGETOS%});
  Writeln('folded: ', LowerCase({$I %FPCTARGETCPU%} + '-' +
    {$I %FPCTARGETOS%}));
end.

Den generelle lektie: når en cross-platform ændring angår en værdi snarere end en type, er compile-only-verifikation ikke verifikation. Udskriv den. Det bredere sæt af cross-compiler-forskelle mellem Delphi og Free Pascal er samlet i artiklen om Delphi- og FPC cross-compiler-fælder

Lad platformen forklare sine egne indlæsningsfejl

Loaderens Windows-gren enumeratorer i hånden de grunde, en indlæsning kan fejle, for de nyttige skel dér — et arkitekturmismatch, en manglende transitiv afhængighed, en sti, der ikke opløses — mapper til fejlkoder værd at navngive hver for sig. Uden for Windows returnerer den portable loader-unit allerede en beskrivende streng, der dækker samme område, så ikke-Windows-grenen bruger den direkte frem for at genaflede kategorier fra et fejlnummer, der betyder forskellige ting på forskellige systemer

At modstå trangen til at normalisere de to til én meddelelse er bevidst. En indlæsningsfejl er et udrulningsproblem, og personen, der læser meddelelsen, behøver platformens eget ordforråd for at søge efter den

En navnekollision, der rekurserer

Endnu en fælde, lille og skarp. Den portable loader-unit eksporterer en procedure kaldet UnloadLibrary, og bindingsuniten har en procedure af samme navn, der foretager sin egen bogføring, før handleen frigives. Inden i den procedure opløses et ukvalificeret kald af UnloadLibrary til den i den aktuelle unit, som kalder sig selv. Rettelsen er at kvalificere kaldet med unitnavnet

Dette er samme form som de identifikator-skyggeproblemer, der dominerer Free Pascal-porteringer generelt: Windows-uniten eksporterer integer-typede minimums- og maksimumfunktioner, der skygger de flydende punkt-versioner, og en synkroniseringstype, der skygger klassen af samme navn, og i alle tilfælde afhænger opløsningen af rækkefølgen af uses-klausulen. At kvalificere kaldstedet er rettelsen, der ikke afhænger af, at nogen bevarer den rækkefølge senere

UnloadLibrary-navnekollision i PDFium-komponenten: et ukvalificeret kald i Delphi-bidningsuniten rekurserer ind i sig selv, mens et unit-kvalificeret kald når den portable loader-unit og frigiver handleen
At kvalificere kaldstedet sender frigivelsen gennem loader-uniten i stedet for at rekurserer ind i bindingsuniten

Udrulningstjekliste

Tre ting står for de fleste indlæsningsfejl, når stiaritmetikken først er rigtig. Arkitekturen skal matche processen, ikke maskinen, så en 32-bit-applikation på 64-bit Windows behøver den 32-bit binære. V8-aktiveret build har et andet filnavn, så en udrulning, der blander dem, vil se korrekt ud og indlæse intet. Og kun én variant kan bo i en systemmappe ad gangen, hvilket er en god grund til at foretrække det eksplicitte undermappe-layout frem for at installere noget systembredt

For Lazarus specifikt: læg det native bibliotek under DLLs/<cpu>-<os> med små bogstaver, ved siden af den eksekverbare, så findes det af kædens første trin på ethvert mål. Viewer-eksemplet, der afprøver dette på Lazarus, er beskrevet i artiklen om Lazarus- og FPC-vieweren, og aktuel platformunderstøttelse er opført på produktsiden PDFium Delphi component