Originali _ftol idioma 32 bitų „Delphi“ aplinkoje bei asamblerio kodu (inline assembly) manipuliavo x87 FPU valdymo žodžiu, nukirsdama reikšmę FPU steke ir išimdama rezultatą. Ji ilgą laiką sėkmingai susikompiliuodavo naudojant DCC32, todėl ir atsirado daugelyje senesnių grafikos bei PDF modulių niekam nekeliant klausimų.
Pakeitus konstravimo tikslą (build target) į 64 bitų, kompiliatorius nutraukia darbą su klaida E1025 Unsupported language feature: 'ASM'. Ši klaida nėra tik suderinamumo įspėjimas. Tai reiškia, kad DCC64 visiškai nekompiliuos šios procedūros, nepriklausomai nuo to, kaip gerai asamblerio kodas veikė anksčiau.
32 bitų originalas paprastai atrodė maždaug taip:
function _ftol(f: Double): Integer; cdecl;
begin
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
end;
Šis asm blokas „Pascal“ begin...end sekcijoje yra būtent tai, ko DCC64 nepriima. Abu kompiliatoriai taiko skirtingas taisykles dėl to, kur leidžiama naudoti asamblerį, ir ši riba yra svarbi.
Kodėl DCC64 nubrėžia ribą kitaip
DCC32 leidžia naudoti vidinį asamblerį įprastose „Pascal“ procedūrose. Kompiliatorius žino 32 bitų iškvietimo konvenciją ir gali nustatyti, kur yra vietiniai kintamieji ir parametrai, todėl jis toleruoja asamblerio fragmentus, kurie pasiekia steko kadrą (stack frame) pagal pavadinimą. DCC64 užima griežtesnę poziciją: asamblerio kodas privalo būti atskiroje asamblerio funkcijoje, kurioje visas kūnas yra asamblerio kalba, o iškvietimo konvencija valdoma aiškiai. Mišrus „Pascal“ ir asamblerio kodas išvis nepalaikomas.
Pagrindinė priežastis yra architektūrinė. Pagal 64 bitų „Windows“ iškvietimo konvenciją (Microsoft ABI), pirmi keturi parametrai perduodami registruose RCX, RDX, R8 ir R9 sveikojo skaičiaus tipams arba nuo XMM0 iki XMM3 slankiojo kablelio skaičiams. Įprastame parametrų perdavime x87 FPU nedalyvauja; x87 techniškai yra prieinamas, tačiau ABI nenaudoja jo argumentų perdavimui. Asamblerio kodas, darantis prielaidą, kad reikšmė yra „FPU steke“, remiasi būsena, kurios 64 bitų ABI niekada nesukuria.
Taigi, senasis fragmentas turi ne tik sintaksės problemą. Net jei DCC64 jį priimtų, prielaidos dėl registrų būtų klaidingos.
Tinkamos 64 bitų asamblerio versijos rašymas
Kai jums tikrai reikia eksportuoti _ftol simbolį su cdecl konvencija dėl dvejetainio suderinamumo, funkcija privalo būti parašyta kaip gryna asamblerio procedūra. Pagal 64 bitų ABI, Double parametras perduodamas registre XMM0, o gautas sveikasis skaičius grąžinant turi būti registre RAX. Direktyva .NOFRAME nurodo DCC64, kad procedūra pati valdo savo steką, o tai tinka tokiai trumpai galutinei (leaf) funkcijai:
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 yra SSE2 instrukcija, skirta dvigubo tikslumo slankiojo kablelio skaičių konvertuoti į pasirašytą sveikąjį skaičių su apvalinimu link nulio (nukirtimu) – tai yra būtent tai, ką turi atlikti _ftol. Tai viena instrukcija, kuri paima parametrą tiesiai iš ten, kur jį paliko ABI, ir patalpina rezultatą ten, kur ABI jo tikisi. Nereikia jokių manipuliacijų su FPU valdymo žodžiu.
Atkreipkite dėmesį, kad jei įvestis viršija 32 bitų pasirašyto sveikojo skaičiaus rėžius, CVTTSD2SI grąžina neapibrėžtą sveikojo skaičiaus reikšmę ($80000000). Tai yra toks pat elgesys kaip ir x87 instrukcijos fistp viršijus diapazoną. Prieš paskelbiant migravimą baigtu, verta įsitikinti, ar jūsų iškvietėjai gali pateikti tokias reikšmes.
Kada Trunc yra geresnis atsakymas
Aukščiau pateiktą asamblerio versiją verta rašyti tik tada, kai turite realų dvejetainio suderinamumo reikalavimą: kai koks nors išorinis iškvietėjas tikisi _ftol simbolio su konkrečia iškvietimo konvencija ir jūs negalite pakeisti šių iškvietėjų. Tokia situacija yra reta. Dažniausiai _ftol buvo privati pagalbinė funkcija, naudojama tik tame pačiame modulyje, ir jokios išorinės priklausomybės nuo jo pavadinimo ar konvencijos išvis nėra.
Tokiu atveju pakeiskite ją įprastu „Pascal“ kodu:
function _ftol(f: Double): Integer; cdecl;
begin
Result := Trunc(f);
end;
Trunc nukerta reikšmę link nulio, o tai atitinka tai, ką _ftol atliko su x87 valdymo žodžiu, nustatytu į nukirtimo režimą. Jis be pakeitimų kompiliuojasi ir su DCC32, ir su DCC64. Kompiliatorius sugeneruoja tinkamą instrukciją kiekvienam tikslui: x64 platformoje jis paprastai vis tiek sugeneruos CVTTSD2SI – tą pačią instrukciją, kaip ir ranka rašytoje versijoje. Gaunate identišką elgseną, išvengiate platformos sąlygų ir asamblerio kodo palaikymo.
Vienas semantinis skirtumas, kurį verta patikrinti: Trunc sukelia EInvalidOp išimtį numatytojoje „Delphi“ konfigūracijoje, kai įvestis yra NaN arba begalybė. x87 fistp instrukcija originaliame kode tiesiog įrašydavo bitų šabloną nesukeldama jokių išimčių. Jei jūsų kodas perduoda neįprastas slankiojo kablelio reikšmes į šią funkciją ir senasis elgesys buvo tylus, prieš kviesdami Trunc apsaugokite ją naudodami IsNaN ir IsInfinite iš Math modulio.
Sąlyginis kompiliavimas, kai abi versijos išlieka aktyvios
Kai kuriuose projektuose privaloma toliau leisti ir 32 bitų, ir 64 bitų programinę įrangą. Jei originali asamblerio versija turi būti išsaugota 32 bitų sistemoms, o nauja realizacija pateikta 64 bitų sistemoms, naudokite sąlygą 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;
Tai yra minimalus mechaninis sprendimas, kurį verta laikyti laikinu. Kodo bazė, kurioje saugoma konkrečios architektūros asamblerio kalba pagalbinėje funkcijoje, kurios vienintelis tikslas yra slankiojo kablelio skaičiaus nukirtimas iki sveikojo skaičiaus, turi nereikalingos techninės skolos. 32 bitų šakos galima visiškai atsisakyti įsitikinus, kad niekas nepriklauso nuo senosios realizacijos FPU šalutinių poveikių.
Jei funkcija naudojama keliuose moduliuose, prieš nuspręsdami, kaip migruoti, visoje kodo bazėje atlikite paiešką pagal _ftol. Šio pavadinimo simbolis gali būti deklaruotas keliose vietose; saistytojas (linker) parenka vieną ir tyliai ignoruoja kitus, o tai reiškia, kad galite ištaisyti vieną kopiją, tačiau vis tiek naudoti kitą, kuri nebuvo paliesta.