Alkuperäinen _ftol-idiomi 32-bittisessä Delphissä näyttää fiksulta yhden rivin ratkaisulta: Pascal-funktiokääre, joka pudottautuu sisäiseen assemblyyn manipuloidakseen x87 FPU:n ohjaussanaa, typistääkseen (truncate) FPU-pinossa olevan arvon ja nostaakseen tuloksen pinosta. Se kääntyi ongelmitta DCC32:lla pitkään, mikä on juuri syy siihen, miksi se päätyi niin moniin vanhempiin grafiikka- ja PDF-yksiköihin kenenkään kyseenalaistamatta sitä
Vaihda koontikohteeksi (build target) 64-bittinen ja kääntäjä keskeyttää toimintansa virheeseen E1025 Unsupported language feature: 'ASM'. Tämä virhe ei ole yhteensopivuusvaroitus. Se tarkoittaa, että DCC64 ei käännä rutiinia ollenkaan, riippumatta siitä, kuinka hyvin assembly toimi aikaisemmin
32-bittinen alkuperäisversio näytti tyypillisesti suunnilleen tältä:
function _ftol(f: Double): Integer; cdecl;
begin
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
end;
Tuo asm-lohko Pascalin begin...end-rungoissa on juuri se, mistä DCC64 kieltäytyy. Näillä kahdella kääntäjällä on erilaiset säännöt siitä, missä assembly on sallittua, ja rajalla on merkitystä
Miksi DCC64 vetää rajan eri tavalla
DCC32 sallii sisäisen assemblyn tavallisten Pascal-rutiinien sisällä. Kääntäjä tuntee 32-bittisen kutsukäytännön (calling convention) ja osaa päätellä, missä paikalliset muuttujat ja parametrit sijaitsevat, joten se sietää assembly-fragmentteja, jotka ulottuvat pinokehykseen (stack frame) nimen perusteella. DCC64 ottaa tiukemman kannan: assemblyn on oltava sille omistetussa assembler-funktiossa, jossa koko runko on assemblyä ja kutsukäytäntöä käsitellään nimenomaisesti. Sekoitettua Pascal-plus-asm-koodia ei tueta ollenkaan
Taustalla oleva syy on arkkitehtuurinen. 64-bittisessä Windowsin kutsukäytännössä (Microsoft ABI) neljä ensimmäistä parametria saapuvat rekistereissä RCX, RDX, R8 ja R9 kokonaislukutyypeille tai rekistereissä XMM0–XMM3 liukuluvuille. x87 FPU ei ole mukana normaalissa parametrien välittämisessä; x87 on teknisesti saatavilla, mutta ABI ei käytä sitä argumenttien kuljettamiseen. Assembly, joka olettaa arvon olevan "FPU-pinossa", päättelee tilasta, jota 64-bittinen ABI ei koskaan luo
Joten vanhalla fragmentilla ei ole vain syntaksiongelmaa. Vaikka DCC64 hyväksyisi sen, rekisterioletukset olisivat vääriä
Kunnollisen 64-bittisen assembler-version kirjoittaminen
Kun sinun todella täytyy viedä _ftol-symboli cdecl-käytännöllä binääriyhteensopivuutta varten, funktio on kirjoitettava puhtaana assembler-rutiinina. 64-bittisen ABI:n alaisuudessa Double-parametri saapuu rekisterissä XMM0, ja kokonaislukutuloksen on oltava rekisterissä RAX palattaessa. Direktiivi .NOFRAME kertoo DCC64:lle, että rutiini hallitsee omaa pinoaan, mikä on asianmukaista näin lyhyelle lehtifunktiolle (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;
CVTTSD2SI on SSE2-käsky, jolla kaksinkertaisen tarkkuuden liukuluku (double-precision float) muunnetaan etumerkilliseksi kokonaisluvuksi typistämällä nollaa kohti, mikä on juuri sitä, mitä _ftol:n oletetaan tekevän. Se on yksi käsky, joka ottaa parametrin suoraan sieltä, mihin ABI sen jätti, ja sijoittaa tuloksen sinne, missä ABI sitä odottaa. FPU:n ohjaussanan jongleerausta ei tarvita
Huomaa, että jos syöte ylittää 32-bittisen etumerkillisen kokonaisluvun alueen, CVTTSD2SI palauttaa kokonaisluvun epämääräisen arvon ($80000000). Tämä on sama käyttäytyminen kuin x87:n fistp:llä alueen ylittävällä syötteellä. On syytä vahvistaa, voivatko kutsujasi tuottaa tällaisia arvoja, ennen kuin julistat siirtymisen valmiiksi
Milloin Trunc on parempi vastaus
Yllä oleva assembler-versio kannattaa kirjoittaa vain silloin, kun sinulla on todellinen binääriyhteensopivuusvaatimus: jokin ulkoinen kutsuja odottaa symbolia _ftol tietyllä kutsukäytännöllä, etkä voi muuttaa noita kutsujia. Tämä tilanne on harvinainen. Useimmiten _ftol oli yksityinen apufunktio, jota käytettiin vain saman yksikön sisällä, eikä sen nimeen tai käytäntöön ole lainkaan ulkoista riippuvuutta
Tässä tapauksessa korvaa se tavallisella Pascalilla:
function _ftol(f: Double): Integer; cdecl;
begin
Result := Trunc(f);
end;
Trunc typistää nollaa kohti, mikä vastaa sitä, mitä _ftol teki, kun x87:n ohjaussana oli asetettu typistystilaan. Se kääntyy DCC32:lla ja DCC64:llä ilman muutoksia. Kääntäjä generoi asianmukaisen käskyn kummallekin kohteelle: x64:llä se lähettää tyypillisesti joka tapauksessa käskyn CVTTSD2SI, saman käskyn kuin käsinkirjoitettu versio. Saat identtisen käyttäytymisen, et tarvitse alusta-ehtolausekkeita eikä ylläpidettävää assemblyä ole
Yksi tarkistamisen arvoinen semanttinen ero: Trunc laukaisee EInvalidOp-poikkeuksen Delphin oletuskokoonpanossa, kun syöte on NaN (Not a Number) tai ääretön. Alkuperäisen koodin x87 fistp vain kirjoitti bittikuvion laukaisematta mitään. Jos koodisi syöttää epätavallisia liukulukuarvoja tähän funktioon ja vanha käyttäytyminen oli hiljainen, suojaa funktio käyttämällä Math-yksikön funktioita IsNaN ja IsInfinite ennen Trunc-kutsua
Ehdollinen kääntäminen, kun molemmat kohteet pysyvät aktiivisina
Joidenkin projektien on jatkettava sekä 32-bittisten että 64-bittisten binäärien toimittamista. Jos alkuperäinen assembler-versio on säilytettävä 32-bittistä varten ja 64-bittistä varten on tarjottava uusi toteutus, käytä ehtolauseketta 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;
Tämä on pienin mekaaninen korjaus, ja sitä kannattaa käsitellä väliaikaisena. Koodikanta, joka kantaa arkkitehtuurikohtaista assemblyä apufunktiossa, jonka ainoa tarkoitus on liukuluvun typistäminen kokonaisluvuksi, kantaa tarpeetonta velkaa. 32-bittinen haara voi poistua kokonaan, kun olet vahvistanut, ettei mikään riipu vanhan toteutuksen FPU-sivuvaikutuksista
Jos funktio esiintyy komponentissa, jota käytetään useissa yksiköissä, hae koko koodikannasta sanaa _ftol ennen kuin päätät, miten siirryt. Tuon niminen symboli voidaan esitellä useammassa kuin yhdessä paikassa; linkittäjä valitsee yhden ja jättää muut hiljaisesti huomiotta, mikä tarkoittaa, että saatat korjata yhden kopion ja silti linkittää toiseen, johon ei ole koskettu