Műszaki cikk

jbig2enc statikus linkelése Free Pascalban DLL nélkül

A PDFlibPas 3.538.0 statikusan linkeli a külső JBIG2-kódolót Free Pascal és Lazarus programokba. A projekt felveszi a PDFlibJBIG2EncC unitot, ugyanazt, amelyet a Delphi és a C++Builder már használ, így a kódoló a futtatható állomány része lesz, és semmi továbbit nem kell mellé telepíteni. Ez megfordítja a funkcióról korábban levont következtetést: akkor úgy tűnt, hogy a Free Pascal csak DLL-en keresztül érheti el a külső kódolót

Miért tűnt a DLL az egyetlen lehetőségnek?

A DLL azért tűnt az egyetlen lehetőségnek, mert három különböző linkelési út három egymástól független módon vallott kudarcot, és egyikhez sem volt megfelelő compilerkapcsoló. A belső linker eleve elutasítja az asszociatív COMDAT-szakaszokat. A mellékelt binutils külső linkelése összeomlik a szakaszok szemétgyűjtésében, amelyet a Free Pascal feltétel nélkül bekapcsol a 64 bites Windows célplatformon. Az újabb binutils egyáltalán nem tudja feldolgozni a Free Pascal linkerszkriptjét. A C++ oldal újrafordítása a másik eszközkészlettel csak az egyik elutasítást cseréli egy másikra, mert a template- és inline-instanciák természetükből fakadóan weak external szimbólumokat bocsátanak ki, a Free Pascal pedig ezeket Unsupported COFF symbol type 105 hibával jelzi. Egyik megfigyelés sem volt téves, és a JBIG2-kódoló backenjeiről és a Free Pascal linkerről szóló korábbi leírás ma is reprodukálhatóan végigveszi a zsákutcákat. A téves feltételezés az volt, hogy a javításnak hol kell megszületnie. Minden próbálkozás egy compileren vagy egy linken ment át, egyik sem tudja megváltoztatni azt, amit egy objektumfájl már tartalmaz. Végig maga az objektumfájl volt a probléma. Az ObjConv COFF-ot olvas és COFF-ot ír, és minden olyan szerkezetnek, amelyen a Free Pascal fennakad, van mechanikus, elfogadható megfelelője

A hiba, amely sosem nevezi meg az okát

A Free Pascal belső linkere csak félig valósítja meg a pick-any COMDAT támogatását, és ez a félmegoldás a legnehezebben diagnosztizálható része az egész történetnek. A duplikált definíciókat a formátum szándéka szerint valóban összevonja. Amikor azonban a használt szakaszokat megjelöli, a TExeOutput.RemoveUnreferencedSections az exesymbol-on keresztül a nyertes definícióra irányít, miközben a TCoffexeoutput.DoRelocationFixup közvetlenül az objreloc.symbol.objsection mezőt olvassa. Ha egy használt szakasz olyan szimbólumra hivatkozik, amelyet a saját objektuma egy, az összevonást elvesztő példányban definiál, a két menet különböző szakaszokat lát, és a linkelés Internal error 200603061 hibával leáll

Érdemes összevetni ezt a két oldalsó korláttal. Az Unsupported COFF symbol type 105 weak external szimbólumot jelent. Az Associative or exact match COMDAT sections are not yet supported asszociatív COMDAT-szakaszt jelent, és még a hibás szimbólumot is megnevezi. Az Internal error 200603061 viszont semmit sem mond: nincs szimbólumnév, szakasznév, fájlnév vagy fázis. Ráadásul ez a normál eset, nem valamilyen ritka sarokeset, mert az MSVC minden string literált, inline-instanciát és template-instanciát pick-any COMDAT-ba tesz, és a kódoló 186 objektumán a linker 2656 összevonást hajtott végre. A /Gy- kapcsolóval a hagyományos függvények kikerülnek a függvényenkénti COMDAT-szakaszokból, de a string literálok és template-instanciák pontosan ott maradnak, ahol voltak

Miért tűnik mindig úgy, hogy a CRT-szimbólumok csonkolása törte el utoljára?

Azért, mert a linker csak akkor jut el a fixup fázisig, amikor minden szimbólum feloldódott. Amíg bármi hiányzik, a futás korán leáll Undefined symbol hibával, így a COMDAT-probléma még nem kerülhet felszínre. Töltsd ki az utolsó C runtime stubböt, és a linker egyetlen fázissal tovább lép, egyenesen az Internal error 200603061 hibába. A terepen ezért rendszeresen félrevezető a tünet: amikor egyenként hozzáadod a hivatkozott C-szimbólumok Pascal-törzsét, mindig úgy látszik, hogy a legutóbbi kiegészítés törte el a buildet, vagy hogy valamilyen száz körüli stubküszöböt léptél át. Egyik sem igaz. Teljesen mindegy, melyik szimbólum került be utoljára, és összesen hány került be, mert a hiba már az első objektumtól kezdve jelen volt, csak a sikeres feloldás után vált elérhetővé. Ha a linker megváltoztatja a panaszát egy látszólag független javítás után, először azt kérdezd meg, hogy egy újabb fázist értél-e el, nem pedig regressziót okoztál

A javítás egy ObjConv-meneten múlik, nem egy compilerkapcsolón

A teljes korrekció egyetlen utófeldolgozó parancs, amelyet minden lefordított objektumon le kell futtatni: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. A három új opció ebből a munkából született. Az -xw az IMAGE_SYM_CLASS_WEAK_EXTERNAL szimbólumokat hagyományos externállá alakítja. Az -xn normalizálja az IMAGE_SYM_CLASS_NULL szimbólumokat, például az _fltused-ot, amelyet a Free Pascal Unsupported COFF symbol type 0 hibaként jelent. Az -xc végzi a nehéz munkát: minden COMDAT-szakaszt egyszerű szakasszá fokoz vissza, és a benne definiált szimbólumokat statikussá teszi. A hiba így azért szűnik meg, mert magát a döntési helyzetet szüntetjük meg: COMDAT-szakaszok nélkül nincs összevonás, nincs nyertes példány, amelyre az egyik menet átirányíthat, a másik pedig nem találhat rá, és az asszociatív .pdata és .xdata unwind-szakaszok is eltűnnek. Ennek valódi, de kicsi ára van: az olyan példányok, amelyek jogosan összeolvadhattak volna, most külön-külön maradnak meg

A -np:__imp_:pdflibimp_ előtag-átnevezés egy külön ütközést old meg. Az MSVC a Win32 API-kat __imp_* nevű indirekciós cellákon keresztül hívja, a Free Pascal ezt az előtagot a saját importmechanizmusának tartja fenn, és egy ilyen név közvetlen definiálása ugyanazt az Internal error 200603061 hibát váltja ki. A cellák átnevezésével a Pascal oldal közönséges változóként teheti közzé és futásidőben töltheti ki őket. Magukat az objektumokat a statikus linkelési kapcsolókkal fordítjuk: /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c-, valamint kikapcsolt kép-kódolókkal, így a nem használt fájl-I/O és codec útvonalakhoz jóval kevesebb csak linkeléshez szükséges stub kell. Az objektumok a Lib\thirdparty\Win64f könyvtárba kerülnek, miközben a Delphi és C++Builder útvonal változatlanul a saját Win64x készletét linkeli. Ez a helyes eredmény egy olyan hordozhatósági javításnál, amelyet egyetlen toolchainre korlátozunk

Mit kell még exportálnia a Pascal oldalnak?

A Free Pascal egy C-objektum importját szimbólumnév alapján oldja fel, ezért minden C belépési pontot helyettesítő Pascal-rutin explicit public name záradékot kap. A Delphi a rutin nevét veszi szimbólumnévnek, és nem kér záradékot, ezért egy unit szolgálja ki mindkét compilert a záradékokkal a {$IFDEF FPC} alatt. A csapda az, hogy egy external 'msvcrt.dll' deklaráció semmit sem old meg: importot hoz létre, nem pedig olyan definíciót, amelyhez egy linkelt objektum kötődhet. A továbbító törzsnek ténylegesen léteznie kell

// A külső deklaráció csak importot hoz létre. Egy linkelt objektum
// nem tud hozzá kötődni.
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  external 'msvcrt.dll' name 'memcmp';

// A pontos C-szimbólumnéven közzétett Pascal-törzshez kötődik
// ténylegesen az objektumkészlet.
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  public name 'memcmp';
begin
  Result := crt_memcmp(Buf1, Buf2, Count);
end;

A variadikus belépési pontok megtörik ezt a mintát, mert egy Pascal wrapper nem tudja továbbadni a saját varargs paramétereit egy másik varargs hívónak. A kiút az, hogy megszűnsz wrapper lenni: exportálj egy naked rutint C-néven, és ugorj farokugrással a valódi implementációra úgy, hogy az argumentumregiszterek és a stack pontosan a hívó által előkészített állapotban maradjanak. A JPEG 2000 réteg már így kezeli a snprintf és vsnprintf függvényeket, az aláhúzásos msvcrt-nevekre ugorva, mert a sima neveket csak az UCRT exportálja. Egy kapcsolódó megkötés ugyanebből a belső hibából következik: az átnevezett importcellák egy initialization szakaszból, GetModuleHandleA és GetProcAddress segítségével töltődnek ki statikus inicializátorok helyett, mert egy importált rutin címének inicializátorban való lekérése olyan fixupot bocsát ki, amelyet a compiler nem tud kezelni, és újra 200603061 hibával áll le

function crt_sprintf(Buffer: PAnsiChar; Fmt: PAnsiChar): Integer; cdecl; varargs;
  external 'msvcrt.dll' name 'sprintf';

var
  SPrintfTarget: Pointer = @crt_sprintf;

// A varargs nem továbbítható Pascal wrapperből, ezért az exportált
// szimbólum úgy ugrik tovább, hogy a hívó által felépített keret érintetlen marad.
procedure jbig2_sprintf; assembler; nostackframe;
  public name 'sprintf';
asm
  jmp qword ptr [rip + SPrintfTarget]
end;

Mit csinál most másképp egy Free Pascal projekt?

Semmit az uses záradékban megadott unit nevén kívül, és többé nincs telepítendő fájl. A backend a saját inicializálási szakaszából, a RegisterJBIG2EncoderBackend segítségével regisztrálja magát, a hívók pedig ugyanúgy kérik, mint eddig: a PDF_JBIG2_OPTION_EXTERNAL_ENCODER opcióbittel, amelynek értéke 4, vagy a kiterjesztett belépési pontok UseExternalEncoder argumentumán keresztül. A kérés továbbra is csak preferencia, nem garancia, mert ha a buildből kimarad a unit, csendben visszaesik a natív Pascal MMR-kódolóra, és hiba helyett nagyobb fájlokat készít

uses
  Classes, SysUtils,
  PDFlibrary,
  PDFlibJBIG2EncC;   // Delphi, C++Builder és 3.538.0-tól 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;

Két korlátot érdemes világosan kimondani. Csak Win64 objektumkészlet létezik, ezért minden más Free Pascal célplatformon a külső kódolás belépési pontja hibát jelez, és a munkát a natív Pascal-kódoló végzi el. Az egész funkciót engedélyező regresszió nem méretellenőrzés, hanem render-összehasonlítás: ugyanazon a forráson mindkét kódoló veszteségmentes, ezért a kimenetet rendereljük és bájtról bájtra hasonlítjuk össze, a Lazarus tesztsorozat pedig 26-ból 26 tesztet teljesít, ezt is beleértve. A tömörített stream méretének összevetése semmit sem bizonyított volna, mert egy invertált oldal nagyjából ugyanakkorára tömörödik, mint a helyes

A tanulság a JBIG2-n túl is általánosítható. A DLL akkor a megfelelő forma, ha a határ valóban dinamikus, erre szolgálnak a DLL, ActiveX és dylib integrációs felületek; viszont rossz forma, ha csak egy COFF-olvasó kerülőútja, mert minden telepítőhöz hozzáad egy fájlt, minden telepítéshez egy keresési útvonalat, és egy olyan verzió-eltérési hibalehetőséget, amely statikus linkelésnél nem létezik. A felsőbb réteg is számít: a bináris kép előállításának módja többet dönthet a végső méretről, mint maga a kódoló, ezt a pipeline-felet pedig a régióalapú monokróm renderelés Delphiben tárgyalja. A toolchain-lefedettség, a compilerenkénti objektumkészletek és a támogatott célok a losLab PDF Developer Library termékoldalán szerepelnek