PDFlibPas 3.538.0 staticky propojí externí enkodér JBIG2 do programů ve Free Pascalu a Lazarusu. Projekt přidá jednotku PDFlibJBIG2EncC, stejnou jednotku, kterou už používají Delphi a C++Builder, a enkodér se ocitne přímo ve spustitelném souboru bez dalšího souboru k distribuci. Tím se obrací předchozí závěr o této funkci, podle něhož se Free Pascal mohl k externímu enkodéru dostat jen přes DLL
Proč DLL vypadala jako jediná možnost
DLL vypadala jako jediná možnost, protože tři cesty propojení selhaly třemi nesouvisejícími způsoby a žádný přepínač kompilátoru se k žádnému z nich nedostal. Interní linker asociativní sekce COMDAT rovnou odmítá. Externí linkování přes přibalené binutils spadne při garbage collection sekcí, kterou Free Pascal na 64bitovém cíli Windows předává bezpodmínečně. Novější binutils nedokážou zpracovat linkovací skript Free Pascalu vůbec. Přestavba části C++ s jiným toolchainem jen vymění jedno odmítnutí za jiné, protože instanciace šablon a inline funkcí ze své podstaty vytváří slabé externí symboly a Free Pascal je hlásí jako Unsupported COFF symbol type 105. Žádný z těchto důkazů nebyl chybný a dřívější popis backendů enkodéru JBIG2 a linkeru Free Pascalu prochází všechny slepé uličky způsobem, který lze dodnes reprodukovat. Chybný byl předpoklad, kde může oprava ležet. Každý pokus vedl přes kompilátor nebo linker a ani jeden z nich nemůže změnit obsah již vytvořeného objektového souboru. Problémem byl od začátku objektový soubor. ObjConv načítá COFF a zapisuje COFF a pro každou konstrukci, na které se Free Pascal zadrhne, existuje mechanicky ekvivalentní podoba, kterou přijme
Chyba, která nikdy nepojmenuje svou příčinu
Interní linker Free Pascalu implementuje pick-any COMDAT jen napůl a právě tato neúplná implementace se zde diagnostikuje nejobtížněji. Duplicitní definice skutečně slučuje, jak formát zamýšlí. Když ale označuje sekce jako použité, TExeOutput.RemoveUnreferencedSections přesměrovává přes exesymbol na vítěznou definici, zatímco TCoffexeoutput.DoRelocationFixup čte přímo objreloc.symbol.objsection. Když použitá sekce odkazuje na symbol, který jeho vlastní objekt definuje v kopii, jež slučování prohrála, obě fáze pracují s jinými sekcemi a linkování skončí chybou Internal error 200603061
Porovnejte to s dvěma limity na obou stranách. Unsupported COFF symbol type 105 říká, že jde o slabý externí symbol. Associative or exact match COMDAT sections are not yet supported říká, že jde o asociativní COMDAT, a dokonce pojmenuje problematický symbol. Interní chyba 200603061 neříká vůbec nic: žádné jméno symbolu, sekce, souboru ani fáze. Je také běžným případem, nikoli okrajovou situací, protože MSVC vkládá každý string literal i každou inline nebo template instanciaci do pick-any COMDAT a v této sadě 186 objektů enkodéru linker provedl 2656 sloučení. Build s /Gy- udrží běžné funkce mimo COMDAT sekce jednotlivých funkcí, ale string literals a template instanciace nechá přesně tam, kde byly
Proč se náhrady symbolů CRT vždy tváří, jako by sestavení rozbila poslední z nich
Protože linker se k fixační fázi dostane až po vyřešení všech symbolů. Dokud něco chybí, běh skončí předčasně hláškou Undefined symbol a problém COMDAT se vůbec nedostane na povrch. Po doplnění posledního stubu C runtime linker postoupí o jednu fázi a narazí přímo na interní chybu 200603061. Viditelný příznak je proto systematicky zavádějící: když postupně přidáváte Pascalová těla odkazovaných C symbolů, vždy to vypadá, že build rozbilo poslední přidání nebo že byla překročena hranice kolem stovky stubů. Ani jedno není pravda. Který symbol byl přidán naposledy i kolik jich přibylo celkem je irelevantní, protože selhání bylo skryté už v prvním objektu a stalo se dosažitelným teprve po úspěšném vyřešení symbolů. Když linker po opravě nesouvisejícího problému změní svou stížnost, ptejte se, zda jste postoupili o fázi, místo abyste způsobili regresi
Oprava je jeden průchod ObjConv, ne přepínač kompilátoru
Celá oprava je jediný příkaz následného zpracování spuštěný nad každým zkompilovaným objektem: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. Tři z těchto voleb byly přidány právě kvůli této práci. -xw převede symboly IMAGE_SYM_CLASS_WEAK_EXTERNAL na běžné externí symboly. -xn normalizuje symboly IMAGE_SYM_CLASS_NULL, například _fltused, které Free Pascal hlásí jako Unsupported COFF symbol type 0. -xc provede hlavní práci: každou sekci COMDAT degraduje na obyčejnou sekci a symboly, které definuje, označí jako statické. Tím odstraní selhání odstraněním samotného rozhodování, protože bez sekcí COMDAT není co slučovat, neexistuje vítězná kopie, na kterou by jedna fáze přesměrovávala a druhá ji hledala marně, a zmizí i asociativní unwind sekce .pdata a .xdata. Cena je skutečná, ale malá, protože kopie, které bylo možné legitimně sloučit, nyní všechny zůstanou samostatně
Přejmenování prefixu -np:__imp_:pdflibimp_ řeší samostatný konflikt. MSVC volá importovaná Win32 API přes nepřímé buňky pojmenované __imp_*, Free Pascal si tento prefix rezervuje pro vlastní importní mechanismus a přímá definice jednoho z těchto jmen znovu vyvolá interní chybu 200603061. Přejmenování dovolí Pascalové straně zveřejnit buňky jako běžné proměnné a naplnit je za běhu. Samotné objekty se kompilují se zapnutým příznakem statického linkování, /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c-, a s vypnutými image kodeky, takže nepoužívané cesty pro soubory a kodeky potřebují mnohem méně stubů určených jen pro linkování. Objekty skončí v Lib\thirdparty\Win64f, zatímco cesta Delphi a C++Builderu dál linkuje vlastní sadu Win64x beze změny, což je správný výsledek opravy přenositelnosti omezené na jediný toolchain
Co musí Pascalová strana stále exportovat
Free Pascal řeší import C objektu podle jména symbolu a potřebuje toto jméno explicitně zapsané, takže každá Pascalová rutina zastupující vstupní bod C nese výslovnou klauzuli public name. Delphi bere jméno rutiny jako jméno symbolu a žádnou klauzuli nepotřebuje, proto jedna jednotka obslouží oba kompilátory s klauzulemi pod {$IFDEF FPC}. Past spočívá v tom, že deklarace external 'msvcrt.dll' nic neřeší: vytvoří import, ale nikoli definici, na kterou by se mohl připojit linkovaný objekt. Předávací tělo musí existovat
// Externí deklarace vytváří jen import. Žádný připojený objekt se
// na něj nemůže navázat.
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
external 'msvcrt.dll' name 'memcmp';
// Pascalové tělo zveřejněné pod přesným jménem C symbolu je to,
// na co se skutečná sada objektů naváž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í, protože Pascalový wrapper nemůže předat vlastní varargs jinému volanému varargs. Řešením je přestat být wrapperem: exportujte nahou rutinu pod jménem C a proveďte tail jump do skutečné implementace s registry argumentů a zásobníkem přesně v podobě, v jaké je připravil volající. Vrstva JPEG 2000 už takto zpracovává snprintf a vsnprintf a skáče na podoby msvcrt s podtržítkem, protože běžná jména exportuje pouze UCRT. Ze stejné interní chyby plyne ještě jedno omezení: přejmenované importní buňky se naplňují z oddílu initialization přes GetModuleHandleA a GetProcAddress, nikoli ze statických inicializátorů, protože získání adresy importované rutiny v inicializátoru přiměje kompilátor vygenerovat fixup, který neumí zpracovat, a znovu selže chybou 200603061
function crt_sprintf(Buffer: PAnsiChar; Fmt: PAnsiChar): Integer; cdecl; varargs;
external 'msvcrt.dll' name 'sprintf';
var
SPrintfTarget: Pointer = @crt_sprintf;
// Varargs nelze předat z Pascalového wrapperu, proto exportovaný symbol
// provede tail jump s rámcem ponechaným přesně podle volajícího.
procedure jbig2_sprintf; assembler; nostackframe;
public name 'sprintf';
asm
jmp qword ptr [rip + SPrintfTarget]
end;
Co projekt Free Pascalu nyní dělá jinak
Nic kromě názvu jednotky v klauzuli uses a nově není potřeba distribuovat žádný soubor. Backend se registruje z vlastního oddílu initialization přes RegisterJBIG2EncoderBackend a volající si ho vyžádají stejně jako dříve: přes bit volby PDF_JBIG2_OPTION_EXTERNAL_ENCODER s hodnotou 4 nebo přes argument UseExternalEncoder rozšířených vstupních bodů. Požadavek zůstává preferencí, nikoli zárukou, protože build, který jednotku vynechá, se tiše vrátí k nativnímu Pascalovému enkodéru MMR a vytvoří větší soubory místo chyby
uses
Classes, SysUtils,
PDFlibrary,
PDFlibJBIG2EncC; // Delphi, C++Builder a od 3.538.0 také 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;
Je třeba jasně uvést dvě omezení. Existuje pouze sada objektů Win64, takže na každém jiném cíli Free Pascalu vstupní bod externího enkódování ohlásí selhání a práci převezme nativní Pascalový enkodér. A regresní test, který to celé hlídá, je porovnání renderu, nikoli velikosti: oba enkodéry jsou nad stejným zdrojem bezztrátové, takže jejich výstup se vyrenderuje a porovná bajt po bajtu a sada Lazarus prošla všech 26 z 26 testů včetně tohoto. Porovnání velikostí komprimovaných streamů by nic neprokázalo, protože převrácená stránka se komprimuje přibližně na stejnou velikost jako správná
Širší poučení přesahuje JBIG2. DLL je správným tvarem, když je hranice skutečně dynamická, což je případ, pro který existují integrační rozhraní DLL, ActiveX a dylib; nesprávným tvarem je ale tehdy, když jde jen o obcházení čtečky COFF, protože přidává soubor do každého instalátoru, vyhledávací cestu do každého nasazení a režim selhání kvůli rozdílným verzím, který statické linkování nemůže mít. Záleží i na předchozí fázi, protože způsob vytvoření dvouúrovňového obrazu ovlivní výslednou velikost více než samotný enkodér a region-based monochrome rendering v Delphi pokrývá tuto polovinu pipeline. Pokrytí toolchainů, sady objektů pro jednotlivé kompilátory a podporované cíle uvádí produktová stránka losLab PDF Developer Library