PDFlibPas 3.538.0 statiniu būdu susieja išorinį JBIG2 koduotuvą su Free Pascal ir Lazarus programomis. Projektas prideda PDFlibJBIG2EncC unitą, tą patį unitą, kurį jau naudoja Delphi ir C++Builder, o koduotuvas patenka į vykdomąjį failą, todėl šalia jo nereikia pateikti jokių papildomų failų. Tai paneigia ankstesnę išvadą apie šią funkciją, kad Free Pascal išorinį koduotuvą galėjo pasiekti tik per DLL
Kodėl atrodė, kad DLL yra vienintelė išeitis?
DLL atrodė vienintelė išeitis, nes trys susiejimo keliai žlugo trimis nesusijusiais būdais, o joks kompiliatoriaus jungiklis jų nepasiekė. Vidinis linkeris iš karto atmeta asociatyvias COMDAT sekcijas. Išorinis susiejimas per kartu pateiktus binutils įrankius nulūžta sekcijų šiukšlių surinkimo etape, kurį Free Pascal 64 bitų Windows taikiniui įjungia besąlygiškai. Naujesni binutils apskritai negali apdoroti Free Pascal susiejimo scenarijaus. Perkompiliavus C++ dalį kita toolchain, vienas atmetimas pakeičiamas kitu, nes šablonų ir inline egzempliorių generavimas iš prigimties sukuria silpnus išorinius simbolius, o Free Pascal juos praneša kaip Unsupported COFF symbol type 105. Šie įrodymai nebuvo klaidingi, o ankstesniame pasakojime apie JBIG2 koduotuvo posistemes ir Free Pascal linkerį kiekviena aklavietė aprašyta taip, kad ją ir šiandien galima pakartoti. Klaidinga buvo prielaida, kur gali būti pataisymas. Kiekvienas bandymas ėjo per kompiliatorių arba linkerį, o nė vienas iš jų negali pakeisti to, kas jau yra objektiniame faile. Problema visą laiką buvo objektiniame faile. ObjConv skaito COFF ir rašo COFF, o kiekviena konstrukcija, nuo kurios Free Pascal stringa, turi mechaninį atitikmenį, kurį jis priima
Klaida, kuri niekada neįvardija priežasties
Free Pascal vidinis linkeris pick-any COMDAT realizuoja tik iš dalies, o būtent ši pusinė realizacija čia sunkiausiai diagnozuojama. Jis iš tiesų sulieja pasikartojančias apibrėžtis taip, kaip numato formatas. Tačiau žymėdamas sekcijas kaip naudojamas TExeOutput.RemoveUnreferencedSections per exesymbol nukreipia į laimėjusią apibrėžtį, o TCoffexeoutput.DoRelocationFixup tiesiogiai skaito objreloc.symbol.objsection. Kai naudojama sekcija nurodo simbolį, kurį jo paties objektas apibrėžia kopijoje, pralaimėjusioje suliejimą, abu etapai mato skirtingas sekcijas ir susiejimas sustoja su Internal error 200603061
Palyginkime tai su dviem apribojimais abipus šios klaidos. Unsupported COFF symbol type 105 reiškia silpną išorinį simbolį. Associative or exact match COMDAT sections are not yet supported nurodo asociatyvią COMDAT sekciją ir net įvardija pažeidžiantį simbolį. Internal error 200603061 nepasako nieko: nei simbolio, nei sekcijos, nei failo pavadinimo, nei etapo. Tai taip pat įprastas, o ne kraštinis atvejis, nes MSVC kiekvieną eilutės literalą ir kiekvieną inline ar šablono egzempliorių deda į pick-any COMDAT, o 186 šio koduotuvo objektuose linkeris atliko 2656 suliejimus. Kuriant su /Gy-, įprastos funkcijos nebededamos į kiekvienos funkcijos COMDAT sekcijas, tačiau eilutės literalai ir šablonų egzemplioriai lieka ten pat
Kodėl CRT simbolių stubų pridėjimas visada atrodo taip, lyg paskutinis jį sugadino?
Linkeris į pataisų etapą patenka tik tada, kai išsprendžiami visi simboliai. Kol ko nors trūksta, vykdymas anksti baigiasi su Undefined symbol, todėl COMDAT problema neturi progos pasirodyti. Įrašykite paskutinį C runtime stubą ir linkeris pereis vienu etapu pirmyn, tiesiai į internal error 200603061. Todėl matomas požymis sistemingai klaidina: po vieną pridedant Pascal realizacijas nurodytiems C simboliams visada atrodo, kad naujausias papildymas sugadino buildą arba kad peržengta maždaug šimto stubų riba. Nė vienas paaiškinimas neteisingas. Nesvarbu, kuris simbolis pridėtas paskutinis ir kiek jų pridėta iš viso, nes gedimas glūdėjo jau pirmajame objekte ir tapo pasiekiamas tik tada, kai simbolių sprendimas baigėsi sėkmingai. Kai linkeris pakeičia skundą po to, kai pataisote nesusijusį dalyką, paklauskite, ar tiesiog perėjote vienu etapu pirmyn, o ne sukėlėte regresiją
Pataisymas yra vienas ObjConv etapas, o ne kompiliatoriaus jungiklis
Visas pataisymas yra viena poapdorojimo komanda, vykdoma su kiekvienu sukompiliuotu objektu: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. Trys iš šių parinkčių pridėtos būtent šiam darbui. -xw paverčia IMAGE_SYM_CLASS_WEAK_EXTERNAL simbolius į paprastus išorinius simbolius. -xn normalizuoja IMAGE_SYM_CLASS_NULL simbolius, pavyzdžiui, _fltused, kurį Free Pascal praneša kaip Unsupported COFF symbol type 0. -xc atlieka pagrindinį darbą: kiekvieną COMDAT sekciją paverčia paprasta sekcija ir joje apibrėžtus simbolius padaro statinius. Taip gedimas pašalinamas pašalinant patį sprendimą, nes be COMDAT sekcijų nėra suliejimo, nėra laiminčios kopijos, į kurią vienas etapas gali nukreipti, o kitas jos nerasti, be to, kartu išnyksta asociatyvios .pdata ir .xdata išvyniojimo sekcijos. Kaina reali, bet maža: kopijos, kurias buvo galima teisėtai sulieti, dabar visos išlieka atskiros
Prefikso -np:__imp_:pdflibimp_ pervadinimas išsprendžia atskirą susidūrimą. MSVC importuotas Win32 API iškviečia per netiesiogines ląsteles, pavadintas __imp_*, o Free Pascal šį prefiksą rezervuoja savo importo mechanizmui, todėl tiesiogiai apibrėžus bet kurį tokį vardą vėl gaunama internal error 200603061. Pervadinus ląsteles, Pascal pusė gali jas skelbti kaip paprastus kintamuosius ir užpildyti vykdymo metu. Patys objektai kompiliuojami su statinio susiejimo parinktimis /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c- ir išjungtais vaizdų kodekais, todėl neveikiantiems failų I/O ir kodekų keliams reikia daug mažiau vien susiejimui skirtų stubų. Jie patenka į Lib\thirdparty\Win64f, o Delphi ir C++Builder kelias toliau susieja savo Win64x rinkinį nepakeistą, ir tai yra teisingas vienai toolchain apriboto perkeliamumo pataisymo rezultatas
Ką Pascal pusė vis dar turi eksportuoti
Free Pascal C objekto importą išsprendžia pagal simbolio vardą, todėl kiekvienai Pascal procedūrai, atstojančiai C entry point, reikia aiškios public name sąlygos. Delphi procedūros vardą naudoja kaip simbolio vardą ir tokios sąlygos nereikalauja, todėl vienas unitas gali aptarnauti abu kompiliatorius, kai sąlygos įrašytos po {$IFDEF FPC}. Spąstai slypi tame, kad deklaracija external 'msvcrt.dll' nieko neišsprendžia: ji sukuria importą, bet niekada nekuria apibrėžties, prie kurios galėtų prisijungti susietas objektas. Tarpinė realizacija turi egzistuoti
// Išorinė deklaracija sukuria tik importą. Joks susietas objektas negali
// prie jo prisijungti.
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
external 'msvcrt.dll' name 'memcmp';
// Pascal realizacija, paskelbta tiksliu C simbolio vardu, yra tai, prie ko
// iš tikrųjų prisijungia objektų rinkinys.
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
public name 'memcmp';
begin
Result := crt_memcmp(Buf1, Buf2, Count);
end;
Įvairaus argumentų skaičiaus entry point sugadina šį modelį, nes Pascal wrapper negali persiųsti savo varargs kitai varargs iškviečiamai funkcijai. Išeitis yra nustoti būti wrapperiu: eksportuokite nuogą procedūrą C vardu ir atlikite tail jump į tikrą realizaciją, palikdami argumentų registrus ir steką tiksliai tokios būsenos, kokią paruošė caller. JPEG 2000 sluoksnis taip jau tvarko snprintf ir vsnprintf, šokdamas į msvcrt vardus su pabraukimo prefiksu, nes paprastus vardus eksportuoja tik UCRT. Su ta pačia vidine klaida susijęs dar vienas apribojimas: pervadintos importo ląstelės užpildomos iš initialization sekcijos per GetModuleHandleA ir GetProcAddress, o ne statiniais inicializatoriais, nes importuotos procedūros adreso paėmimas inicializatoriuje priverčia kompiliatorių sugeneruoti pataisą, kurios jis negali apdoroti, ir vėl baigtis 200603061
function crt_sprintf(Buffer: PAnsiChar; Fmt: PAnsiChar): Integer; cdecl; varargs;
external 'msvcrt.dll' name 'sprintf';
var
SPrintfTarget: Pointer = @crt_sprintf;
// Varargs negalima persiųsti iš Pascal wrapperio, todėl eksportuotas
// simbolis atlieka tail jump, išlaikydamas caller paruoštą kadrą.
procedure jbig2_sprintf; assembler; nostackframe;
public name 'sprintf';
asm
jmp qword ptr [rip + SPrintfTarget]
end;
Kas dabar kitaip Free Pascal projekte
Niekas, išskyrus unit pavadinimą uses sąlygoje, ir nebeliko failo, kurį reikėtų diegti. Posistemė registruoja save iš savo initialization sekcijos per RegisterJBIG2EncoderBackend, o iškvietėjai jos prašo lygiai kaip anksčiau: per parinkties bitą PDF_JBIG2_OPTION_EXTERNAL_ENCODER, kurio reikšmė yra 4, arba per išplėstų entry point parametrą UseExternalEncoder. Prašymas tebėra pageidavimas, o ne garantija, nes buildas, kuriame unitas praleistas, tyliai grįžta prie vietinio Pascal MMR koduotuvo ir vietoje klaidos sukuria didesnius failus
uses
Classes, SysUtils,
PDFlibrary,
PDFlibJBIG2EncC; // Delphi, C++Builder ir nuo 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;
Verta aiškiai įvardyti du apribojimus. Egzistuoja tik Win64 objektų rinkinys, todėl kiekviename kitame Free Pascal taikinyje išorinio kodavimo entry point praneša apie nesėkmę, o darbą atlieka vietinis Pascal koduotuvas. Visą funkciją kontroliuojantis regresijos testas yra atvaizdavimo palyginimas, o ne dydžio tikrinimas: abu koduotuvai tam pačiam šaltiniui yra be nuostolių, todėl jų išvestis atvaizduojama ir lyginama baitas po baito, o Lazarus rinkinys, įskaitant šį testą, išlaikė 26 iš 26. Suglaudintų srautų dydžių palyginimas nieko neįrodytų, nes apverstas puslapis susispaudžia maždaug iki tokio pat dydžio kaip teisingas
Platesnė pamoka taikoma ne vien JBIG2. DLL yra tinkama forma, kai riba iš tikrųjų dinaminė, kaip rodo DLL, ActiveX ir dylib integravimo paviršiai; ji netinkama, kai tėra COFF skaitytuvo apėjimas, nes kiekvienam diegimo paketui prideda failą, kiekvienam diegimui paieškos kelią ir versijų neatitikties gedimo režimą, kurio statinis susiejimas negali turėti. Svarbi ir ankstesnė grandies dalis, nes dvejetainio vaizdo sukūrimo būdas galutinį dydį lemia labiau nei pats koduotuvas, o regionais paremtas vienspalvis atvaizdavimas Delphi aplinkoje apima kitą konvejerio pusę. Toolchain aprėptis, kiekvieno kompiliatoriaus objektų rinkiniai ir palaikomi taikiniai išvardyti losLab PDF Developer Library produkto puslapyje