Technisch artikel

jbig2enc statisch linken in Free Pascal zonder DLL

PDFlibPas 3.538.0 linkt de externe JBIG2-encoder statisch in Free Pascal- en Lazarus-programma's. Een project voegt de PDFlibJBIG2EncC-unit toe, dezelfde unit die Delphi en C++Builder al gebruiken, en de encoder belandt in het executable zonder dat er iets extra's naast hoeft te worden meegeleverd. Daarmee wordt de eerdere conclusie over deze functie omgekeerd: Free Pascal zou de externe encoder alleen via een DLL kunnen bereiken

Waarom leek de DLL de enige optie?

De DLL leek de enige optie omdat drie linkroutes op drie losstaande manieren faalden en geen enkele compileroptie daar iets aan veranderde. De interne linker weigert associative COMDAT-secties zonder meer. Een externe link via de meegeleverde binutils crasht tijdens section garbage collection, die Free Pascal op het 64-bits Windows-doel onvoorwaardelijk doorgeeft. Nieuwere binutils kunnen het Free Pascal-linkscript helemaal niet verwerken. De C++-kant met de andere toolchain opnieuw bouwen ruilt de ene weigering in voor de andere, omdat template- en inline-instantiatie vanzelf weak external-symbolen uitstoot en Free Pascal die meldt als Unsupported COFF symbol type 105. Geen van die aanwijzingen was onjuist, en het eerdere overzicht van de JBIG2-encoderbackends en de Free Pascal-linker loopt elke doodlopende route door op een manier die vandaag nog reproduceerbaar is. Wat niet klopte, was de aanname over waar een oplossing kon zitten. Elke poging liep via een compiler of linker, en geen van beide kan veranderen wat al in een objectbestand staat. Het objectbestand was al die tijd het probleem. ObjConv leest COFF en schrijft COFF, en elke constructie waar Free Pascal op vastloopt heeft een mechanisch equivalent dat het wel accepteert

De fout die de oorzaak nooit noemt

De interne linker van Free Pascal implementeert pick-any COMDAT maar half, en juist die halve implementatie is hier het moeilijkst te diagnosticeren. Dubbele definities worden wel samengevoegd, zoals het formaat voorschrijft. Maar TExeOutput.RemoveUnreferencedSections gaat via exesymbol naar de winnende definitie wanneer secties als gebruikt worden gemarkeerd, terwijl TCoffexeoutput.DoRelocationFixup rechtstreeks objreloc.symbol.objsection leest. Wanneer een gebruikte sectie verwijst naar een symbool dat door het eigen object wordt gedefinieerd in een kopie die de samenvoeging verloor, kijken de twee passes naar verschillende secties en stopt de link met Internal error 200603061

PDFlibPas stuit bij static linking op interne fout 200603061 wanneer de Free Pascal-linker dubbele pick-any COMDAT-secties samenvoegt: RemoveUnreferencedSections resolveert een symbool via exesymbol naar de winnende kopie terwijl DoRelocationFixup rechtstreeks objreloc.symbol.objsection leest, zodat de twee passes op verschillende secties mikken en de link stopt zonder een symbool, sectie, bestand of fase te benoemen
Verwijst een gebruikte sectie naar een symbool waarvan de verloren COMDAT-kopie door de samenvoeging is weggegooid, dan lezen de markeerpass en de relocatiepass verschillende secties en stopt Free Pascal met een interne fout die niets benoemt

Vergelijk dat met de twee grenzen aan weerszijden ervan. Unsupported COFF symbol type 105 betekent weak external. Associative or exact match COMDAT sections are not yet supported betekent associative COMDAT en noemt zelfs het problematische symbool. Internal error 200603061 zegt helemaal niets: geen symboolnaam, geen sectienaam, geen bestandsnaam en geen fase. Het is bovendien de normale situatie en geen randgeval, omdat MSVC elke stringliteral en elke inline- of template-instantiatie in een pick-any COMDAT zet; over de 186 objecten in deze encoderset voerde de linker 2656 samenvoegingen uit. Bouwen met /Gy- houdt gewone functies buiten COMDAT-secties per functie, maar laat stringliterals en template-instantiaties precies staan waar ze stonden

Waarom lijkt het bij stubs voor CRT-symbolen steeds alsof de laatste de build breekt?

Omdat de linker de fixup-pass pas bereikt nadat elk symbool is opgelost. Zolang er nog iets ontbreekt, eindigt de run vroeg met Undefined symbol en krijgt het COMDAT-probleem geen kans om zichtbaar te worden. Vul je de laatste C-runtime-stub in, dan schuift de linker één fase op, recht naar internal error 200603061. Het symptoom in de praktijk is daardoor systematisch misleidend: wanneer je één voor één Pascal-bodies toevoegt voor de gebruikte C-symbolen, lijkt het steeds alsof de meest recente toevoeging de build heeft gebroken, of alsof er een grens rond honderd stubs is overschreden. Geen van beide is waar. Welk symbool als laatste werd toegevoegd en hoeveel het er in totaal waren, is allebei irrelevant, omdat de fout al in het eerste object latent aanwezig was en pas bereikbaar werd nadat de resolutie slaagde. Wanneer een linker na een ongerelateerde fix een andere klacht geeft, vraag dan of je een fase verder bent gekomen in plaats van een regressie te hebben veroorzaakt

De oplossing is één ObjConv-pass, geen compilerflag

De volledige correctie is één post-processingopdracht die op elk gecompileerd object wordt uitgevoerd: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. Drie van die opties zijn voor dit werk toegevoegd. -xw zet IMAGE_SYM_CLASS_WEAK_EXTERNAL-symbolen om in gewone externals. -xn normaliseert IMAGE_SYM_CLASS_NULL-symbolen zoals _fltused, die Free Pascal meldt als Unsupported COFF symbol type 0. -xc doet het zware werk: het maakt van elke COMDAT-sectie een gewone sectie en maakt de symbolen die daarin worden gedefinieerd static. Daarmee verdwijnt de fout doordat ook de beslissing verdwijnt: zonder COMDAT-secties is er geen folding, geen winnende kopie waarnaar de ene pass kan doorverwijzen en de andere kan missen, en verdwijnen de associative .pdata- en .xdata-unwindsecties mee. De prijs is reëel maar klein: kopieën die legitiem hadden kunnen worden samengevoegd, blijven nu elk bestaan

PDFlibPas bouwt zijn statisch gelinkte jbig2enc-backend voor Free Pascal door ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_ over elk MSVC-object te draaien, waarbij COMDAT-secties tot gewone secties worden gedegradeerd, weak externals worden opgelost, IMAGE_SYM_CLASS_NULL-symbolen worden genormaliseerd en importcellen worden hernoemd, waarna de gewone COFF64-objecten uit Lib\thirdparty\Win64f in de executable worden gelinkt zonder DLL om te deployen
Eén post-processingpass met ObjConv verwijdert de constructies die Free Pascal niet kan linken, zodat dezelfde unit PDFlibJBIG2EncC die Delphi en C++Builder bedient de encoder nu binnen de Free Pascal-executable zet zonder bestand om te deployen

De prefixhernoeming -np:__imp_:pdflibimp_ lost een aparte botsing op. MSVC roept geïmporteerde Win32-API's aan via indirectiecellen met namen als __imp_*, Free Pascal reserveert die prefix voor zijn eigen importmechanisme en een dergelijke naam rechtstreeks definiëren triggert dezelfde internal error 200603061. Door de cellen te hernoemen kan de Pascal-kant ze als gewone variabelen publiceren en tijdens runtime vullen. De objecten zelf worden gecompileerd met de static-link-vlaggen /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c- en met de image-codecs uitgeschakeld, zodat de dode file-I/O- en codec-paden veel minder link-only stubs nodig hebben. Ze komen in Lib\thirdparty\Win64f terecht, terwijl de Delphi- en C++Builder-route zijn eigen Win64x-set ongewijzigd blijft linken, precies de juiste uitkomst voor een portabiliteitsfix die tot één toolchain beperkt is

Wat de Pascal-kant nog steeds moet exporteren

Free Pascal lost de import van een C-object op via de symboolnaam en heeft die naam expliciet nodig, dus elke Pascal-routine die voor een C-entrypoint staat, krijgt een expliciete public name-clause. Delphi gebruikt de routinenaam als symboolnaam en heeft helemaal geen clause nodig. Daarom dient één unit beide compilers, met de clauses onder {$IFDEF FPC}. De valkuil is dat een external 'msvcrt.dll'-declaratie niets oplost: ze maakt een import aan, nooit een definitie waaraan een gelinkt object kan binden. De forwarding-body moet bestaan

Eén PDFlibPas-unit bedient Delphi, C++Builder en Free Pascal, omdat een external declaration alleen een import creëert terwijl een Pascal-body die onder de public name memcmp publiceert de definitie is waaraan de objectset bindt, en varargs-entrypoints als sprintf via SPrintfTarget tail-jumpen met het frame van de caller onaangeroerd in plaats van argumenten door te sturen
Een external declaration creëert een import waar niemand aan kan binden, een Pascal-body met een public name-clause is een echte definitie, en varargs-routines ontlopen het doorsturen volledig door te tail-jumpen met het frame van de caller onaangeroerd
// Een external-declaratie maakt alleen een import aan. Geen gelinkt object
// kan eraan binden.
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  external 'msvcrt.dll' name 'memcmp';

// Een Pascal-body die onder exact de C-symboolnaam wordt gepubliceerd, is
// waar de objectset daadwerkelijk aan bindt.
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  public name 'memcmp';
begin
  Result := crt_memcmp(Buf1, Buf2, Count);
end;

Variadische entrypoints doorbreken dat patroon, omdat een Pascal-wrapper zijn eigen varargs niet kan doorgeven aan een andere varargs-callee. De uitweg is ophouden een wrapper te zijn: exporteer een naked routine onder de C-naam en spring met een tail jump naar de echte implementatie, met de argumentregisters en de stack precies zoals de caller ze heeft klaargezet. De JPEG 2000-laag handelt snprintf en vsnprintf al zo af en springt naar de underscore-prefixvarianten van msvcrt, omdat alleen de UCRT de gewone namen exporteert. Een verwante beperking komt uit dezelfde internal error: de hernoemde importcellen worden vanuit een initialization-sectie gevuld via GetModuleHandleA en GetProcAddress in plaats van via static initializers, omdat het adres van een geïmporteerde routine in een initializer nemen de compiler een fixup laat genereren die hij niet kan verwerken, waarna opnieuw 200603061 volgt

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

var
  SPrintfTarget: Pointer = @crt_sprintf;

// Varargs kunnen niet vanuit een Pascal-wrapper worden doorgestuurd, dus het
// geëxporteerde symbool maakt een tail jump met het frame precies intact.
procedure jbig2_sprintf; assembler; nostackframe;
  public name 'sprintf';
asm
  jmp qword ptr [rip + SPrintfTarget]
end;

Wat doet een Free Pascal-project nu anders?

Niets behalve de unitnaam in de uses-clause, en er hoeft geen bestand meer te worden uitgerold. De backend registreert zichzelf vanuit zijn eigen initialization-sectie via RegisterJBIG2EncoderBackend, en callers vragen hem precies als voorheen op: via optiebit PDF_JBIG2_OPTION_EXTERNAL_ENCODER, met waarde 4, of via het argument UseExternalEncoder van de uitgebreide entrypoints. Opvragen blijft een voorkeur en geen garantie, want een build waarin de unit ontbreekt valt stil terug op de native Pascal MMR-encoder en produceert grotere bestanden in plaats van een fout

uses
  Classes, SysUtils,
  PDFlibrary,
  PDFlibJBIG2EncC;   // Delphi, C++Builder en vanaf 3.538.0 Free Pascal

var
  Pdf: TPDFlib;
  Scan: TStream;
  ImageId: Integer;
begin
  Pdf := TPDFlib.Create;
  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;

Twee grenzen moeten duidelijk worden genoemd. Er bestaat alleen een Win64-objectset, dus op elk ander Free Pascal-doel meldt het externe encode-entrypoint een fout en doet de native Pascal-encoder het werk. En de regressie die dit alles vrijgeeft is een rendervergelijking, geen groottecheck: beide encoders zijn lossless op dezelfde bron, dus hun uitvoer wordt gerenderd en byte voor byte vergeleken, waarbij de Lazarus-suite alle 26 tests doorloopt, inclusief deze. Gecomprimeerde streamgroottes vergelijken had niets bewezen, want een geïnverteerde pagina comprimeert ongeveer even groot als een correcte

De bredere les reikt verder dan JBIG2. Een DLL heeft de juiste vorm wanneer de grens echt dynamisch is, het geval waarvoor de DLL-, ActiveX- en dylib-integratieoppervlakken bestaan; het is de verkeerde vorm wanneer het alleen een workaround voor een COFF-reader is, omdat het aan elk installatieprogramma een bestand, aan elke deployment een zoekpad en aan elke uitrol een versieverschilrisico toevoegt dat statische linking niet kent. Ook wat eraan voorafgaat is belangrijk, want de manier waarop de binaire afbeelding wordt gemaakt bepaalt meer over de uiteindelijke grootte dan de encoder, en regiogebaseerde monochrome rendering in Delphi behandelt dat deel van de pipeline. De toolchain-dekking, de objectsets per compiler en de ondersteunde doelen staan op de losLab PDF Developer Library-productpagina