PDFlibPas 3.538.0 statički povezuje spoljašnji JBIG2 enkoder sa programima za Free Pascal i Lazarus. Projekat dodaje jedinicu PDFlibJBIG2EncC, istu onu koju Delphi i C++Builder već koriste, pa enkoder završava unutar izvršnog fajla i uz njega nije potrebno isporučiti ništa dodatno. Time se menja raniji zaključak o ovoj funkciji, prema kojem je Free Pascal do spoljašnjeg enkodera mogao da dođe samo preko DLL-a
Zašto je DLL izgledao kao jedina opcija?
DLL je izgledao kao jedina opcija zato što su tri putanje povezivanja otkazivale na tri nepovezana načina, a nijedan prekidač kompajlera nije dopirao do njihovih uzroka. Interni linker odmah odbija asocijativne COMDAT sekcije. Spoljašnje povezivanje preko priloženog binutils-a pada unutar sakupljanja sekcija za uklanjanje, koje Free Pascal bezuslovno prosleđuje na 64-bitnom Windows cilju. Noviji binutils uopšte ne može da obradi Free Pascal link skriptu. Ponovna izgradnja C++ strane drugim toolchain-om samo menja jednu zabranu drugom, jer instanciranje template-a i inline koda po konstrukciji emituje slabe spoljašnje simbole, a Free Pascal ih prijavljuje kao Unsupported COFF symbol type 105. Nijedan od tih dokaza nije bio pogrešan, a raniji pregled JBIG2 backend-a i Free Pascal linkera i dalje prolazi kroz svaku slepu ulicu u obliku koji se danas može ponoviti. Pogrešna je bila pretpostavka o tome gde popravka može da živi. Svaki pokušaj je prolazio kroz kompajler ili linker, a nijedan ne može da promeni ono što objektni fajl već sadrži. Problem je sve vreme bio u objektnom fajlu. ObjConv čita COFF i upisuje COFF, a svaka konstrukcija na kojoj se Free Pascal spotiče ima mehanički ekvivalent koji prihvata
Greška koja nikada ne kaže svoj uzrok
Interni linker Free Pascal-a implementira pick-any COMDAT samo napola, a upravo je ta poluimplementacija najteža stvar za dijagnostikovanje. Duplikate definicija zaista spaja, kako format nalaže. Ali TExeOutput.RemoveUnreferencedSections pri označavanju korišćenih sekcija preko exesymbol preusmerava na pobedničku definiciju, dok TCoffexeoutput.DoRelocationFixup direktno čita objreloc.symbol.objsection. Kada korišćena sekcija referencira simbol koji njegov sopstveni objekat definiše u kopiji koja je izgubila spajanje, dva prolaza gledaju različite sekcije i povezivanje se zaustavlja sa Internal error 200603061
Uporedite to sa dva ograničenja sa obe strane. Unsupported COFF symbol type 105 kaže da je u pitanju slabi eksterni simbol. Associative or exact match COMDAT sections are not yet supported kaže da je u pitanju asocijativni COMDAT i čak imenuje sporni simbol. Interna greška 200603061 ne kaže ništa: ni ime simbola, ni ime sekcije, ni ime fajla, ni fazu. To je ujedno normalan slučaj, a ne rubni, jer MSVC svaku string konstantu i svaku inline ili template instancu stavlja u pick-any COMDAT, a kroz 186 objekata u ovom skupu enkodera linker je izvršio 2656 spajanja. Izgradnja sa /Gy- izbacuje obične funkcije iz COMDAT sekcija po funkciji, ali string konstante i template instance ostavlja tačno tamo gde su bile
Zašto popunjavanje CRT simbola uvek izgleda kao da je poslednje dodato pokvarilo izgradnju?
Zato što linker do prolaza za popravke stiže tek kada se svaki simbol razreši. Dok god nešto nedostaje, izvršavanje se rano završava sa Undefined symbol i COMDAT problem nema priliku da se pojavi. Popunite poslednji C runtime stub i linker prelazi u sledeću fazu, pravo u internu grešku 200603061. Terenski simptom je zato sistematski varljiv: kada jednu po jednu dodajete Pascal tela za referencirane C simbole, uvek izgleda kao da je poslednji dodatak pokvario izgradnju ili da je pređen neki prag od oko stotinu stubova. Ni jedno ni drugo nije tačno. Koji simbol je dodat poslednji i koliko ih je ukupno dodato potpuno je nebitno, jer je greška bila prisutna od prvog objekta i postala dostupna tek kada je razrešavanje uspelo. Kada linker promeni prigovor nakon što popravite nešto nepovezano, pitajte se da li ste samo prešli u sledeću fazu, umesto da ste izazvali regresiju
Popravka je jedan ObjConv prolaz, a ne prekidač kompajlera
Celokupna korekcija je jedna komanda za naknadnu obradu koja se izvršava nad svakim kompajliranim objektom: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. Tri od tih opcija dodate su upravo za ovaj posao. -xw razrešava simbole IMAGE_SYM_CLASS_WEAK_EXTERNAL u obične eksterne simbole. -xn normalizuje simbole IMAGE_SYM_CLASS_NULL, kao što je _fltused, koje Free Pascal prijavljuje kao Unsupported COFF symbol type 0. -xc obavlja glavni posao: svaku COMDAT sekciju spušta na običnu sekciju, a simbole koje ona definiše čini statičkim. Time se greška uklanja tako što se uklanja sama odluka, jer bez COMDAT sekcija nema spajanja, nema pobedničke kopije na koju bi jedan prolaz preusmerio, a drugi je promašio, i asocijativne .pdata i .xdata unwind sekcije nestaju zajedno sa tim. Cena postoji, ali je mala: kopije koje su legitimno mogle da se spoje sada svaka ostaje zasebno
Prefiksno preimenovanje -np:__imp_:pdflibimp_ rešava zaseban sudar. MSVC poziva uvezene Win32 API-je preko indirekcionih ćelija nazvanih __imp_*, Free Pascal taj prefiks čuva za sopstvenu import mehaniku, a direktno definisanje jednog od tih imena pokreće istu internu grešku 200603061. Preimenovanje ćelija omogućava Pascal strani da ih objavi kao obične promenljive i popuni tokom izvršavanja. Sami objekti kompajliraju se sa uključenom zastavicom za statičko povezivanje, /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c-, i sa isključenim image codec-ima, pa mrtve putanje za rad sa fajlovima i kodecima zahtevaju mnogo manje stubova potrebnih samo za povezivanje. Oni završavaju u Lib\thirdparty\Win64f, dok Delphi i C++Builder putanja nastavlja da povezuje sopstveni skup Win64x bez promena, što je pravi ishod popravke prenosivosti ograničene na jedan toolchain
Šta Pascal strana i dalje mora da izveze
Free Pascal razrešava import C objekta po imenu simbola i zato to ime mora biti eksplicitno navedeno, pa svaka Pascal rutina koja zamenjuje C entry point nosi izričitu klauzulu public name. Delphi uzima ime rutine kao ime simbola i ne zahteva nikakvu klauzulu, zbog čega jedna jedinica služi oba kompajlera uz klauzule pod {$IFDEF FPC}. Zamka je u tome što deklaracija external 'msvcrt.dll' ne zadovoljava ništa: ona kreira import, nikada definiciju za koju povezani objekat može da se veže. Prosleđujuće telo mora da postoji
// Spoljašnja deklaracija kreira samo import. Nijedan povezani objekat ne može
// da se veže za nju.
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
external 'msvcrt.dll' name 'memcmp';
// Pascal telo objavljeno pod tačnim imenom C simbola jeste ono
// za šta se skup objekata zaista vezuje.
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
public name 'memcmp';
begin
Result := crt_memcmp(Buf1, Buf2, Count);
end;
Variadic entry point-i lome taj obrazac, jer Pascal wrapper ne može da prosledi sopstveni varargs drugom varargs pozivaocu. Izlaz je da prestanete da budete wrapper: izvezite naked rutinu pod C imenom i uradite tail-jump na pravu implementaciju sa registrima argumenata i stekom tačno onako kako ih je pozivalac pripremio. JPEG 2000 sloj već na ovaj način obrađuje snprintf i vsnprintf, skačući na spellings sa prefiksom donje crte koje koristi msvcrt, jer obična imena izvozi samo UCRT. Još jedno povezano ograničenje proizlazi iz iste interne greške: preimenovane import ćelije popunjavaju se iz initialization sekcije preko GetModuleHandleA i GetProcAddress, a ne iz statičkih inicijalizatora, pošto uzimanje adrese uvezene rutine u inicijalizatoru navodi kompajler da emituje fixup koji ne može da obradi i ponovo završava sa 200603061
function crt_sprintf(Buffer: PAnsiChar; Fmt: PAnsiChar): Integer; cdecl; varargs;
external 'msvcrt.dll' name 'sprintf';
var
SPrintfTarget: Pointer = @crt_sprintf;
// Varargs se ne mogu proslediti iz Pascal wrapper-a, zato izvezeni
// simbol radi tail-jump sa okvirom tačno onako kako ga je pozivalac postavio.
procedure jbig2_sprintf; assembler; nostackframe;
public name 'sprintf';
asm
jmp qword ptr [rip + SPrintfTarget]
end;
Šta Free Pascal projekat sada radi drugačije
Ništa osim imena jedinice u uses klauzuli, a fajl za isporuku više ne postoji. Backend se registruje iz sopstvene initialization sekcije preko RegisterJBIG2EncoderBackend, a pozivaoci ga traže kao i ranije: preko options bita PDF_JBIG2_OPTION_EXTERNAL_ENCODER, čija je vrednost 4, ili preko argumenta UseExternalEncoder proširenih entry point-a. Zahtev ostaje preferencija, a ne garancija, jer se izgradnja kojoj nedostaje jedinica tiho vraća na izvorni Pascal MMR enkoder i proizvodi veće fajlove umesto greške
uses
Classes, SysUtils,
PDFlibrary,
PDFlibJBIG2EncC; // Delphi, C++Builder i, od 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;
Dva ograničenja vredi izgovoriti jasno. Postoji samo skup Win64 objekata, pa na svakom drugom Free Pascal cilju entry point za spoljašnje kodiranje prijavljuje neuspeh, a izvorni Pascal enkoder obavlja posao. Regresija koja sve ovo kontroliše jeste poređenje rendera, a ne provera veličine: oba enkodera su lossless nad istim izvorom, pa se njihov izlaz renderuje i poredi bajt po bajt, a Lazarus test suite prolazi 26 od 26 testova, uključujući i taj. Poređenje veličina kompresovanih stream-ova ne bi dokazalo ništa, jer se invertovana stranica kompresuje približno isto kao ispravna
Šira pouka važi i izvan JBIG2. DLL je pravi oblik kada je granica zaista dinamička, što je slučaj za koji postoje DLL, ActiveX i dylib integracione površine; pogrešan je kada je samo zaobilazno rešenje za COFF čitač, jer svakom installeru dodaje fajl, svakoj instalaciji search path, kao i način otkaza zbog razlike u verziji koji statičko povezivanje ne može da ima. Važan je i upstream, jer način na koji se proizvodi bilevel slika više utiče na konačnu veličinu nego enkoder, a monohromatsko renderovanje regiona u Delphi-ju pokriva tu polovinu pipeline-a. Pokrivenost toolchain-a, skupovi objekata po kompajleru i podržani ciljevi navedeni su na losLab stranici proizvoda PDF Developer Library