Technical Article

Free Pascal Win32: C Symbol Decoration in HotPDF

Free Pascal on Win32 adds a leading underscore to every cdecl; external import automatically, whilst public name exports the string you wrote, character for character. HotPDF has to satisfy both conventions in the same source tree, because the Delphi build already ships import declarations that spell the underscore out by hand. Getting this asymmetry wrong produces link errors that name a symbol nobody wrote

Extending a Delphi library to Free Pascal is usually described as a portability problem, and on Win64 it mostly is one. Win32 is different. The 32-bit x86 Windows ABI carries thirty years of accumulated convention about how C symbols are spelled, who cleans the stack, and which compiler-private helpers a translation unit is allowed to assume, and each of those is a place where two Pascal compilers that agree on the language can still disagree on the object file

Why does the same symbol resolve on Win64 and fail on Win32?

Because the underscore prefix is a 32-bit convention that Free Pascal applies to imports but not to exports. Declare function deflate(...): Integer; cdecl; external; and FPC looks for _deflate in the object file on Win32, and for deflate on Win64. That is correct behaviour and matches what a C compiler emits. The trap is on the other side of the bridge: a routine marked public name 'deflate' exports precisely deflate on both targets, with no prefix added

Now add the historical detail that makes it concrete. The Delphi build already declares some of these entry points with the underscore written into the name, because that is what its own object files contain. Feed the same declaration to FPC on Win32 and the compiler dutifully prefixes it again, so the linker hunts for __deflate, a symbol nothing exports. The intuitive fix, adding one underscore everywhere, breaks the imports that were already spelled correctly

What works is a pair of prefix constants rather than a single one. HPDFFPCZLib and HPDFFPCCodecStubs use one prefix for plain C imports and another for imports that already carry a Delphi-side prefix, and on Win64 both constants are empty so the existing link names survive untouched. Two constants instead of one is the entire fix, and it is only obvious once you have separated the import rule from the export rule

The same C symbol declarations resolved by Free Pascal and Delphi on Win64 and Win32: cdecl imports gain an underscore only on the 32-bit target, a Delphi declaration already spelled with an underscore becomes __deflate and fails to link, whilst public name exports stay literal on both architectures
One prefix constant cannot serve both rules: plain cdecl imports and imports already carrying the Delphi underscore decorate differently under FPC on Win32, so HotPDF keeps two and leaves both empty on Win64
// Two prefixes, not one: plain C imports and imports that already carry
// a hand-written Delphi prefix decorate differently under FPC/Win32
const
{$IF DEFINED(FPC) and DEFINED(CPU32)}
  CPrefix     = '_';   // FPC adds this itself for cdecl external
  DelphiCName = '';    // already spelled with the underscore in source
{$ELSE}
  CPrefix     = '';
  DelphiCName = '';
{$IFEND}

// Export side: 'public name' is literal on every target
procedure hpdf_codec_free(P: Pointer); cdecl;
  public name 'hpdf_codec_free';

WIN32 tells you the architecture, not the ABI

This is the conditional-compilation mistake with the longest debugging tail, and it is worth stating plainly: WIN32 and WIN64 describe the target architecture and say nothing about which compiler-private runtime helpers exist. Free Pascal defines both symbols on the corresponding Windows targets, exactly as Delphi does. A guard written as {$IFDEF WIN32} around code that calls a Delphi runtime helper therefore compiles under FPC and fails at link time

Concretely, three families of code fall into this trap. The Delphi 64-bit integer trampolines reached through System.@_ll helpers, the MSVC Win32 assembly support routines, and the import slots that go with them all exist to serve precompiled C objects that the Delphi build links. Free Pascal does not link those objects, so it needs none of that machinery, and every reference to it has to disappear. The subtlety is that both the declaration and the implementation have to be excluded together. Exclude only one and the compiler reports something unhelpful about an identifier it cannot match to anything

The rule that falls out is short. Guard on the compiler when the question is about ABI or runtime support, guard on the architecture when the question is about pointer width or register count, and never let one stand in for the other

Guarding declarations and implementations together

An interface-section conditional block is easy to fall into without noticing, and the resulting error message points anywhere but at the cause. Add a method declaration to a class interface and the natural place to put it is next to the related methods, which is fine right up to the moment those neighbours happen to sit inside an existing {$IFDEF} block. Conditional directives are not indented, so a block that opened forty lines above is essentially invisible whilst you are reading the surrounding declarations

What happens next is a compile that succeeds on one toolchain and produces a cascade on another. If the surrounding guard is a Delphi version check that Free Pascal does not satisfy, the declaration vanishes for FPC whilst the unconditional implementation remains, and the compiler reports a long list of complaints about method identifiers it expected and did not find. None of the messages mentions the conditional block that caused it

Two habits prevent the whole class of failure. Before inserting into an interface section, look upwards for the nearest open conditional rather than trusting the visual grouping. And treat a green Delphi test suite as evidence about Delphi only: the Free Pascal library build is a separate gate, and the only way to know it passes is to run build-Win32-Lib-FPC.cmd and build-Win64-Lib-FPC.cmd as part of the same change

What breaks in 32-bit arithmetic code

One language restriction shows up in exactly the code least willing to change: 32-bit Free Pascal will not accept a UInt64 as a for loop control variable. In the elliptic-curve units that carry X25519 and X448, the loops that walk limb arrays were written with 64-bit counters simply because everything else in the file is 64-bit

The fix has to be surgical, because in field arithmetic the width of a variable is part of the correctness argument. Loop indices become Integer, since a limb array has a handful of elements and no index ever approaches the 32-bit range. Everything that participates in the arithmetic, the limbs themselves, the carry propagation and the masks, stays UInt64, because narrowing any of those silently changes the result modulo the field prime

// 32-bit FPC rejects a UInt64 loop variable. Narrow the index only;
// limbs, masks and carries keep their width or the field maths changes
var
  I: Integer;                 // was UInt64
  Carry, Mask: UInt64;
begin
  Carry := 0;
  for I := 0 to High(Limbs) do
  begin
    Limbs[I] := Limbs[I] + Carry;
    Carry := Limbs[I] shr 51;
    Limbs[I] := Limbs[I] and Mask;
  end;
end;

The verification for a change like this cannot be a round-trip test. Encrypting and decrypting with the same broken implementation agrees with itself perfectly, which is why known-answer vectors are non-negotiable here: run the published X25519 and X448 test vectors and compare the exact output bytes. That is the only check that distinguishes a correct implementation from a self-consistent wrong one, and it applies equally to the symmetric primitives discussed in the Free Pascal deflate and AES codec boundaries

The two break points of a HotPDF Win32 Free Pascal build: an {$IFDEF WIN32} guard around Delphi runtime helpers that compiles but fails at link unless declaration and implementation are excluded together, and the UInt64 loop variable in X25519 and X448 limb walks narrowed to Integer whilst limbs, carries and masks keep their width
Guard on the compiler when the question is ABI or runtime support and on the architecture when it is pointer width, then prove arithmetic changes against published known-answer vectors rather than round-trip tests

What a Win32 Free Pascal build is worth

The practical payoff is that a Lazarus application targeting 32-bit Windows gets the same document engine as its Delphi counterpart, without a separate binary contract to maintain. That matters most for the deployments people rarely talk about: industrial controllers, point-of-sale terminals and long-lived line-of-business software where the 32-bit runtime is not a legacy choice but a hardware constraint

The Win64 story came first and is described in Free Pascal and Lazarus support on Win64. Win32 is not a re-run of it. Win64 has one calling convention, no name decoration and no Delphi-private integer helpers to work around, so almost everything in this article is specific to the 32-bit target. The arithmetic units that needed the loop-variable change are the same ones described in Montgomery arithmetic over the NIST curves, where the width discipline is explained in more depth

The general lesson is that cross-compiler portability work is not primarily about language features. Both compilers accept the same Object Pascal here. What differs is the object file: how symbols are spelled, which helper routines the runtime is assumed to provide, and which precompiled objects are in the link. HotPDF ships the Free Pascal and Lazarus packages alongside the Delphi and C++Builder ones in the HotPDF Delphi PDF component, so the same source tree feeds every toolchain rather than forking per compiler