Technical Article

Static Linking jbig2enc Into Free Pascal Without a DLL

PDFlibPas 3.538.0 links the external JBIG2 encoder statically into Free Pascal and Lazarus programs. A project adds the PDFlibJBIG2EncC unit, the same unit Delphi and C++Builder already use, and the encoder ends up inside the executable with nothing extra to ship beside it. That overturns the previous conclusion on this feature, which was that Free Pascal could reach the external encoder only through a DLL

Why did the DLL look like the only option?

The DLL looked like the only option because three linking routes failed in three unrelated ways and no compiler switch reached any of them. The internal linker rejects associative COMDAT sections outright. An external link through the bundled binutils crashes inside section garbage collection, which Free Pascal passes unconditionally on the 64-bit Windows target. A newer binutils cannot process the Free Pascal link script at all. Rebuilding the C++ side with the other toolchain trades one refusal for another, because template and inline instantiation emits weak external symbols by construction and Free Pascal reports those as Unsupported COFF symbol type 105. None of that evidence was wrong, and the earlier account of the JBIG2 encoder backends and the Free Pascal linker walks each dead end in a form that still reproduces today. What was wrong was the assumption about where a fix could live. Every attempt went through a compiler or a linker, and neither can change what an object file already contains. The object file was the problem the whole time. ObjConv reads COFF and writes COFF, and every construct Free Pascal chokes on has a mechanical equivalent it accepts

The error that never names its cause

Free Pascal's internal linker implements pick-any COMDAT only halfway, and that half-implementation is the single hardest thing to diagnose here. It does fold duplicate definitions, as the format intends. But TExeOutput.RemoveUnreferencedSections redirects through exesymbol to the winning definition when it marks sections as used, while TCoffexeoutput.DoRelocationFixup reads objreloc.symbol.objsection directly. When a used section references a symbol that its own object defines in a copy that lost the fold, the two passes are looking at different sections, and the link stops with Internal error 200603061

Compare that with the two limits on either side of it. Unsupported COFF symbol type 105 says weak external. Associative or exact match COMDAT sections are not yet supported says associative COMDAT and even names the offending symbol. Internal error 200603061 says nothing at all: no symbol name, no section name, no file name, no phase. It is also the normal case rather than a corner case, because MSVC puts every string literal and every inline or template instantiation into a pick-any COMDAT, and across the 186 objects in this encoder set the linker performed 2656 folds. Building with /Gy- keeps ordinary functions out of per-function COMDAT sections but leaves string literals and template instantiations exactly where they were

Why does stubbing out CRT symbols always look like the last one broke it?

Because the linker only reaches the fixup pass after every symbol has resolved. While anything is still missing the run ends early with Undefined symbol and the COMDAT problem never gets a chance to surface. Fill in the final C runtime stub and the linker advances one phase, straight into internal error 200603061. The field symptom is therefore systematically misleading: adding Pascal bodies for the referenced C symbols one at a time, it always looks like the most recent addition broke the build, or that some threshold around a hundred stubs has been crossed. Neither is true. Which symbol went in last and how many went in altogether are both irrelevant, because the failure was latent from the first object and only became reachable once resolution succeeded. When a linker changes its complaint after you fix something unrelated, ask whether you advanced a phase rather than caused a regression

The fix is one ObjConv pass, not a compiler flag

The whole correction is a single post-processing command run over each compiled object: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. Three of those options were added for this work. -xw resolves IMAGE_SYM_CLASS_WEAK_EXTERNAL symbols into ordinary externals. -xn normalizes IMAGE_SYM_CLASS_NULL symbols such as _fltused, which Free Pascal reports as Unsupported COFF symbol type 0. -xc does the heavy lifting: it demotes every COMDAT section to a plain section and makes the symbols it defines static. That removes the failure by removing the decision, since with no COMDAT sections there is no folding, no winning copy for one pass to redirect to and another to miss, and the associative .pdata and .xdata unwind sections go with it. The cost is real but small, in that copies which could legitimately have merged now each survive

The -np:__imp_:pdflibimp_ prefix rename solves a separate collision. MSVC calls imported Win32 APIs through indirection cells named __imp_*, Free Pascal reserves that prefix for its own import machinery, and defining one of those names outright trips the same internal error 200603061. Renaming the cells lets the Pascal side publish them as ordinary variables and fill them in at run time. The objects themselves are compiled with the static-link flag set, /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c-, and with the image codecs switched off, so the dead file-I/O and codec paths need far fewer link-only stubs. They land in Lib\thirdparty\Win64f, while the Delphi and C++Builder route keeps linking its own Win64x set untouched, which is the right outcome for a portability fix confined to one toolchain

What the Pascal side still has to export

Free Pascal resolves a C object's import by symbol name and needs that name spelled out, so every Pascal routine standing in for a C entry point carries an explicit public name clause. Delphi takes the routine name as the symbol name and needs no clause at all, which is why one unit serves both compilers with the clauses under {$IFDEF FPC}. The trap is that an external 'msvcrt.dll' declaration does not satisfy anything: it creates an import, never a definition a linked object can bind to. The forwarding body has to exist

// An external declaration creates an import only. No linked object can
// bind to it.
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  external 'msvcrt.dll' name 'memcmp';

// A Pascal body published under the exact C symbol name is what the
// object set actually binds to.
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  public name 'memcmp';
begin
  Result := crt_memcmp(Buf1, Buf2, Count);
end;

Variadic entry points break that pattern, because a Pascal wrapper cannot forward its own varargs to another varargs callee. The way out is to stop being a wrapper: export a naked routine under the C name and tail-jump to the real implementation with the argument registers and the stack exactly as the caller arranged them. The JPEG 2000 layer already handles snprintf and vsnprintf this way, jumping to the underscore-prefixed msvcrt spellings because the plain names are exported by the UCRT only. One related constraint comes from the same internal error: the renamed import cells are filled in from an initialization section through GetModuleHandleA and GetProcAddress rather than from static initializers, since taking the address of an imported routine in an initializer makes the compiler emit a fixup it cannot handle and fail with 200603061 again

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

var
  SPrintfTarget: Pointer = @crt_sprintf;

// Varargs cannot be forwarded from a Pascal wrapper, so the exported
// symbol tail-jumps with the frame left exactly as the caller set it up.
procedure jbig2_sprintf; assembler; nostackframe;
  public name 'sprintf';
asm
  jmp qword ptr [rip + SPrintfTarget]
end;

What a Free Pascal project does differently now

Nothing beyond the unit name in the uses clause, and there is no longer a file to deploy. The backend registers itself from its own initialization section through RegisterJBIG2EncoderBackend, and callers request it exactly as before: through options bit PDF_JBIG2_OPTION_EXTERNAL_ENCODER, which has the value 4, or through the UseExternalEncoder argument of the extended entry points. Requesting it stays a preference rather than a guarantee, since a build that leaves the unit out falls back to the native Pascal MMR encoder silently and produces larger files instead of an error

uses
  Classes, SysUtils,
  PDFlibrary,
  PDFlibJBIG2EncC;   // Delphi, C++Builder and, from 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;

Two limits are worth stating plainly. Only a Win64 object set exists, so on every other Free Pascal target the external encode entry point reports failure and the native Pascal encoder carries the work. And the regression that gates all of this is a render comparison rather than a size check: both encoders are lossless on the same source, so their output is rendered and compared byte for byte, with the Lazarus suite passing 26 of 26 including that test. Comparing compressed stream sizes would have proved nothing, because an inverted page compresses to roughly the same size as a correct one

The wider lesson generalizes past JBIG2. A DLL is the right shape when the boundary is genuinely dynamic, which is the case the DLL, ActiveX and dylib integration surfaces exist to serve; it is the wrong shape when it is only a workaround for a COFF reader, because it adds a file to every installer, a search path to every deployment and a version-skew failure mode static linking cannot have. Upstream matters too, since the way the bilevel image is produced decides more about final size than the encoder does, and region-based monochrome rendering in Delphi covers that half of the pipeline. Toolchain coverage, the per-compiler object sets and the supported targets are listed on the losLab PDF Developer Library product page