Technical Article

FPC Win32 OMF to COFF Object Linking in PDFlibPas

PDFlibPas builds under Free Pascal for 32-bit Windows, and the hard part was never Pascal. It was the object files: the AES and OpenJPEG objects the Delphi build links are OMF, the Free Pascal internal linker requires COFF, and converting between the two produces section names and section-definition symbols that make the linker fail with internal errors rather than diagnostics

Anyone who has linked C objects into a Pascal library knows this territory. Win64 is comparatively civil, with one object format, one calling convention and no name decoration. Win32 preserves every layer of history the platform accumulated, and a library that statically links third-party C code meets all of them at once

The compiler directory does not tell you the target

Start with the build entry point, because getting this wrong wastes hours before any object file is involved. A Free Pascal installation directory name identifies where the main compiler lives, not what it produces. A 32-bit host compiler can invoke a cross-compiler sitting beside it and emit 64-bit code when you pass the right target switches, so inferring the target from a path is guesswork that happens to work until someone reorganizes their toolchain

The reliable approach is to ask the compiler. Query the actual target processor and operating system through the compiler own information switches, and accept both common installation layouts, the flat binary directory and the version-nested one, because different installers and toolchain managers produce different shapes. A build script that hard-codes either layout works on exactly one machine

Why does a converted object file break the internal linker?

Because the conversion preserves the OMF section naming convention and synthesizes section-definition symbols that do not match what the COFF linker expects. Converting the OMF objects to COFF is necessary and not sufficient: the resulting files carry the classic _TEXT, _DATA and _BSS section names, plus section-definition symbol names derived from them, and feeding that to the Free Pascal internal linker produces internal compiler errors rather than a message about section naming

An internal error is the worst failure mode for a build problem, because it says nothing about what was wrong with the input. The fix is a post-conversion normalization pass over the COFF file: rewrite the section names to the expected form and rewrite the corresponding section-definition symbols to match, while leaving the symbol index, the code bytes and the relocations untouched. That last constraint is the entire difficulty. A rewrite that renumbers symbols or shifts offsets produces an object that links and then crashes

There is a preliminary step for one of the two object sets. The OpenJPEG objects built by the classic 32-bit C++ compiler depend on Delphi private 64-bit integer helper routines, which Free Pascal does not provide, so no amount of format conversion makes them usable. Those are rebuilt with the Clang-based compiler first, which does not emit those dependencies, and converted afterward

Pipeline carrying the PDFlibPas static C objects from Delphi OMF in Lib\thirdparty\Win32 to FPC-linkable COFF in Lib\thirdparty\Win32f on Win32: OMF to COFF conversion, a normalization pass that rewrites section names and section-definition symbols while leaving the symbol index, code bytes and relocations untouched, and the Clang rebuild for OpenJPEG objects that call Delphi 64-bit helpers
Conversion is necessary but not sufficient: feed the renamed-but-not-normalized COFF to the Free Pascal internal linker and it answers with internal errors, so a post-conversion pass fixes names and symbols without touching offsets
// Objects for the FPC target live in their own directory. They do not
// replace the Delphi object set, because both toolchains build from the
// same source tree and each needs its own link inputs
//
//   Lib\thirdparty\Win32   Delphi OMF objects, unchanged
//   Lib\thirdparty\Win32f  FPC COFF objects, converted and normalized
//
// Build entry points:
//   build-Win32-Lib-FPC.cmd
//   build-Win64-Lib-FPC.cmd

Compiler-private helpers are not portable, and neither are their conventions

The Delphi runtime supplies assembly trampolines for 64-bit integer operations on 32-bit x86, and precompiled C objects built for Delphi call into them. Free Pascal has its own arrangement, so those references have to be satisfied differently rather than redirected. The detail that makes redirection impossible is the calling convention: the timing helper used by the imaging code has its four-byte argument cleaned by the callee, while the 64-bit division helper cleans sixteen bytes and returns its result in the classic register pair. Two helpers, two conventions, and a trampoline written for one silently corrupts the stack for the other

Name decoration adds the second half of the problem. On Win32, Free Pascal prefixes external C imports with an underscore automatically while exporting public name declarations verbatim, so the import side and the export side of the same bridge follow different rules. The C runtime bridge that OpenJPEG needs therefore has to export the exact C symbol names, and the variadic entry points need a 32-bit indirect jump rather than a direct one. None of this is exotic once stated. All of it fails as a link error naming a symbol nobody wrote

What made a Win32 executable die before main?

A 64-bit DLL on the search path, reached because the Free Pascal zlib unit binds dynamically rather than linking statically. The symptom was an immediate exit with the invalid-image status code, before any Pascal code in the program ran, which sends you looking at the program you just built when the fault is in the loader resolving an import against the wrong architecture

The lesson is about assumptions rather than zlib. A unit named after a compression library does not necessarily contain one; it may be a binding that expects a shared library at run time, and a dynamic dependency you did not intend is a deployment liability even when it happens to resolve. Switching to the pure Pascal stream implementation gives both targets a statically included compression path with no external dependency at all, which is what a library embedded in someone else application should have in the first place

The same instinct applies to the external JBIG2 encoder backend. On the 32-bit target the external encoder is not linked, so requests fall back to the built-in Pascal encoder, and the test that verifies this has to check the registration state of the current target rather than treating a successful encode as proof the external backend is present. A fallback that works is exactly the thing that hides a missing dependency, which is the failure pattern examined in diagnosing silent stub failures. The 64-bit static linking work is covered in static linking jbig2enc under FPC

Diagnostic flow for a Win32 executable that exits before main under Free Pascal: the loader resolves imports while unit initialization runs, the zlib binding finds a 64-bit DLL on the search path, and the process dies with an invalid-image status before any Pascal statement, pushing PDFlibPas toward a statically included pure Pascal compression path
The fault was never the program just built: a unit named zlib was a runtime binding resolved against the wrong architecture, and a fallback that works, like the built-in JBIG2 encoder, hides a missing dependency

32-bit arithmetic on a memory stream

Code that manipulates buffer sizes with pointer-width unsigned arithmetic is correct on Win64 and one large image away from overflow on Win32. The in-memory stream that feeds the JPEG 2000 codec grows by doubling and advances by addition, and on a 32-bit target both operations can wrap on inputs that are large but entirely legitimate

Every write, skip, seek and initial allocation therefore checks before it computes, and the capacity ceiling is the maximum signed pointer-width value, chosen to match what the block move routine and the callback return values can express. The behavioral requirement when a request is refused is easy to get wrong: refusing must not change the stream position or its length. A partial mutation followed by an error leaves the stream in a state the caller cannot reason about, and the next operation compounds it

// Check before you compute. On Win32 both of these wrap on inputs a
// large JPEG 2000 image produces legitimately
if Needed > NativeUInt(High(NativeInt)) - FPosition then
  Exit(False);                    // refuse, leave position and size alone

NewCapacity := FCapacity;
while (NewCapacity < FPosition + Needed) do
begin
  if NewCapacity > NativeUInt(High(NativeInt)) shr 1 then
    Exit(False);                  // doubling would overflow
  NewCapacity := NewCapacity shl 1;
end;

Two build-output traps that outlive the port

Separating test and sample executables by target architecture into per-target output directories is obviously right and immediately breaks anything that located its test data by counting directory levels upward. The fix is to search upward for the asset directory rather than assume a fixed depth, with one deliberate restriction: the signing sample accepts a certificate fallback only from its own project directory, never from an arbitrary ancestor, because a same-named certificate found further up the tree is a security surprise rather than a convenience

The second trap survives every port and is worth carrying to any FPC project. After a compiler upgrade, having the compiler reject stale PPU files is not enough, because the linker still prefers leftover object files in the unit search path even when the PPU it loaded came from the correct directory, and adding an explicit object output path does not override that preference. The only reliable answer is a fresh temporary unit directory per build round. Anything less produces a binary linked from two compiler versions, which fails in ways that look like source bugs

Platform conditionals are the last piece, and choosing the right axis matters more than it appears. The right question is usually whether the code is Windows-specific rather than whether a particular widget library is present, as the metafile conversion work in EMF vector import and platform conditionals showed: switching that guard from a control-library condition to a platform condition turned a supposed rewrite into a change of one directive. Free Pascal and Lazarus support for both Windows targets ships with the PDFlibPas Delphi PDF library, built from the same sources as the Delphi and C++Builder packages