Izvirni idiom _ftol v 32-bitnem Delphiju izgleda kot pametna enovrstičnica: Pascalov ovijalnik funkcije, ki uporabi vdelano zbirno kodo (inline assembly) za manipulacijo kontrolne besede x87 FPU, skrajša vrednost na skladu FPU in vrne rezultat. Pod DCC32 se je to dolgo časa prevajalo brez težav, zato se je znašlo v toliko starejših grafičnih in PDF enotah, ne da bi se kdo vprašal o njegovi ustreznosti.
Ko cilj gradnje preklopite na 64-bitnega, prevajalnik zaključi z napako E1025 Unsupported language feature: 'ASM'. Ta napaka ni le združljivostno opozorilo. Pomeni, da DCC64 rutine sploh ne bo prevedel, ne glede na to, kako dobro je zbirna koda prej delovala.
32-bitni izvirnik je običajno izgledal nekako takole:
function _ftol(f: Double): Integer; cdecl;
begin
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
end;
Ta blok asm znotraj telesa Pascal begin...end je natanko tisto, kar DCC64 zavrne. Prevajalnika imata različna pravila o tem, kje je zbirna koda dovoljena, ta meja pa je pomembna.
Zakaj DCC64 določa meje drugače
DCC32 dovoljuje vdelano zbirno kodo znotraj običajnih Pascalovih rutin. Prevajalnik pozna 32-bitno konvencijo klicanja in lahko sklepa o tem, kje se nahajajo lokalne spremenljivke in parametri, zato dopušča fragmente zbirne kode, ki po imenu dostopajo do okvirja sklada. DCC64 pa zavzema strožje stališče: zbirna koda mora biti v namenski zbirni funkciji (assembler function), kjer je celotno telo zbirni zapis in se konvencija klicanja obravnava eksplicitno. Mešani zapis Pascal + asm sploh ni podprt.
Glavni razlog za to je arhitekturni. V 64-bitni konvenciji klicanja Windows (Microsoft ABI) prvi štirje parametri prispejo v registrih RCX, RDX, R8 in R9 za celoštevilske tipe ali v XMM0 do XMM3 za tipe s plavajočo vejico. Pri običajnem prenosu parametrov x87 FPU ne sodeluje; x87 je sicer tehnično na voljo, vendar ga ABI ne uporablja za prenos argumentov. Zbirna koda, ki predvideva, da je vrednost "na skladu FPU", izhaja iz stanja, ki ga 64-bitni ABI nikoli ne ustvari.
Zato stari fragment nima le sintaktične težave. Tudi če bi ga DCC64 sprejel, bi bile predpostavke o registrih napačne.
Pisanje ustrezne 64-bitne zbirne različice
Kadar resnično potrebujete izvoz simbola _ftol s konvencijo cdecl za binarno združljivost, mora biti funkcija napisana kot čista zbirna rutina. V 64-bitnem ABI parameter Double prispe v XMM0, celoštevilski rezultat pa mora biti ob vrnitvi v RAX. Direktiva .NOFRAME sporoča DCC64, da rutina sama upravlja s svojim skladom, kar je primerno za tako kratko listno funkcijo (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 je ukaz SSE2 za pretvorbo števila s plavajočo vejico z dvojno natančnostjo (double-precision float) v predznačeno celo število z zaokroževanjem proti nič, kar je natanko tisto, kar naj bi počel _ftol. Gre za en sam ukaz, ki prevzame parameter neposredno tam, kjer ga je pustil ABI, in postavi rezultat tja, kjer ga ABI pričakuje. Nobenega usklajevanja kontrolnih besed FPU ni več potrebno.
Upoštevajte, da če vhod preseže obseg 32-bitnega predznačenega celega števila, CVTTSD2SI vrne nedoločeno celoštevilsko vrednost ($80000000). To je enako vedenje kot pri x87 fistp pri vhodu izven obsega. Preden zaključite selitev, preverite, ali lahko vaši klicatelji proizvedejo takšne vrednosti.
Kdaj je Trunc bolša rešitev
Zbirno različico zgoraj je vredno napisati le, ko imate dejansko zahtevo po binarni združljivosti: ko zunanji klicatelj pričakuje simbol _ftol z določeno konvencijo klicanja in teh klicateljev ne morete spremeniti. Ta primer je redek. Večinoma je bil _ftol zasebni pomožni element, uporabljen le znotraj iste enote, brez kakršne koli zunanje odvisnosti od njegovega imena ali konvencije.
V tem primeru ga nadomestite z navadno kodo Pascal:
function _ftol(f: Double): Integer; cdecl;
begin
Result := Trunc(f);
end;
Trunc zaokrožuje proti nič, kar ustreza delovanju _ftol s kontrolno besedo x87, nastavljeno na način rezanja (truncation mode). Brez sprememb se prevede tako na DCC32 kot na DCC64. Prevajalnik za vsak cilj ustvari ustrezen ukaz: na x64 običajno izda CVTTSD2SI, enak ukaz kot pri ročno napisani različici. Dobite enako vedenje, brez sistemskih pogojev in brez zbirne kode za vzdrževanje.
Ena semantična razlika, ki jo je vredno preveriti: Trunc sproži izjemo EInvalidOp v privzeti konfiguraciji Delphija, ko je vhod NaN (ni število) ali neskončnost. x87 fistp je v prvotni kodi le zapisal bitni vzorec brez sprožanja izjem. Če vaša koda tej funkciji posreduje nenavadne vrednosti s plavajočo vejico in je bilo prejšnje vedenje tiho, pred klicem Trunc uporabite zaščito z IsNaN in IsInfinite iz enote Math.
Pogojno prevajanje, ko sta aktivna oba cilja gradnje
Nekateri projekti morajo še naprej ponujati tako 32-bitne kot 64-bitne binarne datoteke. Če morate za 32-bitno obdržati izvirno zbirno različico, za 64-bitno pa zagotoviti novo izvedbo, uporabite pogojni stavek 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;
To je minimalen mehanski popravek in ga je pametno obravnavati kot začasnega. Koda, ki prenaša specifično zbirno kodo za arhitekturo v pomožniku, katerega edini namen je pretvorba plavajoče vejice v celo število, si nabira nepotreben tehnični dolg. 32-bitna veja se lahko popolnoma odstrani, ko potrdite, da nič ne temelji na stranskih učinkih FPU stare izvedbe.
Če se funkcija pojavi v komponenti, ki se uporablja v več enotah, pred odločitvijo o selitvi preiščite celotno kodo za simbolom _ftol. Simbol s tem imenom je lahko deklariran na več kot enem mestu; povezovalnik izbere enega in tiho prezre ostale, kar pomeni, da lahko popravite eno kopijo, a še vedno povezujete z drugo različico, ki je niste spremenili.