Оригиналният идиом _ftol в 32-битовия Delphi изглежда като умен едноредов код: обвивка (wrapper) на Pascal функция, която се впуска във вграден асемблер (inline assembly), за да манипулира контролната дума на x87 FPU, да съкрати (truncate) стойността в стека на FPU и да извади (pop) резултата. Той се изграждаше (built) добре под DCC32 дълго време, което е точно причината да се озове в толкова много по-стари графични и PDF модули (units), без никой да го подлага на съмнение
Превключете таргета за изграждане (build target) на 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 разрешава вграден асемблер (inline assembly) в обикновени Pascal рутини. Компилаторът познава 32-битовата конвенция за извикване (calling convention) и може да разсъждава относно това къде живеят локалните променливи и параметрите, така че толерира фрагменти от асемблер, които достигат до рамката на стека по име. DCC64 заема по-строга позиция: асемблерът трябва да бъде в специализирана асемблерна функция (assembler function), в която цялото тяло е на асемблер и конвенцията за извикване се обработва изрично. Смесен Pascal-плюс-asm изобщо не се поддържа
Основната причина е архитектурна. При 64-битовата Windows конвенция за извикване (Microsoft ABI), първите четири параметъра пристигат в RCX, RDX, R8 и R9 за целочислени типове, или в XMM0 до XMM3 за типове с плаваща запетая (floating-point). При нормално предаване на параметри няма участие на x87 FPU; x87 е технически достъпен, но ABI не го използва за транспорт на аргументи. Асемблер, който предполага, че дадена стойност е "в стека на FPU", разсъждава за състояние, което 64-битовият ABI никога не създава
Така че старият фрагмент няма само синтактичен проблем. Дори ако DCC64 го приемеше, предположенията за регистрите щяха да бъдат грешни
Написване на правилна 64-битова асемблерна версия
Когато наистина имате нужда да експортирате символ _ftol с конвенция cdecl заради двоична съвместимост, функцията трябва да бъде написана като чиста асемблерна рутина (pure assembler routine). Съгласно 64-битовия ABI, параметър Double пристига в XMM0, а целочисленият резултат трябва да бъде в RAX при връщане. Директивата .NOFRAME казва на DCC64, че рутината управлява свой собствен стек, което е подходящо за толкова кратка leaf функция:
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 инструкцията за конвертиране на число с плаваща запетая с двойна точност в цяло число със знак чрез съкращаване към нула (truncation toward zero), което е точно това, което _ftol се предполага, че прави. Това е една инструкция, взема параметъра директно от мястото, където ABI го е оставил, и поставя резултата там, където ABI го очаква. Не е необходимо жонглиране с контролни думи на FPU
Имайте предвид, че ако входът надвишава диапазона на 32-битово цяло число със знак, CVTTSD2SI връща цяло число с неопределена стойност ($80000000). Това е същото поведение като при x87 fistp при входни данни извън диапазона. Струва си да потвърдите дали вашите извикващи могат да произведат такива стойности, преди да обявите миграцията за приключена
Когато Trunc е по-добрият отговор
Горната асемблерна версия си струва да се напише само когато имате действително изискване за двоична съвместимост: някакъв външен извикващ (external caller) очаква символа _ftol със специфична конвенция за извикване и вие не можете да промените тези извикващи. Тази ситуация е необичайна. През повечето време, _ftol е бил частен помощник, използван само в рамките на същия модул (unit), и няма абсолютно никаква външна зависимост от неговото име или конвенция
За този случай го заменете с чист Pascal:
function _ftol(f: Double): Integer; cdecl;
begin
Result := Trunc(f);
end;
Trunc съкращава към нула, което съвпада с това, което _ftol правеше с контролната дума на x87, настроена на режим на съкращаване (truncation mode). То се компилира на DCC32 и DCC64 без модификации. Компилаторът генерира подходящата инструкция за всеки таргет: при x64 той така или иначе обикновено ще излъчи (emit) CVTTSD2SI, същата инструкция като в ръчно написаната версия. Получавате идентично поведение, никакви платформени условни конструкции и никакъв асемблер за поддръжка
Едната семантична разлика, която си струва да проверите: Trunc повдига изключение EInvalidOp в конфигурацията на Delphi по подразбиране, когато входът е NaN или безкрайност. x87 fistp в оригиналния код просто записваше битов модел (bit pattern), без да повдига нищо. Ако вашият код подава необичайни стойности с плаваща запетая към тази функция и старото поведение е било безшумно, защитете го с 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 от старата имплементация
Ако функцията се появява в компонент, използван в множество модули (units), претърсете цялата кодова база за _ftol, преди да решите как да мигрирате. Символ с това име може да бъде деклариран на повече от едно място; линкерът избира един и мълчаливо игнорира останалите, което означава, че можете да поправите едно копие и все още да се свързвате с друго, което не е било докоснато