Tekninen artikkeli

jbig2encin staattinen linkitys Free Pascaliin ilman DLL:ää

PDFlibPas 3.538.0 linkittää ulkoisen JBIG2-enkooderin staattisesti Free Pascal- ja Lazarus-ohjelmiin. Projekti lisää PDFlibJBIG2EncC-unitin, saman unitin jota Delphi ja C++Builder jo käyttävät, ja enkooderi päätyy suoritettavaan tiedostoon ilman sen rinnalle toimitettavaa lisätiedostoa. Tämä kumoaa aiemman tätä ominaisuutta koskeneen johtopäätöksen, jonka mukaan Free Pascal pääsisi ulkoiseen enkooderiin vain DLL:n kautta

Miksi DLL näytti ainoalta vaihtoehdolta?

DLL näytti ainoalta vaihtoehdolta, koska kolme linkityspolkua epäonnistui kolmella toisistaan riippumattomalla tavalla eikä mikään kääntäjän valinta yltänyt niihin. Sisäinen linkkeri hylkää assosiatiiviset COMDAT-osiot suoraan. Mukana toimitettujen binutils-työkalujen kautta tehty ulkoinen linkitys kaatuu osioiden garbage collection -vaiheessa, jonka Free Pascal ottaa 64-bittisessä Windows-kohteessa ehdottomasti käyttöön. Uudempi binutils ei pysty käsittelemään Free Pascalin linkkisriptiä lainkaan. C++-puolen kääntäminen toisella toolchainilla vaihtaa yhden hylkäyksen toiseen, koska template- ja inline-instansiointi tuottaa luonnostaan heikkoja ulkoisia symboleja ja Free Pascal raportoi ne muodossa Unsupported COFF symbol type 105. Mikään tästä näytöstä ei ollut väärin, ja aiempi selvitys JBIG2-enkooderin taustajärjestelmistä ja Free Pascal -linkkeristä käy yhä läpi jokaisen umpikujan tavalla, joka voidaan toistaa. Väärä oletus koski sitä, missä korjauksen voisi tehdä. Jokainen yritys kulki kääntäjän tai linkkerin kautta, eikä kumpikaan voi muuttaa sitä, mitä objektitiedosto jo sisältää. Ongelma oli koko ajan objektitiedostossa. ObjConv lukee COFFia ja kirjoittaa COFFia, ja jokaisella rakenteella, johon Free Pascal kompastuu, on mekaaninen vastine, jonka se hyväksyy

Virhe, joka ei koskaan kerro syytään

Free Pascalin sisäinen linkkeri toteuttaa pick-any COMDAT -menettelyn vain puoliksi, ja juuri tämä puolittainen toteutus on tämän ongelman vaikein diagnosoitava osa. Se kyllä yhdistää päällekkäiset määrittelyt formaatin tarkoittamalla tavalla. Mutta TExeOutput.RemoveUnreferencedSections ohjaa käytettyjä osioita merkittäessään exesymbol-symbolin kautta voittaneeseen määrittelyyn, kun taas TCoffexeoutput.DoRelocationFixup lukee suoraan kentän objreloc.symbol.objsection. Kun käytetty osio viittaa symboliin, jonka sen oma objekti määrittelee yhdistämisen hävinneessä kopiossa, vaiheet tarkastelevat eri osioita ja linkitys pysähtyy virheeseen Internal error 200603061

Vertaa tätä virheen molemmin puolin oleviin kahteen rajoitukseen. Unsupported COFF symbol type 105 tarkoittaa weak external -symbolia. Associative or exact match COMDAT sections are not yet supported kertoo assosiatiivisesta COMDAT-osiosta ja nimeää jopa rikkovan symbolin. Internal error 200603061 ei kerro mitään: ei symbolin nimeä, osion nimeä, tiedoston nimeä eikä vaihetta. Se on myös normaalitapaus eikä reunatapaus, koska MSVC sijoittaa jokaisen merkkijonoliteraalin sekä jokaisen inline- tai template-instansioinnin pick-any COMDAT -osioon, ja tämän enkooderijoukon 186 objektin yli linkkeri teki 2656 yhdistämistä. Kääntäminen valinnalla /Gy- pitää tavalliset funktiot poissa funktiokohtaisista COMDAT-osioista, mutta jättää merkkijonoliteraalit ja template-instansioinnit täsmälleen entisille paikoilleen

Miksi CRT-symbolien stubien lisääminen näyttää aina rikkovan buildin viimeisellä kerralla?

Koska linkkeri saavuttaa fixup-vaiheen vasta, kun jokainen symboli on ratkaistu. Niin kauan kuin jotakin puuttuu, ajo päättyy aikaisin virheeseen Undefined symbol, eikä COMDAT-ongelma pääse koskaan näkyviin. Täytä viimeinen C-runtime-stub ja linkkeri etenee yhden vaiheen suoraan internal error 200603061 -virheeseen. Käytännön oire johtaa siksi järjestelmällisesti harhaan: kun viitatuille C-symboleille lisätään Pascal-rungot yksi kerrallaan, näyttää aina siltä, että viimeisin lisäys rikkoi buildin tai että noin sadan stubin kohdalla ylitettiin jokin raja. Kumpikaan ei pidä paikkaansa. Sillä, mikä symboli lisättiin viimeisenä, ja sillä, kuinka monta niitä lisättiin yhteensä, ei ole merkitystä, koska vika oli piilevänä jo ensimmäisessä objektissa ja tuli saavutettavaksi vasta, kun ratkaisu onnistui. Kun linkkeri vaihtaa ilmoittamaansa virhettä sen jälkeen, kun korjasit näennäisesti asiaan liittymättömän kohdan, kysy etenitkö yhden vaiheen verran sen sijaan, että olisit aiheuttanut regression

Korjaus on yksi ObjConv-ajo, ei kääntäjän valinta

Koko korjaus on yksi jokaiseen käännettyyn objektiin ajettava jälkikäsittelykomento: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. Kolme näistä valinnoista lisättiin tätä työtä varten. -xw muuntaa IMAGE_SYM_CLASS_WEAK_EXTERNAL-symbolit tavallisiksi ulkoisiksi symboleiksi. -xn normalisoi IMAGE_SYM_CLASS_NULL -symbolit, kuten _fltused-symbolin, jonka Free Pascal raportoi muodossa Unsupported COFF symbol type 0. -xc tekee raskaan työn: se alentaa jokaisen COMDAT-osion tavalliseksi osioksi ja muuttaa sen määrittelemät symbolit staattisiksi. Vika poistuu poistamalla päätös, sillä ilman COMDAT-osioita ei ole yhdistämistä, voittavaa kopiota jonka yksi vaihe voisi ohittaa ja toinen missata, eivätkä assosiatiiviset .pdata- ja .xdata-unwind-osiotkaan jää jäljelle. Kustannus on todellinen mutta pieni, sillä kopiot jotka olisi voitu oikeutetusti yhdistää säilyvät nyt kaikki erillisinä

-np:__imp_:pdflibimp_-etuliitteen uudelleennimeäminen ratkaisee erillisen törmäyksen. MSVC kutsuu tuotuja Win32-APIja __imp_*-nimisten epäsuoruussolujen kautta, Free Pascal varaa tämän etuliitteen omaan import-mekanismiinsa, ja yhden näistä nimistä suoraan määritteleminen laukaisee jälleen internal error 200603061 -virheen. Kun solut nimetään uudelleen, Pascal-puoli voi julkaista ne tavallisina muuttujina ja täyttää ne ajon aikana. Objektit käännetään itse staattisen linkityksen valinnoilla /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c- ja kuvakoodekit kytketään pois päältä, joten kuolleet tiedosto-I/O- ja codec-polut tarvitsevat paljon vähemmän vain linkitystä varten tehtyjä stub-rutiineja. Ne sijoitetaan hakemistoon Lib\thirdparty\Win64f, kun taas Delphi- ja C++Builder-polku linkittää oman Win64x-joukkonsa muuttamatta sitä, mikä on oikea lopputulos yhteen toolchainiin rajatulle siirrettävyyskorjaukselle

Mitä Pascal-puolen täytyy edelleen eksportata

Free Pascal ratkaisee C-objektin importin symbolin nimen perusteella, joten jokaisella C-entry pointin korvaavalla Pascal-rutiinilla täytyy olla eksplisiittinen public name -lauseke. Delphi käyttää rutiinin nimeä symbolin nimenä eikä tarvitse lausetta lainkaan, minkä vuoksi yksi unit palvelee molempia kääntäjiä, kun lausekkeet sijoitetaan kohdan {$IFDEF FPC} alle. Ansana on, ettei external 'msvcrt.dll' -määrittely tyydytä mitään: se luo importin, ei koskaan määritelmää johon linkitetty objekti voisi sitoutua. Välitysrungon täytyy olla olemassa

// Ulkoinen määrittely luo vain importin. Mikään linkitetty objekti ei voi
// sitoutua siihen.
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  external 'msvcrt.dll' name 'memcmp';

// Tarkalla C-symbolin nimellä julkaistu Pascal-runko on se, johon
// objektijoukko todella sitoutuu.
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  public name 'memcmp';
begin
  Result := crt_memcmp(Buf1, Buf2, Count);
end;

Vaihtelevan argumenttimäärän entry pointit rikkovat tämän mallin, koska Pascal-wrapper ei voi välittää omia varargs-argumenttejaan toiselle varargs-calleelle. Ratkaisu on lakata olemasta wrapper: eksportoi C-nimellä paljas rutiini ja tee tail-jump oikeaan toteutukseen argumenttirekistereillä ja pinolla täsmälleen siinä muodossa, johon caller ne järjesti. JPEG 2000 -kerros käsittelee jo snprintf- ja vsnprintf-funktiot tällä tavalla ja hyppää underscore-etuliitteisiin msvcrt-nimiin, koska tavalliset nimet eksportoi vain UCRT. Sama internal error tuo mukanaan vielä yhden rajoituksen: uudelleennimetyt import-solut täytetään initialization-osiosta GetModuleHandleA- ja GetProcAddress-kutsuilla eikä staattisista initialisaattoreista, sillä tuodun rutiinin osoitteen ottaminen initialisaattorissa saa kääntäjän tuottamaan fixupin jota se ei pysty käsittelemään ja epäonnistumaan jälleen virheeseen 200603061

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

var
  SPrintfTarget: Pointer = @crt_sprintf;

// Varargs-argumentteja ei voi välittää Pascal-wrapperista, joten eksportattu
// symboli tekee tail-jumpin callerin jättämällä frame-asettelulla.
procedure jbig2_sprintf; assembler; nostackframe;
  public name 'sprintf';
asm
  jmp qword ptr [rip + SPrintfTarget]
end;

Miten Free Pascal -projekti toimii nyt eri tavalla

Ei mitenkään muuten kuin uses-lausekkeen unit-nimen osalta, eikä toimitettavaa tiedostoa enää ole. Taustajärjestelmä rekisteröi itsensä omasta initialization-osiostaan RegisterJBIG2EncoderBackend-kutsulla, ja kutsujat pyytävät sitä täsmälleen kuten ennenkin: options-bitillä PDF_JBIG2_OPTION_EXTERNAL_ENCODER, jonka arvo on 4, tai laajennettujen entry pointien UseExternalEncoder-argumentilla. Pyyntö on yhä toive eikä takuu, koska unitin pois jättävä build putoaa äänettömästi alkuperäiseen Pascal MMR -enkooderiin ja tuottaa virheen sijaan suurempia tiedostoja

uses
  Classes, SysUtils,
  PDFlibrary,
  PDFlibJBIG2EncC;   // Delphi, C++Builder ja versiosta 3.538.0 alkaen 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;

Kaksi rajoitusta on syytä sanoa suoraan. Win64-objektijoukko on ainoa olemassa oleva, joten jokaisella muulla Free Pascal -kohteella ulkoinen encode-entry point raportoi epäonnistumisen ja työn tekee alkuperäinen Pascal-enkooderi. Kaiken tämän hyväksyvä regressiotesti on renderöintivertailu eikä kokotarkistus: molemmat enkooderit ovat häviöttömiä samalla lähteellä, joten tulosteet renderöidään ja niitä verrataan tavu tavulta, ja Lazarus-testipaketti läpäisi kaikki 26 testiä, myös tämän. Pakattujen streamien kokojen vertaaminen ei olisi todistanut mitään, koska käänteinen sivu pakkautuu suunnilleen saman kokoiseksi kuin oikea sivu

Laajempi oppi ulottuu JBIG2:n ulkopuolelle. DLL on oikea muoto, kun rajapinta on todella dynaaminen, kuten DLL-, ActiveX- ja dylib-integraatiorajapinnat osoittavat; se on väärä muoto, kun se on vain kiertotie COFF-lukijan ohi, koska se lisää tiedoston jokaiseen asennusohjelmaan, hakupolun jokaiseen käyttöönottoon ja version ristiriidasta johtuvan vikatilanteen, jota staattinen linkitys ei voi tuottaa. Myös enkooderia edeltävä vaihe on tärkeä, sillä kaksitasoisen kuvan tuotantotapa ratkaisee lopullisesta koosta enemmän kuin enkooderi, ja alueisiin perustuva yksivärinen renderöinti Delphissä kattaa tämän pipelinen puolikkaan. Toolchain-kattavuus, kääntäjäkohtaiset objektijoukot ja tuetut kohteet on lueteltu losLab PDF Developer Library -tuotesivulla