技术文章

将 Delphi _ftol 内联汇编迁移到 64 位构建

· Delphi 编程

较旧的 Delphi 代码有时会包含一个小型 _ftol 辅助函数,用于把浮点值转换为整数。在 32 位项目中,这类辅助函数常用内联汇编实现,尤其常见于为旧编译器调优过的图形和 PDF 代码。

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;

同一个单元编译为 64 位目标时,这种写法会失败。Delphi 64 位编译器会报告 E1025 Unsupported language feature: 'ASM',因为 DCC64 不支持在普通 Pascal 例程内部混合 Pascal 和内联汇编。

为什么 64 位编译器会拒绝

DCC64 可以汇编 64 位汇编例程,但例程必须作为完整的 assembler routine 编写,并遵守 64 位调用约定。不能把 32 位内联汇编复制到 Pascal 函数体里,指望编译器自动转换寄存器使用、栈布局或 x87 行为。

完整汇编版本是可能的,下面的代码展示了当值已经位于 FPU 栈上时,可能需要怎样处理控制字。

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;

只需要截断时优先使用 Pascal

对大多数应用代码来说,汇编例程并不必要。如果真实需求只是把 Double 向零截断并返回整数值,清晰且可移植的版本就是:

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

Trunc 直接表达意图,可同时编译到 32 位和 64 位目标,并避免维护架构相关汇编的成本。后续维护者也更容易理解范围检查和溢出行为。

范围和行为注意事项

Trunc 替换汇编仍应被视为需要测试的行为变更。请确认负数、接近整数边界的小数,以及超出 Integer 范围的值应该如何处理。旧汇编可能依赖 FPU 控制字状态,而 Pascal 版本遵循目标编译器的 RTL 实现。

如果该辅助函数位于 PDF 或图形组件中,请测试旋转文字、缩放坐标和变换后的绘制命令。这些区域比简单示例更容易暴露浮点到整数转换的细微差异。

推荐迁移路径

  • 如果只需要数值转换,优先使用清晰的 Pascal 实现。
  • 只有在二进制兼容性需要特定符号和调用约定时,才保留汇编导出。
  • 不要在没有审查调用约定和数据宽度的情况下复制 32 位寄存器代码。
  • 测试负数、大数以及超出目标整数范围的值。