Оригинальная идиома _ftol в 32-битном Delphi выглядит как умное однострочное решение: обертка функции Pascal, которая использует встроенный ассемблер для управления управляющим словом x87 FPU, усечения значения в стеке FPU и извлечения результата. Она долгое время успешно компилировалась в DCC32, и именно поэтому оказалась во многих старых графических модулях и модулях PDF без каких-либо вопросов со стороны разработчиков
Переключите целевую платформу на 64-битную, и компилятор завершит работу с ошибкой E1025 Unsupported language feature: 'ASM'. Эта ошибка не является предупреждением о совместимости. Она означает, что DCC64 вообще не будет компилировать эту процедуру, независимо от того, насколько хорошо ассемблерный код работал раньше
32-битный оригинал обычно выглядел примерно так:
function _ftol(f: Double): Integer; cdecl;
begin
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
end;
Этот блок asm внутри тела begin...end на Pascal — именно то, что отклоняет DCC64. У двух компиляторов разные правила относительно того, где разрешен ассемблер, и эти границы имеют значение
Почему DCC64 проводит границу иначе
DCC32 разрешает встроенный ассемблер внутри обычных процедур Pascal. Компилятор знает 32-битное соглашение о вызове и может определить, где находятся локальные переменные и параметры, поэтому он допускает фрагменты ассемблера, которые обращаются к кадру стека по имени. DCC64 занимает более строгую позицию: ассемблер должен находиться в выделенной функции ассемблера, где все тело состоит из ассемблера, а соглашение о вызове обрабатывается явно. Смешанный код Pascal-плюс-asm не поддерживается вообще
Основная причина носит архитектурный характер. В 64-битном соглашении о вызове Windows (Microsoft ABI) первые четыре параметра передаются в RCX, RDX, R8 и R9 для целочисленных типов, или в XMM0 — XMM3 для чисел с плавающей запятой. При обычной передаче параметров x87 FPU не используется; технически x87 доступен, но ABI не использует его для транспортировки аргументов. Ассемблер, предполагающий, что значение находится «в стеке FPU», опирается на состояние, которое 64-битный ABI никогда не создает
Таким образом, старый фрагмент имеет проблему не только с синтаксисом. Даже если бы DCC64 принял его, предположения о регистрах были бы неверными
Написание правильной версии на 64-битном ассемблере
Если вам действительно нужно экспортировать символ _ftol с соглашением cdecl для двоичной совместимости, функция должна быть написана как процедура на чистом ассемблере. В 64-битном ABI параметр Double поступает в XMM0, а целочисленный результат должен быть возвращен в RAX. Директива .NOFRAME сообщает DCC64, что процедура управляет собственным стеком, что подходит для такой короткой листовой функции:
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 — это инструкция SSE2 для преобразования числа с плавающей запятой двойной точности в число со знаком с усечением к нулю, что именно и должна делать функция _ftol. Это одна инструкция, она берет параметр прямо оттуда, где его оставил ABI, и помещает результат туда, где его ожидает ABI. Жонглирование управляющим словом FPU не требуется
Обратите внимание, что если входные данные превышают диапазон 32-битного целого числа со знаком, CVTTSD2SI возвращает неопределенное целочисленное значение ($80000000). Это то же самое поведение, что и у fistp x87 при входных данных вне диапазона. Прежде чем объявлять миграцию завершенной, стоит убедиться, могут ли ваши вызывающие объекты создавать такие значения
Когда Trunc является лучшим решением
Приведенную выше версию на ассемблере стоит писать только тогда, когда у вас есть фактическое требование двоичной совместимости: какой-то внешний вызывающий объект ожидает символ _ftol с определенным соглашением о вызове, и вы не можете изменить эти вызывающие объекты. Такая ситуация встречается редко. В большинстве случаев _ftol был внутренним помощником, используемым только внутри того же модуля, и вообще не было никакой внешней зависимости от его имени или соглашения
В этом случае замените его обычным Pascal:
function _ftol(f: Double): Integer; cdecl;
begin
Result := Trunc(f);
end;
Trunc усекает к нулю, что соответствует тому, что делал _ftol, когда управляющее слово x87 было установлено в режим усечения. Он компилируется в DCC32 и DCC64 без изменений. Компилятор генерирует соответствующую инструкцию для каждой целевой платформы: на x64 он обычно все равно будет генерировать CVTTSD2SI, ту же инструкцию, что и в версии, написанной вручную. Вы получаете идентичное поведение, никаких условий для платформ и никакого ассемблера для поддержки
Одно семантическое отличие, которое стоит проверить: Trunc вызывает исключение EInvalidOp в конфигурации Delphi по умолчанию, когда входные данные являются NaN или бесконечностью. fistp x87 в оригинальном коде просто записывал битовый шаблон без вызова каких-либо исключений. Если ваш код передает в эту функцию необычные значения с плавающей запятой, а старое поведение было скрытым, используйте проверку IsNaN и IsInfinite из Math перед вызовом Trunc
Условная компиляция, когда обе платформы остаются активными
Некоторые проекты должны продолжать поставлять как 32-битные, так и 64-битные бинарные файлы. Если исходную версию ассемблера необходимо сохранить для 32-битной версии и предоставить новую реализацию для 64-битной, используйте условие CPUX64:
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;
Это минимальное механическое исправление, и к нему стоит относиться как к временному. База кода, которая содержит специфичный для архитектуры ассемблер во вспомогательной функции, единственной целью которой является усечение числа с плавающей запятой до целого числа, несет ненужный технический долг. 32-битная ветка может полностью исчезнуть, как только вы подтвердите, что ничто не зависит от побочных эффектов FPU старой реализации
Если функция появляется в компоненте, используемом в нескольких модулях, найдите во всей базе кода символ _ftol, прежде чем решать, как выполнять миграцию. Символ с таким именем может быть объявлен более чем в одном месте; компоновщик выбирает один и молча игнорирует остальные, а это означает, что вы можете исправить одну копию и все равно скомпоновать другую, которая не была тронута