PDFlibPas 3.538.0 länkar den externa JBIG2-kodaren statiskt i Free Pascal- och Lazarus-program. Projektet lägger till enheten PDFlibJBIG2EncC, samma enhet som Delphi och C++Builder redan använder, och kodaren hamnar inne i den körbara filen utan något extra som måste levereras bredvid den. Det kullkastar den tidigare slutsatsen om funktionen, nämligen att Free Pascal bara kunde nå den externa kodaren via en DLL
Varför såg DLL ut som det enda alternativet?
DLL såg ut som det enda alternativet eftersom tre länkningsvägar misslyckades på tre oberoende sätt och ingen kompilatorväxel nådde någon av dem. Den interna länkaren avvisar associerande COMDAT-sektioner direkt. En extern länkning via de medföljande binutils kraschar under insamlingen av oanvända sektioner, som Free Pascal ovillkorligen skickar med för 64-bitars Windows-målet. Nyare binutils kan inte alls bearbeta Free Pascals länkskript. Att bygga om C++-delen med den andra verktygskedjan byter bara ett avslag mot ett annat, eftersom instansiering av mallar och inline-kod enligt konstruktionen avger svaga externa symboler och Free Pascal rapporterar dem som Unsupported COFF symbol type 105. Inget av det bevismaterialet var fel, och den tidigare genomgången av JBIG2-kodarens backends och Free Pascal-länkaren går igenom varje återvändsgränd i en form som fortfarande kan återskapas i dag. Det felaktiga var antagandet om var en lösning kunde ligga. Alla försök gick genom en kompilator eller en länkare, och ingen av dem kan ändra det som en objektfil redan innehåller. Objektfilen var problemet hela tiden. ObjConv läser COFF och skriver COFF, och varje konstruktion som Free Pascal snubblar över har en mekanisk motsvarighet som den accepterar
Felet som aldrig nämner sin orsak
Free Pascals interna länkare implementerar pick-any COMDAT bara till hälften, och den halvfärdiga implementeringen är det svåraste enskilda problemet att diagnostisera här. Den viker faktiskt ihop dubbla definitioner, precis som formatet avser. Men TExeOutput.RemoveUnreferencedSections omdirigerar via exesymbol till den vinnande definitionen när sektioner markeras som använda, medan TCoffexeoutput.DoRelocationFixup läser objreloc.symbol.objsection direkt. När en använd sektion refererar till en symbol som dess egen objektfil definierar i en kopia som förlorade vikningen tittar de två passen på olika sektioner, och länkningen stannar med Internal error 200603061
Jämför det med de två gränsfallen på varsin sida. Unsupported COFF symbol type 105 säger svag extern symbol. Associative or exact match COMDAT sections are not yet supported säger associerande COMDAT och namnger till och med symbolen som orsakar problemet. Internal error 200603061 säger ingenting alls: inget symbolnamn, inget sektionsnamn, inget filnamn, ingen fas. Det är dessutom normalfallet snarare än ett hörnfall, eftersom MSVC placerar varje strängliteral och varje inline- eller mallinstansiering i en pick-any COMDAT, och länkaren utförde 2656 vikningar över de 186 objekt som ingår i denna kodare. En byggning med /Gy- håller vanliga funktioner utanför COMDAT-sektioner per funktion, men lämnar strängliteraler och mallinstansieringar exakt där de var
Varför ser borttagna CRT-symboler alltid ut som om den sista förstörde bygget?
För att länkaren når fixup-passet först efter att varje symbol har lösts. Så länge något fortfarande saknas avslutas körningen tidigt med Undefined symbol och COMDAT-problemet får aldrig visa sig. Fyll i den sista stubben för C-runtime och länkaren går vidare en fas, rakt in i internal error 200603061. Det synliga symptomet blir därför systematiskt missvisande: när Pascal-kroppar för de refererade C-symbolerna läggs till en i taget ser det alltid ut som om det senaste tillägget förstörde bygget, eller som om någon tröskel runt hundra stubbar passerades. Inget av det stämmer. Vilken symbol som lades till sist och hur många som lades till totalt är båda irrelevanta, eftersom felet låg latent redan i den första objektfilen och först blev nåbart när upplösningen lyckades. När en länkare ändrar sitt klagomål efter att du har rättat något orelaterat, fråga om du avancerade en fas i stället för att ha orsakat en regression
Lösningen är ett ObjConv-pass, inte en kompilatorflagga
Hela korrigeringen är ett enda efterbehandlingskommando som körs på varje kompilerat objekt: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. Tre av dessa alternativ lades till för detta arbete. -xw löser symboler av typen IMAGE_SYM_CLASS_WEAK_EXTERNAL till vanliga externa symboler. -xn normaliserar symboler av typen IMAGE_SYM_CLASS_NULL, som _fltused, vilka Free Pascal rapporterar som Unsupported COFF symbol type 0. -xc gör det tunga arbetet: det nedgraderar varje COMDAT-sektion till en vanlig sektion och gör symbolerna den definierar statiska. Det tar bort felet genom att ta bort själva beslutet, eftersom inga COMDAT-sektioner innebär ingen vikning, ingen vinnande kopia som ett pass kan omdirigera till och ett annat missa, och de associerande unwind-sektionerna .pdata och .xdata försvinner samtidigt. Kostnaden är verklig men liten, eftersom kopior som legitimt kunde ha slagits ihop nu överlever var för sig
Prefixbytet -np:__imp_:pdflibimp_ löser en separat kollision. MSVC anropar importerade Win32-API:er via indirektionsceller med namn som __imp_*, Free Pascal reserverar det prefixet för sin egen importmekanism, och en direkt definition av ett sådant namn utlöser samma internal error 200603061. När cellerna får nya namn kan Pascal-sidan publicera dem som vanliga variabler och fylla dem under körning. Objekten själva kompileras med den statiska länkningsflagguppsättningen /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c- och med bildkodekarna avstängda, så de oanvända fil-I/O- och kodvägarna behöver betydligt färre stubbar som bara finns för länkningen. De hamnar i Lib\thirdparty\Win64f, medan Delphi- och C++Builder-vägen fortsätter att länka sin egen Win64x-uppsättning orörd, vilket är rätt resultat för en portabilitetsfix som är begränsad till en verktygskedja
Vad måste Pascal-sidan fortfarande exportera?
Free Pascal löser en C-objektsimport via symbolnamnet och behöver därför namnet utskrivet, så varje Pascal-rutin som ersätter en C-ingångspunkt har en uttrycklig public name-sats. Delphi tar rutinnamnet som symbolnamn och behöver ingen sådan sats alls, vilket är varför en enhet kan betjäna båda kompilatorerna med satserna under {$IFDEF FPC}. Fällan är att en deklaration som external 'msvcrt.dll' inte uppfyller någonting: den skapar en import, aldrig en definition som ett länkat objekt kan binda till. Vidarebefordringskroppen måste finnas
// En extern deklaration skapar bara en import. Inget länkat objekt kan
// binda till den.
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
external 'msvcrt.dll' name 'memcmp';
// En Pascal-kropp som publiceras under exakt rätt C-symbolnamn är det som
// objektuppsättningen faktiskt binder till.
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
public name 'memcmp';
begin
Result := crt_memcmp(Buf1, Buf2, Count);
end;
Variadiska ingångspunkter bryter det mönstret, eftersom en Pascal-wrapper inte kan vidarebefordra sina egna varargs till en annan varargs-anropad funktion. Vägen ut är att sluta vara en wrapper: exportera en naken rutin under C-namnet och gör ett tail-jump till den riktiga implementationen med argumentregistren och stacken exakt som anroparen ordnade dem. JPEG 2000-lagret hanterar redan snprintf och vsnprintf på detta sätt och hoppar till de understrukna msvcrt-namnen, eftersom de vanliga namnen endast exporteras av UCRT. En närliggande begränsning kommer från samma interna fel: de omdöpta importcellerna fylls från ett initialization-avsnitt via GetModuleHandleA och GetProcAddress i stället för från statiska initialiserare, eftersom kompilatorn annars avger en fixup den inte kan hantera när adressen till en importerad rutin tas i en initialiserare och misslyckas igen med 200603061
function crt_sprintf(Buffer: PAnsiChar; Fmt: PAnsiChar): Integer; cdecl; varargs;
external 'msvcrt.dll' name 'sprintf';
var
SPrintfTarget: Pointer = @crt_sprintf;
// Varargs kan inte vidarebefordras från en Pascal-wrapper, så den
// exporterade symbolen gör ett tail-jump med anroparens ram orörd.
procedure jbig2_sprintf; assembler; nostackframe;
public name 'sprintf';
asm
jmp qword ptr [rip + SPrintfTarget]
end;
Vad gör ett Free Pascal-projekt annorlunda nu?
Ingenting utöver enhetsnamnet i uses-satsen, och det finns inte längre någon fil att distribuera. Backenden registrerar sig själv från sitt eget initialization-avsnitt via RegisterJBIG2EncoderBackend, och anroparna begär den precis som tidigare: via alternativbiten PDF_JBIG2_OPTION_EXTERNAL_ENCODER, vars värde är 4, eller via argumentet UseExternalEncoder hos de utökade ingångspunkterna. Att begära den förblir en preferens snarare än en garanti, eftersom ett bygge som utelämnar enheten tyst faller tillbaka till den inbyggda Pascal-MMR-kodaren och ger större filer i stället för ett fel
uses
Classes, SysUtils,
PDFlibrary,
PDFlibJBIG2EncC; // Delphi, C++Builder och, från 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;
Två begränsningar bör sägas tydligt. Det finns bara en Win64-objektuppsättning, så på alla andra Free Pascal-mål rapporterar ingångspunkten för extern kodning ett fel och den inbyggda Pascal-kodaren tar över arbetet. Och regressionen som styr allt detta är en renderingsjämförelse, inte en storlekskontroll: båda kodarna är förlustfria på samma källa, så deras utdata renderas och jämförs byte för byte, och Lazarus-sviten klarade 26 av 26 inklusive det testet. Att jämföra storleken på komprimerade strömmar hade inte bevisat något, eftersom en inverterad sida komprimeras till ungefär samma storlek som en korrekt sida
Den bredare lärdomen gäller även utanför JBIG2. En DLL är rätt form när gränsen verkligen är dynamisk, vilket är fallet som integrationsytorna för DLL, ActiveX och dylib är till för; den är fel form när den bara är en tillfällig lösning för en COFF-läsare, eftersom den lägger till en fil i varje installationsprogram, en sökväg i varje distribution och ett versionsdriftsfel som statisk länkning inte kan ha. Det som kommer före spelar också roll, eftersom sättet den binära bilden produceras på avgör mer av den slutliga storleken än kodaren gör, och regionbaserad monokrom rendering i Delphi täcker den halvan av pipelinen. Verktygskedjetäckning, objektuppsättningarna per kompilator och de stödda målen listas på produktsidan för losLab PDF Developer Library