PDFlibPas 3.538.0 lenker den eksterne JBIG2-koderen statisk inn i Free Pascal- og Lazarus-programmer. Et prosjekt legger til enheten PDFlibJBIG2EncC, den samme enheten som Delphi og C++Builder allerede bruker, og koderen havner inne i den kjørbare filen uten noe ekstra å levere ved siden av. Det snur den tidligere konklusjonen om denne funksjonen, nemlig at Free Pascal bare kunne nå den eksterne koderen via en DLL
Hvorfor så DLL-en ut som det eneste alternativet?
DLL-en så ut som det eneste alternativet fordi tre lenkeruter feilet på tre uavhengige måter, og ingen kompilatorbryter nådde noen av dem. Den interne linkeren avviser associative COMDAT-seksjoner direkte. En ekstern lenking gjennom den medfølgende binutils krasjer inne i seksjonsoppryddingen, som Free Pascal alltid sender med på 64-biters Windows-målet. En nyere binutils kan ikke behandle Free Pascal-linkeskriptet i det hele tatt. Å bygge C++-siden på nytt med den andre verktøykjeden bytter én avvisning mot en annen, fordi template- og inline-instansiering av natur avler svake eksterne symboler, og Free Pascal rapporterer disse som Unsupported COFF symbol type 105. Ingen av disse observasjonene var feil, og den tidligere gjennomgangen av JBIG2-koderens backends og Free Pascal-linkeren går gjennom hver blindvei på en måte som fortsatt kan gjenskapes i dag. Det som var feil, var antakelsen om hvor en løsning kunne ligge. Alle forsøk gikk gjennom en kompilator eller en linker, og ingen av dem kan endre det en objektfil allerede inneholder. Objektfilen var problemet hele tiden. ObjConv leser COFF og skriver COFF, og hver konstruksjon Free Pascal setter seg fast på, har en mekanisk ekvivalent som den godtar
Feilen som aldri navngir årsaken
Free Pascal sin interne linker implementerer pick-any COMDAT bare halvveis, og denne halvferdige implementasjonen er det vanskeligste å diagnostisere her. Den slår faktisk sammen duplikatdefinisjoner, slik formatet skal. Men TExeOutput.RemoveUnreferencedSections går via exesymbol til den vinnende definisjonen når seksjoner merkes som brukte, mens TCoffexeoutput.DoRelocationFixup leser objreloc.symbol.objsection direkte. Når en brukt seksjon refererer til et symbol som objektet selv definerer i en kopi som tapte sammenslåingen, ser de to fasene på forskjellige seksjoner, og lenkingen stopper med Internal error 200603061
Sammenlign det med de to begrensningene på hver sin side av feilen. Unsupported COFF symbol type 105 sier weak external. Associative or exact match COMDAT sections are not yet supported sier associative COMDAT og navngir til og med det krenkende symbolet. Internal error 200603061 sier ingenting: ikke symbolnavn, seksjonsnavn, filnavn eller fase. Det er også normaltilfellet, ikke et særtilfelle, fordi MSVC legger alle strengliteraler og all inline- eller template-instansiering i en pick-any COMDAT, og på tvers av de 186 objektene i dette kodesettet utførte linkeren 2656 sammenslåinger. Bygging med /Gy- holder vanlige funksjoner ute av COMDAT-seksjoner per funksjon, men lar strengliteraler og template-instansieringer ligge akkurat der de lå
Hvorfor ser det alltid ut som den siste CRT-stubben ødela bygget?
Fordi linkeren først når fixup-fasen etter at alle symboler er løst. Så lenge noe fortsatt mangler, avsluttes kjøringen tidlig med Undefined symbol, og COMDAT-problemet får aldri vise seg. Fyll inn den siste C-runtime-stubben, og linkeren går videre én fase, rett inn i internal error 200603061. Felt-symptomet blir derfor systematisk misvisende: Når du legger til Pascal-kropper for de refererte C-symbolene én etter én, ser det alltid ut som den siste tilførselen ødela bygget, eller at en terskel rundt hundre stubber ble passert. Ingen av delene stemmer. Hvilket symbol som ble lagt til sist, og hvor mange som totalt ble lagt til, er irrelevant, fordi feilen lå latent i det første objektet og bare ble nåbar da oppløsningen lyktes. Når en linker endrer klagen etter at du har rettet noe urelatert, bør du spørre om du avanserte en fase, ikke om du skapte en regresjon
Løsningen er én ObjConv-kjøring, ikke et kompilatorflagg
Hele rettingen er én etterbehandlingskommando som kjøres på hvert kompilert objekt: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. Tre av alternativene ble lagt til for dette arbeidet. -xw løser IMAGE_SYM_CLASS_WEAK_EXTERNAL-symboler til vanlige eksterne symboler. -xn normaliserer IMAGE_SYM_CLASS_NULL-symboler som _fltused, som Free Pascal rapporterer som Unsupported COFF symbol type 0. -xc gjør det tunge arbeidet: Det degraderer hver COMDAT-seksjon til en vanlig seksjon og gjør symbolene den definerer, statiske. Det fjerner feilen ved å fjerne avgjørelsen, for uten COMDAT-seksjoner finnes det ingen sammenslåing, ingen vinnende kopi som én fase kan omdirigere til og en annen kan gå glipp av, og de associative .pdata- og .xdata-unwind-seksjonene forsvinner sammen med dem. Kostnaden er reell, men liten, siden kopier som legitimt kunne ha blitt slått sammen, nå hver forblir i live
Prefiksomdøpingen -np:__imp_:pdflibimp_ løser en separat kollisjon. MSVC kaller importerte Win32-API-er gjennom indirekte celler med navnene __imp_*, Free Pascal reserverer prefikset for sin egen importmekanisme, og det å definere ett av disse navnene direkte utløser den samme internal error 200603061. Når cellene får nye navn, kan Pascal-siden publisere dem som vanlige variabler og fylle dem ved kjøring. Selve objektene kompileres med flaggene for statisk lenking, /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c-, og med bilde-kodekene slått av, slik at de døde fil-I/O- og kodek-stiene trenger langt færre stubber som bare er til for lenkingen. De havner i Lib\thirdparty\Win64f, mens Delphi- og C++Builder-ruten fortsetter å lenke sitt eget Win64x-sett urørt. Det er riktig resultat for en portabilitetsretting som er avgrenset til én verktøykjede
Hva må Pascal-siden fortsatt eksportere?
Free Pascal løser en C-objektfilimport med utgangspunkt i symbolnavnet og trenger derfor navnet skrevet eksplisitt, så hver Pascal-rutine som står i stedet for et C-entry point, har en eksplisitt public name-klausul. Delphi bruker rutinenavnet som symbolnavn og trenger ingen klausul, og derfor kan én enhet brukes av begge kompilatorene med klausulene under {$IFDEF FPC}. Fellen er at en external 'msvcrt.dll'-deklarasjon ikke tilfredsstiller noe: Den oppretter en import, aldri en definisjon som et lenket objekt kan binde seg til. Forwarding-kroppen må finnes
// En ekstern deklarasjon oppretter bare en import. Ingen lenket objektfil kan
// binde seg til den.
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
external 'msvcrt.dll' name 'memcmp';
// En Pascal-kropp publisert under det nøyaktige C-symbolnavnet er det
// objektsettet faktisk binder seg 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 bryter dette mønsteret, fordi en Pascal-wrapper ikke kan sende sine egne varargs videre til en annen varargs-callee. Veien ut er å slutte å være en wrapper: Eksporter en naken rutine under C-navnet og hopp med tail jump til den virkelige implementasjonen, med argumentregistrene og stacken nøyaktig slik caller-en satte dem opp. JPEG 2000-laget håndterer allerede snprintf og vsnprintf på denne måten, og hopper til de understreksprefiksede msvcrt-navnene fordi de vanlige navnene eksporteres av UCRT alene. En beslektet begrensning kommer fra den samme internal error: De omdøpte importcellene fylles fra en initialization-seksjon gjennom GetModuleHandleA og GetProcAddress i stedet for fra statiske initialisatorer, siden det å ta adressen til en importert rutine i en initialisator får kompilatoren til å generere en fixup den ikke kan håndtere, og utløser 200603061 på nytt
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 eksporterte
// symbolet hopper med tail jump mens stack-rammen står nøyaktig som caller-en satte den opp.
procedure jbig2_sprintf; assembler; nostackframe;
public name 'sprintf';
asm
jmp qword ptr [rip + SPrintfTarget]
end;
Hva gjør et Free Pascal-prosjekt annerledes nå?
Ingenting utover enhetsnavnet i uses-klausulen, og det finnes ikke lenger en fil som må leveres. Backenden registrerer seg fra sin egen initialiseringsseksjon gjennom RegisterJBIG2EncoderBackend, og callers ber om den på akkurat samme måte som før: gjennom alternativbiten PDF_JBIG2_OPTION_EXTERNAL_ENCODER, som har verdien 4, eller gjennom argumentet UseExternalEncoder i de utvidede entry points. Det å be om den er fortsatt en preferanse, ikke en garanti, siden et bygg som utelater enheten, faller stille tilbake til den native Pascal MMR-koderen og lager større filer i stedet for en feil
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 begrensninger bør sies tydelig. Det finnes bare et Win64-objektsett, så på alle andre Free Pascal-mål rapporterer det eksterne encode-entry point-et en feil, og den native Pascal-koderen gjør arbeidet. Og regresjonstesten som styrer hele dette, er en render-sammenligning, ikke en størrelseskontroll: Begge kodere er tapsfrie på samme kilde, så resultatet deres rendres og sammenlignes byte for byte, og Lazarus-testpakken består 26 av 26, inkludert denne testen. Å sammenligne størrelsen på komprimerte strømmer ville ikke ha bevist noe, fordi en invertert side komprimeres til omtrent samme størrelse som en korrekt side
Den bredere lærdommen gjelder også utenfor JBIG2. En DLL er riktig form når grensen faktisk er dynamisk, slik overflatene for DLL-, ActiveX- og dylib-integrasjon er laget for å betjene. Den er feil form når den bare er en omvei rundt en COFF-leser, fordi den legger til en fil i hvert installasjonsprogram, en søkesti i hver distribusjon og en versjonsforskyvningsfeil som statisk lenking ikke kan ha. Oppstrømssiden betyr også noe, siden måten binærbildet produseres på avgjør mer for den endelige størrelsen enn koderen gjør, og regionbasert monokrom rendering i Delphi dekker den delen av pipelinen. Verktøykjededekning, objektsettene per kompilator og støttede mål er listet på losLab PDF Developer Library-produktsiden