Technical Article

Porting _ftol Inline Assembly from 32-bit Delphi to DCC64

The original _ftol idiom in 32-bit Delphi looks like a clever one-liner: a Pascal function wrapper that drops into inline assembly to manipulate the x87 FPU control word, truncate the value on the FPU stack, and pop the result. It built fine under DCC32 for a long time, which is exactly why it ended up in so many older graphics and PDF units without anyone questioning it.

Switch the build target to 64-bit and the compiler terminates with E1025 Unsupported language feature: 'ASM'. That error is not a compatibility warning. It means DCC64 will not compile the routine at all, regardless of how well the assembly worked before.

The 32-bit original typically looked something like this:

function _ftol(f: Double): Integer; cdecl;
begin
  asm
    lea   eax, f
    fstp  qword ptr [eax]
  end;
  Result := Trunc(f);
end;

That asm block inside a Pascal begin...end body is exactly what DCC64 refuses. The two compilers have different rules about where assembly is allowed, and the boundary matters.

Why DCC64 draws the line differently

DCC32 permits inline assembly inside ordinary Pascal routines. The compiler knows the 32-bit calling convention and can reason about where local variables and parameters live, so it tolerates assembly fragments that reach into the stack frame by name. DCC64 takes a stricter position: assembly must be in a dedicated assembler function, one where the entire body is assembly and the calling convention is handled explicitly. Mixed Pascal-plus-asm is not supported at all.

The underlying reason is architectural. In the 64-bit Windows calling convention (Microsoft ABI), the first four parameters arrive in RCX, RDX, R8, and R9 for integer types, or in XMM0 through XMM3 for floating-point. There is no x87 FPU involvement in normal parameter passing; x87 is technically available but the ABI does not use it for argument transport. Assembly that assumes a value is "on the FPU stack" is reasoning about a state that the 64-bit ABI never creates.

So the old fragment does not just have a syntax problem. Even if DCC64 accepted it, the register assumptions would be wrong.

Writing a proper 64-bit assembler version

When you genuinely need to export a _ftol symbol with cdecl convention for binary compatibility, the function must be written as a pure assembler routine. Under the 64-bit ABI a Double parameter arrives in XMM0, and the integer result must be in RAX on return. The .NOFRAME directive tells DCC64 that the routine manages its own stack, which is appropriate for a leaf function this short:

function _ftol: Integer; cdecl;
// Double value expected in XMM0 per 64-bit ABI
asm
  .NOFRAME
  cvttsd2si  rax, xmm0   // truncate-to-integer, result in rax
end;

CVTTSD2SI is the SSE2 instruction for converting a double-precision float to a signed integer with truncation toward zero, which is exactly what _ftol is supposed to do. It is one instruction, takes the parameter directly from where the ABI left it, and places the result where the ABI expects it. No FPU control-word juggling needed.

Note that if the input exceeds the range of a 32-bit signed integer, CVTTSD2SI returns the integer indefinite value ($80000000). That is the same behavior as the x87 fistp on out-of-range input. Whether your callers can produce such values is worth confirming before declaring the migration done.

When Trunc is the better answer

The assembler version above is only worth writing when you have an actual binary compatibility requirement: some external caller expects the symbol _ftol with a specific calling convention, and you cannot change those callers. That situation is uncommon. Most of the time, _ftol was a private helper used only within the same unit, and there is no external dependency on its name or convention at all.

For that case, replace it with plain Pascal:

function _ftol(f: Double): Integer; cdecl;
begin
  Result := Trunc(f);
end;

Trunc truncates toward zero, which matches what _ftol was doing with the x87 control word set to truncation mode. It compiles on DCC32 and DCC64 without modification. The compiler generates the appropriate instruction for each target: on x64 it will typically emit CVTTSD2SI anyway, the same instruction as the hand-written version. You get identical behavior, no platform conditionals, and no assembly to maintain.

The one semantic difference worth checking: Trunc raises an EInvalidOp exception in Delphi's default configuration when the input is a NaN or infinity. The x87 fistp in the original code just wrote a bit pattern without raising anything. If your code feeds unusual floating-point values into this function and the old behavior was silent, guard with IsNaN and IsInfinite from Math before calling Trunc.

Conditional compilation when both targets remain active

Some projects must continue shipping both 32-bit and 64-bit binaries. If the original assembler version must be kept for 32-bit and a new implementation provided for 64-bit, use the CPUX64 conditional:

function _ftol(f: Double): Integer; cdecl;
begin
{$IFDEF CPUX64}
  Result := Trunc(f);
{$ELSE}
  // 32-bit path: DCC32 accepts inline asm
  asm
    lea   eax, f
    fstp  qword ptr [eax]
  end;
  Result := Trunc(f);
{$ENDIF}
end;

That is the minimum mechanical fix, and it is worth treating as temporary. A codebase that carries architecture-specific assembly in a helper whose sole purpose is float-to-integer truncation is carrying unnecessary debt. The 32-bit branch can go away entirely once you confirm nothing depends on the FPU side-effects of the old implementation.

If the function appears in a component used across multiple units, search the whole codebase for _ftol before deciding how to migrate. A symbol by that name can be declared in more than one place; the linker picks one and silently ignores the others, which means you might fix one copy and still link against a different one that has not been touched.