Az eredeti _ftol idióma a 32 bites Delphi-ben egy okos egysorosnak (one-liner) tűnik: egy Pascal függvényburkoló, amely beágyazott assembly-be (inline assembly) ugrik, hogy manipulálja az x87 FPU vezérlőszavát (control word), csonkolja az FPU veremén lévő értéket, és kivegye az eredményt. Hosszú ideig hiba nélkül lefordult a DCC32 alatt, pontosan ezért kötött ki olyan sok régebbi grafikus és PDF unitban anélkül, hogy bárki megkérdőjelezte volna
Ha átállítja a fordítási célt 64 bitesre, a fordító az E1025 Unsupported language feature: 'ASM' hibával leáll. Ez a hiba nem egy kompatibilitási figyelmeztetés. Azt jelenti, hogy a DCC64 egyáltalán nem fordítja le a rutint, függetlenül attól, hogy az assembly korábban milyen jól működött
A 32 bites eredeti általában valahogy így nézett ki:
function _ftol(f: Double): Integer; cdecl;
begin
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
end;
Ez a Pascal begin...end törzsön belüli asm blokk pontosan az, amit a DCC64 elutasít. A két fordítónak eltérő szabályai vannak arra vonatkozóan, hogy hol engedélyezett az assembly, és a határvonal számít
Miért húzza meg a határt máshol a DCC64
A DCC32 engedélyezi a beágyazott assembly-t (inline assembly) a normál Pascal rutinokon belül. A fordító ismeri a 32 bites hívási konvenciót, és képes kikövetkeztetni, hogy hol találhatók a helyi változók és paraméterek, így tolerálja azokat az assembly töredékeket, amelyek név szerint nyúlnak be a veremkeretbe (stack frame). A DCC64 szigorúbb álláspontot képvisel: az assembly-nek egy dedikált assembler függvényben kell lennie, ahol az egész törzs assembly, és a hívási konvenciót kifejezetten kezelik. A kevert Pascal-plusz-asm egyáltalán nem támogatott
A háttérben meghúzódó ok architekturális. A 64 bites Windows hívási konvencióban (Microsoft ABI) az első négy paraméter egész szám típusok esetén az RCX, RDX, R8 és R9 regiszterekben, míg lebegőpontos számok esetén az XMM0-tól XMM3-ig érkezik. A normál paraméterátadásban nincs x87 FPU részvétel; az x87 technikailag elérhető, de az ABI nem használja argumentumok továbbítására. Az az assembly, amely feltételezi, hogy egy érték „az FPU veremén” (on the FPU stack) van, egy olyan állapotról gondolkodik, amelyet a 64 bites ABI soha nem hoz létre
Tehát a régi kódtöredéknek nem csak szintaktikai problémája van. Még ha a DCC64 el is fogadná, a regiszterekre vonatkozó feltételezések tévesek lennének
Egy megfelelő 64 bites assembler verzió írása
Amikor valóban exportálnia kell egy _ftol szimbólumot cdecl konvencióval a bináris kompatibilitás miatt, a függvényt tisztán assembler rutinként kell megírni. A 64 bites ABI alatt egy Double paraméter az XMM0-ban érkezik, és az egész szám (integer) eredménynek az RAX-ban kell lennie a visszatéréskor. A .NOFRAME direktíva megmondja a DCC64-nek, hogy a rutin saját maga kezeli a vermét, ami megfelelő egy ilyen rövid levélfüggvényhez (leaf function):
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;
A CVTTSD2SI az az SSE2 utasítás, amely egy dupla pontosságú lebegőpontos számot alakít át előjeles egész számmá nulla felé történő csonkolással, ami pontosan az, amit az _ftol-nak csinálnia kell. Ez egyetlen utasítás, közvetlenül onnan veszi a paramétert, ahol az ABI hagyta, és oda helyezi az eredményt, ahol az ABI várja. Nincs szükség az FPU vezérlőszavával való zsonglőrködésre (FPU control-word juggling)
Vegye figyelembe, hogy ha a bemenet meghaladja egy 32 bites előjeles egész szám tartományát, a CVTTSD2SI az egész szám határozatlan (indefinite) értékét ($80000000) adja vissza. Ez ugyanaz a viselkedés, mint az x87 fistp esetében tartományon kívüli bemenetnél. Hogy a hívói (callers) képesek-e ilyen értékeket előállítani, érdemes megerősíteni, mielőtt a migrációt befejezettnek nyilvánítaná
Amikor a Trunc a jobb válasz
A fenti assembler verziót csak akkor érdemes megírni, ha tényleges bináris kompatibilitási követelménye van: valamilyen külső hívó (external caller) elvárja az _ftol szimbólumot egy adott hívási konvencióval, és nem tudja megváltoztatni ezeket a hívókat. Ez a helyzet nem gyakori. Legtöbbször az _ftol egy privát segítő (private helper) volt, amelyet csak ugyanabban a unitban használtak, és egyáltalán nincs külső függőség a nevétől vagy a konvenciójától
Ebben az esetben cserélje le sima (plain) Pascal-ra:
function _ftol(f: Double): Integer; cdecl;
begin
Result := Trunc(f);
end;
A Trunc nulla felé csonkol, ami megegyezik azzal, amit az _ftol csinált a csonkolási módra állított x87 vezérlőszóval (control word). DCC32-n és DCC64-en módosítás nélkül fordul. A fordító a megfelelő utasítást generálja minden egyes célpont számára: x64-en amúgy is jellemzően CVTTSD2SI-t fog kibocsátani (emit), ugyanazt az utasítást, mint a kézzel írott verzió. Azonos viselkedést kap, platformfüggő feltételek nélkül (no platform conditionals), és nem kell assembly-t karbantartania
Az egyetlen szemantikai különbség, amelyet érdemes ellenőrizni: a Trunc egy EInvalidOp kivételt dob a Delphi alapértelmezett konfigurációjában, ha a bemenet egy NaN vagy végtelen. Az x87 fistp az eredeti kódban csak egy bitmintát írt anélkül, hogy bármit is dobott volna. Ha az Ön kódja szokatlan lebegőpontos értékeket (floating-point values) táplál ebbe a függvénybe, és a régi viselkedés csendes volt, védekezzen a Math unit IsNaN és IsInfinite függvényeivel a Trunc meghívása előtt
Feltételes fordítás, ha mindkét célpont aktív marad
Egyes projekteknek továbbra is 32 bites és 64 bites binárisokat egyaránt kell szállítaniuk. Ha az eredeti assembler verziót meg kell tartani 32 bitre, és új implementációt kell biztosítani 64 bitre, használja a CPUX64 feltételt:
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;
Ez a minimális mechanikus javítás, és érdemes ideiglenesként kezelni. Egy olyan kódbázis, amely architektúra-specifikus assembly-t hordoz egy segítőben (helper), amelynek egyetlen célja a lebegőpontos-egész (float-to-integer) csonkolás, szükségtelen technikai adósságot (debt) cipel. A 32 bites elágazás (branch) teljesen eltűnhet, amint megerősíti, hogy semmi sem függ a régi implementáció FPU mellékhatásaitól (side-effects)
Ha a függvény több unitban használt komponensben jelenik meg, keressen rá az _ftol kifejezésre a teljes kódbázisban, mielőtt eldönti a migráció módját. Egy ilyen nevű szimbólum több helyen is deklarálható; a linker kiválaszt egyet, és csendben figyelmen kívül hagyja a többit, ami azt jelenti, hogy előfordulhat, hogy kijavít egy példányt, és mégis egy olyanhoz linkel (link against), amelyhez nem nyúltak