Technical Article

Port Delphi _ftol inline assembly to 64-bit builds

· Delphi Programming

Older Delphi code sometimes included a small _ftol helper to convert a floating-point value to an integer. In 32-bit projects it was common to write this kind of helper with inline assembler, especially in graphics and PDF code that had been tuned for older compilers.

1
2
3
4
5
6
7
8
function _ftol( f: double) : Integer; cdecl;
begin
  asm
    lea   eax, f
    fstp  qword ptr [eax]
  end;
  result := Trunc(f);
end;

That approach breaks when the same unit is compiled for a 64-bit target. The Delphi 64-bit compiler reports E1025 Unsupported language feature: 'ASM' because DCC64 does not support mixed Pascal and inline assembly inside normal Pascal routines.

Why the 64-bit compiler rejects it

DCC64 can assemble 64-bit assembly routines, but the routine must be written as a complete assembler routine with the 64-bit calling convention in mind. You cannot copy 32-bit inline assembler into a Pascal function body and expect the compiler to translate register usage, stack layout, or x87 behaviour.

A full assembler version is possible, and code like the following shows the kind of control-word handling that may be required when the value is already on the FPU stack.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function _ftol: Integer; cdecl;
// Assumes double value is in FPU stack on entry
// Make a truncation to integer and put it into function result
var
  TmpVal: Int64;
  SaveCW, ScratchCW: word;
 
asm
  .NOFRAME
  fnstcw word ptr [SaveCW]
  fnstcw word ptr [ScratchCW]
  or word ptr [ScratchCW], 0F00h  ;// trunc toward zero, full precision
  fldcw word ptr [ScratchCW]
  fistp qword ptr [TmpVal]
  fldcw word ptr [SaveCW]
  mov rax, TmpVal
end;

Prefer Pascal when the goal is only truncation

For most application code, the assembler routine is unnecessary. If the real requirement is to truncate a Double toward zero and return an integer value, the clear and portable version is simply:

1
2
3
4
function _ftol(f: double): integer; cdecl;
begin
  Result := Trunc(f);
end;

Trunc expresses the intent directly, compiles for both 32-bit and 64-bit targets, and avoids the maintenance cost of architecture-specific assembler. It is also easier for future maintainers to reason about range checking and overflow behaviour.

Migration guidance

  • Use the Pascal implementation when you control the call site and only need numeric conversion.
  • Keep an assembler export only when binary compatibility requires a specific symbol and calling convention.
  • Do not copy 32-bit register code into a 64-bit routine without reviewing the calling convention and data width.
  • Test edge cases such as negative values, large values, and values outside the target integer range.

Range and behaviour considerations

Replacing assembler with Trunc should still be treated as a behaviour change that deserves tests. Confirm how your code should behave for negative numbers, fractions near an integer boundary, and values outside the range of Integer. The old assembler may have depended on FPU control-word state, while the Pascal version follows the RTL implementation for the target compiler.

If the helper sits inside a PDF or graphics component, test output that uses rotated text, scaled coordinates, and transformed drawing commands. Those areas are more likely to reveal subtle float-to-integer conversion differences than simple examples.

Recommended migration path

First, replace private conversion helpers with clear Pascal code where possible. Second, keep the original 32-bit implementation only behind a 32-bit conditional if it is still required. Third, add a separate 64-bit branch only when a measured performance or binary compatibility requirement justifies it. This keeps the portable path simple and makes the architecture-specific path an exception rather than the default.