PDFlibPas 3.538.0 leagă static encoder-ul extern JBIG2 în programe Free Pascal și Lazarus. Proiectul adaugă unit-ul PDFlibJBIG2EncC, același unit folosit deja de Delphi și C++Builder, iar encoder-ul ajunge în executabil fără nimic suplimentar de livrat alături de el. Asta răstoarnă concluzia anterioară despre această funcționalitate, conform căreia Free Pascal putea ajunge la encoder-ul extern doar printr-un DLL
De ce părea DLL-ul singura opțiune?
DLL-ul părea singura opțiune deoarece trei rute de linking eșuau în trei moduri fără legătură între ele și niciun compiler switch nu ajungea la cauza lor. Linker-ul intern respinge direct secțiunile COMDAT asociative. Un link extern prin binutils incluse se prăbușește în garbage collection-ul secțiunilor, pe care Free Pascal îl transmite necondiționat pentru target-ul Windows pe 64 de biți. Binutils mai noi nu pot procesa deloc link script-ul Free Pascal. Recompilarea părții C++ cu celălalt toolchain schimbă un refuz cu altul, deoarece instanțierile de template și inline emit prin construcție simboluri weak external, iar Free Pascal le raportează ca Unsupported COFF symbol type 105. Niciuna dintre aceste observații nu era greșită, iar descrierea anterioară a backend-urilor encoder-ului JBIG2 și a linker-ului Free Pascal parcurge fiecare dead end într-o formă care se reproduce și astăzi. Greșită era presupunerea despre locul în care putea exista remedierea. Fiecare încercare trecea printr-un compiler sau linker, iar niciunul nu poate schimba ce conține deja un object file. Object file-ul era problema tot timpul. ObjConv citește COFF și scrie COFF, iar fiecare construcție la care se împiedică Free Pascal are un echivalent mecanic pe care îl acceptă
Eroarea care nu își numește niciodată cauza
Linker-ul intern Free Pascal implementează pick-any COMDAT doar pe jumătate, iar această implementare incompletă este cea mai greu de diagnosticat parte a problemei. El chiar unește definițiile duplicate, așa cum cere formatul. Însă TExeOutput.RemoveUnreferencedSections redirecționează prin exesymbol către definiția câștigătoare când marchează secțiunile ca folosite, în timp ce TCoffexeoutput.DoRelocationFixup citește direct objreloc.symbol.objsection. Când o secțiune folosită face referire la un simbol pe care propriul object îl definește într-o copie care a pierdut unirea, cele două treceri privesc secțiuni diferite, iar link-ul se oprește cu Internal error 200603061
Compară asta cu cele două limite de pe fiecare parte. Unsupported COFF symbol type 105 spune weak external. Associative or exact match COMDAT sections are not yet supported spune associative COMDAT și numește chiar simbolul problematic. Internal error 200603061 nu spune nimic: nici nume de simbol, nici nume de secțiune, nici nume de fișier, nici etapă. Este și cazul normal, nu o situație marginală, deoarece MSVC pune fiecare string literal și fiecare instanțiere inline sau template într-un pick-any COMDAT, iar în cele 186 de object-uri ale acestui encoder linker-ul a făcut 2656 de uniri. Build-ul cu /Gy- ține funcțiile obișnuite în afara secțiunilor COMDAT per funcție, dar lasă string literal-urile și instanțierile de template exact acolo unde erau
De ce pare că ultimul stub pentru simbolurile CRT a stricat totul?
Deoarece linker-ul ajunge la pass-ul de fixup abia după ce fiecare simbol a fost rezolvat. Cât timp mai lipsește ceva, execuția se termină devreme cu Undefined symbol, iar problema COMDAT nu are ocazia să iasă la suprafață. Completează ultimul stub pentru C runtime și linker-ul avansează o etapă, direct în Internal error 200603061. Simptomul din teren este deci sistematic înșelător: când adaugi pe rând corpuri Pascal pentru simbolurile C referite, pare mereu că ultima adăugare a stricat build-ul sau că a fost depășit un prag de aproximativ o sută de stub-uri. Niciuna nu este adevărată. Ce simbol a fost adăugat ultimul și câte au fost adăugate în total sunt irelevante, fiindcă failure-ul era latent încă din primul object și a devenit accesibil doar după ce rezolvarea a reușit. Când linker-ul își schimbă plângerea după ce repari ceva fără legătură, întreabă-te dacă ai avansat o etapă, nu dacă ai introdus un regression
Remedierea este un singur pass ObjConv, nu un compiler flag
Corecția completă este o singură comandă de post-processing rulată peste fiecare object compilat: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. Trei dintre aceste opțiuni au fost adăugate pentru această lucrare. -xw transformă simbolurile IMAGE_SYM_CLASS_WEAK_EXTERNAL în external-uri obișnuite. -xn normalizează simbolurile IMAGE_SYM_CLASS_NULL, precum _fltused, pe care Free Pascal le raportează ca Unsupported COFF symbol type 0. -xc face munca grea: retrogradează fiecare secțiune COMDAT la o secțiune simplă și face statice simbolurile definite de ea. Astfel failure-ul dispare prin eliminarea deciziei în sine: fără secțiuni COMDAT nu mai există unire, nici copie câștigătoare către care un pass să redirecționeze iar altul să nu o găsească, iar secțiunile associative .pdata și .xdata de unwind dispar împreună cu problema. Costul există, dar este mic: copiile care s-ar fi putut uni legitim supraviețuiesc acum separat
Redenumirea cu prefixul -np:__imp_:pdflibimp_ rezolvă o coliziune separată. MSVC apelează API-urile Win32 importate prin indirection cells numite __imp_*, Free Pascal rezervă prefixul pentru propria import machinery, iar definirea directă a unuia dintre aceste nume declanșează aceeași eroare internă 200603061. Redenumirea celulelor îi permite părții Pascal să le publice ca variabile obișnuite și să le completeze la runtime. Object-urile sunt compilate cu flag-ul de static link /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c- și cu codecurile de imagine dezactivate, astfel încât rutele moarte de file I/O și codec au nevoie de mult mai puține stub-uri doar pentru link. Ele ajung în Lib\thirdparty\Win64f, în timp ce ruta Delphi și C++Builder continuă să lege propriul set Win64x fără schimbări, care este rezultatul corect pentru un fix de portabilitate limitat la un singur toolchain
Ce mai trebuie să exporte partea Pascal
Free Pascal rezolvă importul unui object C după numele simbolului și are nevoie ca acel nume să fie scris explicit, așa că fiecare rutină Pascal care ține locul unui C entry point poartă o clauză explicită public name. Delphi folosește numele rutinei ca nume de simbol și nu are nevoie de nicio clauză, motiv pentru care un singur unit deservește ambele compilere cu clauzele sub {$IFDEF FPC}. Capcana este că declarația external 'msvcrt.dll' nu satisface nimic: creează un import, nu o definiție la care să se poată lega un object. Corpul de forwarding trebuie să existe
// O declarație external creează doar un import. Niciun object legat nu poate
// face binding la el.
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
external 'msvcrt.dll' name 'memcmp';
// Un corp Pascal publicat sub numele exact al simbolului C este cel la care
// se leagă efectiv setul de object-uri.
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
public name 'memcmp';
begin
Result := crt_memcmp(Buf1, Buf2, Count);
end;
Entry point-urile variadice rup acest model, deoarece un wrapper Pascal nu poate transmite propriile varargs către un alt callee varargs. Soluția este să nu mai fii wrapper: exportă o rutină naked sub numele C și fă un tail-jump către implementarea reală, cu registrele de argumente și stack-ul exact cum le-a pregătit caller-ul. Layer-ul JPEG 2000 tratează deja snprintf și vsnprintf astfel, sărind către spelling-urile msvcrt cu prefix underscore, deoarece numele simple sunt exportate doar de UCRT. O limitare înrudită vine din aceeași eroare internă: celulele de import redenumite sunt completate dintr-o secțiune initialization prin GetModuleHandleA și GetProcAddress, nu din static initializers, deoarece luarea adresei unei rutine importate într-un initializer face compiler-ul să emită un fixup pe care nu îl poate procesa și să eșueze din nou cu 200603061
function crt_sprintf(Buffer: PAnsiChar; Fmt: PAnsiChar): Integer; cdecl; varargs;
external 'msvcrt.dll' name 'sprintf';
var
SPrintfTarget: Pointer = @crt_sprintf;
// Varargs nu pot fi transmise dintr-un wrapper Pascal, așa că simbolul
// exportat face tail-jump cu frame-ul exact așa cum l-a pregătit caller-ul.
procedure jbig2_sprintf; assembler; nostackframe;
public name 'sprintf';
asm
jmp qword ptr [rip + SPrintfTarget]
end;
Ce face diferit acum un proiect Free Pascal
Nimic în afară de numele unit-ului din clauza uses, iar acum nu mai există un fișier de livrat. Backend-ul se înregistrează din propria secțiune de initialization prin RegisterJBIG2EncoderBackend, iar caller-ii îl cer exact ca înainte: prin bitul de opțiuni PDF_JBIG2_OPTION_EXTERNAL_ENCODER, care are valoarea 4, sau prin argumentul UseExternalEncoder al entry point-urilor extinse. Cererea rămâne o preferință, nu o garanție, deoarece un build care omite unit-ul revine silențios la encoder-ul Pascal MMR nativ și produce fișiere mai mari în locul unei erori
uses
Classes, SysUtils,
PDFlibrary,
PDFlibJBIG2EncC; // Delphi, C++Builder și, din 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;
Două limite merită enunțate clar. Există doar un object set Win64, așa că pe orice alt target Free Pascal entry point-ul de encoding extern raportează failure, iar encoder-ul Pascal nativ preia lucrul. Iar regression-ul care controlează toate acestea este o comparație de render, nu o verificare a dimensiunii: ambele encodere sunt lossless pe aceeași sursă, deci output-ul este randat și comparat byte cu byte, iar suita Lazarus trece 26 din 26, inclusiv testul respectiv. Compararea dimensiunii stream-urilor comprimate nu ar fi demonstrat nimic, deoarece o pagină inversată se comprimă aproximativ la aceeași dimensiune ca una corectă
Lecția mai largă se aplică și dincolo de JBIG2. Un DLL este forma potrivită când limita este cu adevărat dinamică, exact cazul pentru care există suprafețele de integrare DLL, ActiveX și dylib; este forma greșită când este doar un workaround pentru un COFF reader, deoarece adaugă un fișier în fiecare installer, un search path în fiecare deployment și un failure mode de version skew pe care linking-ul static nu îl poate avea. Contează și ce vine înainte, deoarece modul în care este produsă imaginea bilevel decide mai mult din dimensiunea finală decât encoder-ul, iar randarea monocromă pe regiuni în Delphi acoperă acea jumătate a pipeline-ului. Acoperirea toolchain-urilor, seturile de object-uri pentru fiecare compiler și target-urile acceptate sunt listate pe pagina de produs losLab PDF Developer Library