Оригинальная идиома _ftol в 32-битном Delphi существует из-за C-объектных файлов. Когда модуль линкует .obj-файлы, созданные 32-битным C-компилятором, эти объекты вызывают рантайм-помощник _ftol для каждого приведения float к целому, при этом значение остаётся на вершине стека x87 в ST(0), а не передаётся как параметр. Поэтому Delphi-замена объявляет фиктивный параметр Double, выталкивает ST(0) в него двухстрочным блоком встроенного ассемблера, а затем усекает его уже средствами Pascal. Под 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-битном ассемблере
Сначала проверьте, нужен ли 64-битный _ftol вообще. C-компиляторы Win64 — и MSVC, и Clang-цепочка C++Builder — конвертируют float в целое встроенными SSE2-инструкциями и не генерируют вызовов _ftol, поэтому 64-битные объектные файлы редко ссылаются на этот символ. Когда вам действительно нужно экспортировать символ _ftol с соглашением cdecl ради бинарной совместимости, функцию следует написать как чистую ассемблерную процедуру. В 64-битном ABI параметр Double приходит в XMM0, а результат Integer возвращается в EAX, младшей половине RAX. Директива .NOFRAME сообщает DCC64, что процедура сама управляет своим стеком, что подходит для столь короткой листовой функции:
function _ftol: Integer; cdecl;
// Значение Double ожидается в XMM0 по 64-битному ABI
asm
.NOFRAME
cvttsd2si eax, xmm0 // усечение до 32-битного целого, результат в eax
end;
CVTTSD2SI — это инструкция SSE2 для преобразования числа с плавающей запятой двойной точности в целое число со знаком с усечением к нулю, что именно и должна делать функция _ftol. Это одна инструкция, она берет параметр прямо оттуда, где его оставил ABI, и помещает результат туда, где его ожидает ABI. Жонглирование управляющим словом FPU не требуется
32-битный приёмник имеет значение. Если вход выходит за диапазон 32-битного целого со знаком, CVTTSD2SI с приёмником EAX возвращает значение integer indefinite ($80000000). Запись в RAX вместо этого дала бы 64-битную конвертацию, и входы между 231 и 263 возвращались бы теми самыми младшими 32 битами, которые оказались в них. Это то же поведение, что у x87 fistp на входе вне диапазона. Прежде чем объявлять миграцию завершённой, стоит убедиться, могут ли ваши вызывающие стороны порождать такие значения
Когда 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: DCC32 принимает inline asm
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
{$ENDIF}
end;
Это минимальное механическое исправление, и к нему стоит относиться как к временному. База кода, которая содержит специфичный для архитектуры ассемблер во вспомогательной функции, единственной целью которой является усечение числа с плавающей запятой до целого числа, несет ненужный технический долг. 32-битная ветка может полностью исчезнуть, как только вы подтвердите, что ничто не зависит от побочных эффектов FPU старой реализации
Если функция появляется в компоненте, используемом в нескольких модулях, найдите во всей базе кода символ _ftol, прежде чем решать, как выполнять миграцию. Символ с таким именем может быть объявлен более чем в одном месте; компоновщик выбирает один и молча игнорирует остальные, а это означает, что вы можете исправить одну копию и все равно скомпоновать другую, которая не была тронута