Техническая статья

Перенос inline asm Delphi _ftol в 64-битные сборки

Старый код Delphi иногда содержал небольшой помощник _ftol для преобразования значения с плавающей точкой в целое. В 32-битных проектах такие помощники часто писали на inline assembler, особенно в графическом и 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-битной цели. 64-битный компилятор Delphi сообщает E1025 Unsupported language feature: 'ASM' потому что DCC64 не поддерживает смешивание Pascal и inline assembly внутри обычных Pascal-процедур.

Почему 64-битный компилятор отклоняет это

DCC64 может ассемблировать 64-битные assembly-процедуры, но процедура должна быть написана как полноценная assembler routine с учетом 64-битного соглашения о вызовах. Нельзя скопировать 32-битный inline assembler в тело Pascal-функции и ожидать, что компилятор переведет использование регистров, раскладку стека или поведение x87.

Полная assembler-версия возможна, и следующий код показывает обработку control-word, которая может понадобиться, когда значение уже находится в 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;

Предпочитайте Pascal, если нужна только усеченная конвертация

Для большинства прикладного кода assembler-процедура не нужна. Если реальное требование — усечь Double к нулю и вернуть целое значение, ясная и переносимая версия выглядит просто так:

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

Trunc прямо выражает намерение, компилируется для 32- и 64-битных целей и избегает затрат на сопровождение architecture-specific assembler. Будущим сопровождающим также проще рассуждать о проверке диапазона и переполнении.

Рекомендации по миграции

  • Используйте реализацию на Pascal, когда вы контролируете место вызова и нужна только числовая конвертация.
  • Оставляйте assembler export только тогда, когда двоичная совместимость требует конкретный символ и соглашение о вызовах.
  • Не копируйте 32-битный код регистров в 64-битную процедуру без проверки соглашения о вызовах и ширины данных.
  • Тестируйте пограничные случаи: отрицательные значения, большие значения и значения вне диапазона целевого целого типа.

Диапазон и поведение

Замена assembler на Trunc все равно должна считаться изменением поведения, требующим тестов. Проверьте, как код должен вести себя с отрицательными числами, дробями около целой границы и значениями вне диапазона Integer. Старый assembler мог зависеть от состояния FPU control-word, тогда как версия Pascal следует реализации RTL целевого компилятора.

Если помощник находится внутри PDF- или графического компонента, протестируйте вывод с повернутым текстом, масштабированными координатами и трансформированными командами рисования. Эти области чаще выявляют тонкие различия float-to-integer, чем простые примеры.

Рекомендуемый путь миграции

Сначала замените приватные помощники конвертации ясным кодом Pascal там, где это возможно. Затем оставьте исходную 32-битную реализацию только за 32-битным условием, если она все еще нужна. Наконец, добавляйте отдельную 64-битную ветку только при измеренном требовании производительности или двоичной совместимости. Так переносимый путь остается простым, а architecture-specific путь становится исключением, а не умолчанием.