PDFlibPas 3.538.0 staticky linkuje externý enkóder JBIG2 do programov vo Free Pascale a Lazare. Projekt pridá unit PDFlibJBIG2EncC, rovnaký unit, ktorý už používajú Delphi a C++Builder, a enkóder sa ocitne priamo v spustiteľnom súbore bez čohokoľvek ďalšieho, čo by bolo treba distribuovať. Tým sa prevracia predchádzajúci záver o tejto funkcii, podľa ktorého sa Free Pascal mohol k externému enkóderu dostať iba cez DLL
Prečo DLL vyzerala ako jediná možnosť?
DLL vyzerala ako jediná možnosť, pretože tri linkovacie cesty zlyhali tromi nesúvisiacimi spôsobmi a žiadny prepínač kompilátora sa k nim nedostal. Interný linker úplne odmieta asociatívne COMDAT sekcie. Externé linkovanie cez pribalené binutils spadne v garbage collectore sekcií, ktorý Free Pascal na 64-bitovom Windows cieli volá bezpodmienečne. Novšie binutils zase vôbec nedokážu spracovať linkovací skript Free Pascalu. Rebuild C++ strany s druhým toolchainom vymení jedno odmietnutie za iné, pretože inštanciácia templateov a inline kódu zo svojej podstaty vytvára weak externé symboly a Free Pascal ich hlási ako Unsupported COFF symbol type 105. Žiadny z týchto dôkazov nebol nesprávny a predchádzajúci opis backendov JBIG2 enkódera a linkera Free Pascalu dodnes prechádza každou slepou uličkou v reprodukovateľnej podobe. Nesprávny bol predpoklad, kde môže oprava žiť. Každý pokus prechádzal cez kompilátor alebo linker a ani jeden z nich nedokáže zmeniť to, čo už objektový súbor obsahuje. Problémom bol celý čas objektový súbor. ObjConv číta COFF a zapisuje COFF a každý konštrukt, na ktorom sa Free Pascal potkne, má mechanický ekvivalent, ktorý prijme
Chyba, ktorá nikdy nepovie svoju príčinu
Interný linker Free Pascalu implementuje pick-any COMDAT iba napoly a práve táto polovičná implementácia sa tu diagnostikuje najťažšie. Duplicitné definície síce zlučuje, ako formát zamýšľa. Keď však TExeOutput.RemoveUnreferencedSections pri označovaní sekcií ako použitých presmeruje cez exesymbol na víťaznú definíciu, TCoffexeoutput.DoRelocationFixup číta objreloc.symbol.objsection priamo. Keď použitá sekcia odkazuje na symbol, ktorý jeho vlastný objekt definuje v kópii, ktorá pri zlučovaní prehrala, tieto dva prechody pozerajú na rôzne sekcie a linkovanie sa zastaví na Internal error 200603061
Porovnajte to s dvoma obmedzeniami na oboch stranách. Unsupported COFF symbol type 105 znamená weak external. Associative or exact match COMDAT sections are not yet supported znamená asociatívny COMDAT a dokonca pomenuje problematický symbol. Interná chyba 200603061 nepovie nič: žiadny názov symbolu, sekcie ani súboru, žiadnu fázu. Zároveň ide o bežný prípad, nie o okrajovú situáciu, pretože MSVC vkladá každý string literal a každú inline alebo template inštanciáciu do pick-any COMDAT a v 186 objektoch tejto množiny enkódera linker vykonal 2656 zliatí. Build s /Gy- udrží bežné funkcie mimo per-function COMDAT sekcií, ale string literály a template inštanciácie nechá presne tam, kde boli
Prečo dopĺňanie stubov CRT symbolov vždy vyzerá, akoby zlyhal posledný?
Pretože linker sa k fixup fáze dostane až po vyriešení každého symbolu. Kým niečo chýba, beh sa skončí skôr na Undefined symbol a problém s COMDAT sa vôbec nemá ako prejaviť. Doplňte posledný stub C runtime a linker postúpi o jednu fázu priamo do internej chyby 200603061. Vonkajší príznak je preto systematicky zavádzajúci: keď pridávate telá Pascalu pre odkazované C symboly po jednom, vždy to vyzerá, akoby build pokazilo práve posledné doplnenie alebo akoby sa prekročil prah okolo stovky stubov. Ani jedno nie je pravda. To, ktorý symbol pribudol posledný, aj celkový počet pridaných symbolov sú irelevantné, pretože zlyhanie bolo skryté už v prvom objekte a stalo sa dosiahnuteľným až po úspešnom vyriešení. Keď linker po oprave niečoho nesúvisiaceho zmení sťažnosť, pýtajte sa, či ste postúpili do ďalšej fázy, nie či ste spôsobili regresiu
Oprava je jeden prechod ObjConv, nie príznak kompilátora
Celá oprava je jediný post-processingový príkaz spustený nad každým skompilovaným objektom: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. Tri z týchto volieb vznikli pre túto prácu. -xw rieši symboly IMAGE_SYM_CLASS_WEAK_EXTERNAL ako obyčajné externé symboly. -xn normalizuje symboly IMAGE_SYM_CLASS_NULL, napríklad _fltused, ktoré Free Pascal hlási ako Unsupported COFF symbol type 0. -xc vykonáva hlavnú prácu: každú COMDAT sekciu zníži na obyčajnú sekciu a symboly, ktoré definuje, urobí statickými. Zlyhanie tak odstráni tým, že odstráni samotné rozhodovanie, pretože bez COMDAT sekcií niet zlučovania, víťaznej kópie, na ktorú by jeden prechod presmeroval a druhý ju minul, ani asociatívnych unwind sekcií .pdata a .xdata. Cena je reálna, ale malá: kópie, ktoré sa mohli legitímne zliať, teraz každá zostane samostatne
Premenovanie prefixu -np:__imp_:pdflibimp_ rieši samostatnú kolíziu. MSVC volá importované Win32 API cez nepriame bunky s názvami __imp_*, Free Pascal si tento prefix rezervuje pre vlastný importný mechanizmus a priame definovanie jedného z týchto názvov vyvolá tú istú internú chybu 200603061. Premenovanie buniek umožní Pascalovej strane publikovať ich ako obyčajné premenné a naplniť ich za behu. Objekty sa kompilujú so zapnutým príznakom statického linkovania, /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c-, a s vypnutými obrazovými kodekmi, takže mŕtve cesty pre prácu so súbormi a kodekmi potrebujú oveľa menej stubov iba pre linker. Ukladajú sa do Lib\thirdparty\Win64f, zatiaľ čo cesta pre Delphi a C++Builder naďalej linkuje vlastnú množinu Win64x bez zásahu, čo je správny výsledok prenositeľnostnej opravy obmedzenej na jeden toolchain
Čo musí strana Pascalu stále exportovať
Free Pascal rieši import C objektu podľa názvu symbolu a potrebuje tento názov výslovne uvedený, takže každá Pascalová rutina zastupujúca vstupný bod C nesie explicitnú klauzulu public name. Delphi berie názov rutiny ako názov symbolu a žiadnu klauzulu nepotrebuje, preto jeden unit slúži obom kompilátorom s klauzulami pod {$IFDEF FPC}. Háčik je v tom, že deklarácia external 'msvcrt.dll' nič neuspokojí: vytvorí import, nie definíciu, na ktorú by sa linkovaný objekt mohol naviazať. Forwardujúce telo musí existovať
// Externá deklarácia vytvorí iba import. Žiadny linkovaný objekt sa na
// ňu nemôže naviazať.
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
external 'msvcrt.dll' name 'memcmp';
// Telo Pascalu publikované pod presným názvom C symbolu je to, na čo sa
// množina objektov skutočne viaže.
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
public name 'memcmp';
begin
Result := crt_memcmp(Buf1, Buf2, Count);
end;
Variadické vstupné body tento vzor porušujú, pretože wrapper Pascalu nemôže odovzdať vlastné varargy ďalšiemu varargovému callee. Riešením je prestať byť wrapperom: exportovať naked rutinu pod názvom C a skočiť na skutočnú implementáciu s argumentovými registrami a zásobníkom presne v stave, v akom ich pripravil volajúci. Vrstva JPEG 2000 už takto rieši snprintf a vsnprintf, pričom skáče na názvy msvcrt s prefixom podčiarkovníka, pretože obyčajné názvy exportuje iba UCRT. S tou istou internou chybou súvisí aj ďalšie obmedzenie: premenované importné bunky sa napĺňajú z initialization sekcie cez GetModuleHandleA a GetProcAddress, nie zo statických inicializátorov, pretože získanie adresy importovanej rutiny v inicializátore prinúti kompilátor vytvoriť fixup, ktorý nevie spracovať, a znovu zlyhá na 200603061
function crt_sprintf(Buffer: PAnsiChar; Fmt: PAnsiChar): Integer; cdecl; varargs;
external 'msvcrt.dll' name 'sprintf';
var
SPrintfTarget: Pointer = @crt_sprintf;
// Varargs sa nedajú preposlať z wrappera Pascalu, preto exportovaný symbol
// skočí na skutočnú implementáciu s rámcom presne v stave volajúceho.
procedure jbig2_sprintf; assembler; nostackframe;
public name 'sprintf';
asm
jmp qword ptr [rip + SPrintfTarget]
end;
Čo teraz robí projekt vo Free Pascale inak
Nič okrem názvu unitu v klauzule uses a už niet súboru, ktorý by bolo treba distribuovať. Backend sa zaregistruje z vlastnej inicializačnej sekcie cez RegisterJBIG2EncoderBackend a volajúci si ho vyžiada rovnako ako predtým: cez bit voľby PDF_JBIG2_OPTION_EXTERNAL_ENCODER, ktorý má hodnotu 4, alebo cez argument UseExternalEncoder rozšírených vstupných bodov. Vyžiadanie zostáva preferenciou, nie zárukou, pretože build, ktorý unit vynechá, potichu prejde na natívny Pascalový MMR enkóder a vytvorí väčšie súbory namiesto chyby
uses
Classes, SysUtils,
PDFlibrary,
PDFlibJBIG2EncC; // Delphi, C++Builder a od 3.538.0 aj 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;
Dve obmedzenia treba povedať otvorene. Existuje iba objektová množina Win64, takže na každom inom cieli Free Pascalu vstupný bod externého enkódovania nahlási zlyhanie a prácu prevezme natívny Pascalový enkóder. A regresný test, ktorý toto celé stráži, je porovnanie renderu, nie veľkosti: oba enkódery sú bezstratové nad rovnakým zdrojom, takže výstup sa vyrenderuje a porovná bajt po bajte, pričom Lazarus suite prechádza 26 z 26 vrátane tohto testu. Porovnávanie veľkostí komprimovaných streamov by nič nedokázalo, pretože invertovaná stránka sa skomprimuje približne na rovnakú veľkosť ako správna
Širšie poučenie presahuje JBIG2. DLL má správny tvar, keď je hranica skutočne dynamická, čo je prípad, ktorému slúžia integračné plochy DLL, ActiveX a dylib; je nesprávnym tvarom, keď ide iba o obchádzku čítačky COFF, pretože pridáva súbor do každého inštalátora, vyhľadávaciu cestu do každého nasadenia a režim zlyhania spôsobený rozdielnymi verziami, ktorý statické linkovanie nemôže mať. Záleží aj na predchádzajúcej fáze: spôsob vytvorenia bitonálneho obrazu rozhoduje o výslednej veľkosti viac než samotný enkóder a monochromatické renderovanie po regiónoch v Delphi pokrýva túto polovicu pipeline. Pokrytie toolchainov, objektové množiny pre jednotlivé kompilátory a podporované ciele sú uvedené na produktovej stránke losLab PDF Developer Library