PDFlibPas 3.538.0 linker den eksterne JBIG2-encoder statisk ind i Free Pascal- og Lazarus-programmer. Et projekt tilføjer enheden PDFlibJBIG2EncC, den samme enhed som Delphi og C++Builder allerede bruger, og encoderen ender inde i den eksekverbare fil uden noget ekstra at levere sammen med den. Det ændrer den tidligere konklusion om denne funktion, nemlig at Free Pascal kun kunne nå den eksterne encoder gennem en DLL
Hvorfor virkede DLL som den eneste mulighed?
DLL virkede som den eneste mulighed, fordi tre linkningsveje fejlede på tre uafhængige måder, og ingen compiler-switch nåede frem til nogen af dem. Den interne linker afviser associative COMDAT-sektioner direkte. Et eksternt link gennem de medfølgende binutils crasher under garbage collection af sektioner, som Free Pascal ubetinget aktiverer på 64-bit Windows-målet. Nyere binutils kan slet ikke behandle Free Pascals linkscript. Hvis C++-siden bygges med den anden toolchain, byttes én afvisning ud med en anden, fordi template- og inline-instansiering af natur udsender svage eksterne symboler, og Free Pascal rapporterer dem som Unsupported COFF symbol type 105. Ingen af de observationer var forkerte, og den tidligere gennemgang af JBIG2-encoder-backends og Free Pascal-linkeren gennemgår stadig hver blindgyde på en måde, der kan reproduceres i dag. Det forkerte var antagelsen om, hvor en løsning kunne ligge. Alle forsøg gik gennem en compiler eller en linker, og ingen af dem kan ændre det, en objektfil allerede indeholder. Objektfilen var problemet hele tiden. ObjConv læser COFF og skriver COFF, og hver konstruktion, som Free Pascal snubler over, har en mekanisk ækvivalent, det accepterer
Fejlen, der aldrig nævner sin årsag
Free Pascals interne linker implementerer kun pick-any COMDAT halvt, og den halvfærdige implementering er det sværeste at diagnosticere her. Den folder ganske vist duplikerede definitioner sammen, som formatet kræver. Men TExeOutput.RemoveUnreferencedSections går via exesymbol til vinderdefinitionen, når sektioner markeres som brugte, mens TCoffexeoutput.DoRelocationFixup læser objreloc.symbol.objsection direkte. Når en brugt sektion refererer til et symbol, som dens eget objekt definerer i en kopi, der tabte foldningen, ser de to passager på forskellige sektioner, og linkningen stopper med Internal error 200603061
Sammenlign det med de to begrænsninger på hver sin side af fejlen. Unsupported COFF symbol type 105 siger weak external. Associative or exact match COMDAT sections are not yet supported siger associative COMDAT og nævner endda det krænkende symbol. Internal error 200603061 siger ingenting: intet symbolnavn, intet sektionsnavn, intet filnavn, ingen fase. Det er også normaltilfældet snarere end et hjørnetilfælde, fordi MSVC placerer hver string literal og hver inline- eller template-instansiering i en pick-any COMDAT, og på tværs af de 186 objekter i dette encodersæt udførte linkeren 2656 foldninger. Bygning med /Gy- holder almindelige funktioner ude af COMDAT-sektioner pr. funktion, men lader string literals og template-instansieringer ligge præcis, hvor de var
Hvorfor ser det altid ud, som om den sidste CRT-stub ødelagde det?
Fordi linkeren først når frem til fixup-fasen, når hvert symbol er opløst. Så længe noget stadig mangler, stopper kørslen tidligt med Undefined symbol, og COMDAT-problemet får aldrig mulighed for at vise sig. Udfyld den sidste C-runtime-stub, og linkeren går én fase videre, direkte ind i Internal error 200603061. Det synlige symptom er derfor systematisk misvisende: Når man tilføjer Pascal-kroppe for de refererede C-symboler ét ad gangen, ser det altid ud, som om den seneste tilføjelse ødelagde buildet, eller som om en tærskel omkring hundrede stubs blev overskredet. Ingen af delene er rigtige. Hvilket symbol der blev tilføjet sidst, og hvor mange der blev tilføjet i alt, er begge irrelevante, fordi fejlen lå latent i det første objekt og først kunne nås, da opløsningen lykkedes. Når en linker ændrer sin fejlbesked, efter at du har rettet noget tilsyneladende uvedkommende, så spørg, om du avancerede en fase i stedet for at skabe en regression
Rettelsen er én ObjConv-kørsel, ikke et compiler-flag
Hele rettelsen er én efterbehandlingskommando, der køres på hvert kompileret objekt: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. Tre af disse flag blev tilføjet til arbejdet. -xw opløser symboler med IMAGE_SYM_CLASS_WEAK_EXTERNAL til almindelige eksterne symboler. -xn normaliserer symboler med IMAGE_SYM_CLASS_NULL, såsom _fltused, som Free Pascal rapporterer som Unsupported COFF symbol type 0. -xc udfører det tunge arbejde: Det nedgraderer hver COMDAT-sektion til en almindelig sektion og gør de symboler, den definerer, statiske. Fejlen fjernes ved at fjerne beslutningen, for når der ikke findes COMDAT-sektioner, er der ingen foldning, ingen vinderkopi som den ene fase kan omdirigere til, mens den anden overser den, og de associative .pdata- og .xdata-unwind-sektioner forsvinder sammen med den. Prisen er reel, men lille, fordi kopier, der legitimt kunne være slået sammen, nu hver især overlever
Præfikset -np:__imp_:pdflibimp_ løser en separat kollision. MSVC kalder importerede Win32-APIer gennem indirekte celler med navne som __imp_*, Free Pascal reserverer dette præfiks til sin egen importmekanisme, og hvis et af navnene defineres direkte, udløses den samme Internal error 200603061. Når cellerne omdøbes, kan Pascal-siden eksportere dem som almindelige variabler og udfylde dem ved kørsel. Selve objekterne kompileres med static-link-flagene /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c- og med image-codecs slået fra, så de døde fil-I/O- og codec-stier kræver langt færre link-only-stubs. De placeres i Lib\thirdparty\Win64f, mens Delphi- og C++Builder-vejen fortsat linker sit eget Win64x-sæt urørt, hvilket er det rigtige resultat for en portabilitetsrettelse, der er begrænset til én toolchain
Hvad Pascal-siden stadig skal eksportere
Free Pascal opløser en C-objekts import via symbolnavnet og har derfor brug for, at navnet skrives eksplicit, så hver Pascal-rutine, der står i stedet for et C-entry point, har en public name-klausul. Delphi bruger rutinenavnet som symbolnavn og behøver ingen klausul, hvilket er grunden til, at én unit kan tjene begge compilere med klausulerne under {$IFDEF FPC}. Fælden er, at en external 'msvcrt.dll'-deklaration ikke tilfredsstiller noget: Den opretter en import, aldrig en definition som et linket objekt kan binde til. Forwarding-kroppen skal eksistere
// En ekstern deklaration opretter kun en import. Intet linket objekt kan
// binde til den.
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
external 'msvcrt.dll' name 'memcmp';
// En Pascal-krop, der publiceres under det nøjagtige C-symbolnavn, er det,
// som objektsættet faktisk binder til.
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
public name 'memcmp';
begin
Result := crt_memcmp(Buf1, Buf2, Count);
end;
Variadiske entry points bryder dette mønster, fordi en Pascal-wrapper ikke kan videresende sine egne varargs til en anden varargs-callee. Løsningen er at holde op med at være en wrapper: eksportér en nøgen rutine under C-navnet og lav et tail-jump til den rigtige implementering med argumentregistrene og stacken præcis, som kaldet arrangerede dem. JPEG 2000-laget håndterer allerede snprintf og vsnprintf på denne måde og springer til de underscore-præfikserede msvcrt-navne, fordi de almindelige navne kun eksporteres af UCRT. En beslægtet begrænsning kommer fra den samme Internal error: De omdøbte importceller udfyldes fra en initialization-sektion gennem GetModuleHandleA og GetProcAddress i stedet for fra statiske initialisatorer, fordi det at tage adressen på en importeret rutine i en initialisator får compileren til at udsende en fixup, den ikke kan håndtere, og fejle med 200603061 igen
function crt_sprintf(Buffer: PAnsiChar; Fmt: PAnsiChar): Integer; cdecl; varargs;
external 'msvcrt.dll' name 'sprintf';
var
SPrintfTarget: Pointer = @crt_sprintf;
// Varargs kan ikke videresendes fra en Pascal-wrapper, så det eksporterede
// symbol tail-jumper med frame-layoutet præcis, som caller satte det op.
procedure jbig2_sprintf; assembler; nostackframe;
public name 'sprintf';
asm
jmp qword ptr [rip + SPrintfTarget]
end;
Hvad gør et Free Pascal-projekt anderledes nu?
Intet ud over unit-navnet i uses-klausulen, og der er ikke længere nogen fil, der skal distribueres. Backenden registrerer sig selv fra sin egen initialization-sektion gennem RegisterJBIG2EncoderBackend, og kaldere anmoder om den præcis som før: via optionsbitten PDF_JBIG2_OPTION_EXTERNAL_ENCODER, der har værdien 4, eller via argumentet UseExternalEncoder til de udvidede entry points. Anmodningen er stadig en præference, ikke en garanti, fordi et build, der udelader enheden, falder lydløst tilbage til den native Pascal MMR-encoder og producerer større filer i stedet for en fejl
uses
Classes, SysUtils,
PDFlibrary,
PDFlibJBIG2EncC; // Delphi, C++Builder og fra 3.538.0 Free Pascal
var
Pdf: TPDFlib;
Scan: TStream;
ImageId: Integer;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.NewDocument;
Pdf.NewPage;
Scan := TFileStream.Create('scan-page-1.tif', fmOpenRead);
try
// Interpolate, SymbolExtract, UseExternalEncoder, SkipBlackDots,
// BlackDotSize, LossyLevel
ImageId := Pdf.AddImageJBIG2FromStreamEx(Scan, 0, 1, 1, 0, 0, 0);
finally
Scan.Free;
end;
if ImageId = 0 then
raise Exception.Create('JBIG2 encoding failed');
Pdf.SaveToFile('archive.pdf');
finally
Pdf.Free;
end;
end;
To begrænsninger er værd at sige klart. Der findes kun et Win64-objektsæt, så på alle andre Free Pascal-mål rapporterer den eksterne encode-entry point en fejl, og den native Pascal-encoder udfører arbejdet. Den regression, der godkender det hele, er også en render-sammenligning snarere end et størrelsestjek: Begge encodere er lossless på den samme kilde, så outputtet renderes og sammenlignes byte for byte, og Lazarus-testpakken bestod 26 af 26 inklusive denne test. En sammenligning af komprimerede streamstørrelser ville ikke have bevist noget, fordi en inverteret side komprimeres til omtrent samme størrelse som en korrekt side
Den bredere lære gælder også uden for JBIG2. En DLL har den rigtige form, når grænsen reelt er dynamisk, hvilket er tilfældet, som DLL-, ActiveX- og dylib-integrationsfladerne er lavet til; den er den forkerte form, når den kun er en omvej uden om en COFF-læser, fordi den tilføjer en fil til hvert installationsprogram, en søgesti til hver deployment og en versionsskævhedsfejl, som statisk linkning ikke kan have. Det, der kommer før encoderen, betyder også noget, eftersom måden, det binære billede produceres på, bestemmer mere for den endelige størrelse end encoderen gør, og regionbaseret monokrom rendering i Delphi dækker den halvdel af pipelinen. Toolchain-dækning, objektsættene pr. compiler og de understøttede mål er opført på losLab PDF Developer Library-produktsiden