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
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
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
// 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(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;
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