Izvorni izraz _ftol u 32-bitnom Delphiju izgleda kao domišljata linija koda: Pascal omot funkcije (wrapper) koji prelazi u ugrađeni asembler (inline assembly) kako bi manipulirao kontrolnom riječi x87 FPU-a, skratio (truncate) vrijednost na FPU stogu i preuzeo (pop) rezultat. Dugo se uspješno kompajlirao pod DCC32, što je upravo razlog zašto je završio u toliko mnogo starijih grafičkih i PDF jedinica, a da ga nitko nije preispitivao.
Prebacite ciljnu platformu na 64-bitnu i kompajler će se prekinuti s pogreškom E1025 Unsupported language feature: 'ASM'. Ta pogreška nije samo upozorenje o kompatibilnosti. Ona znači da DCC64 uopće neće kompajlirati rutinu, bez obzira na to koliko je asembler prije dobro radio.
Izvorna 32-bitna verzija obično je izgledala otprilike ovako:
function _ftol(f: Double): Integer; cdecl;
begin
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
end;
Taj asm blok unutar Pascalova begin...end tijela je upravo ono što DCC64 odbija. Ova dva kompajlera imaju različita pravila o tome gdje je asembler dopušten, a ta je granica važna.
Zašto DCC64 drugačije postavlja granice
DCC32 dopušta ugrađeni asembler unutar običnih Pascal rutina. Kompajler poznaje 32-bitnu konvenciju pozivanja i može odrediti gdje se nalaze lokalne varijable i parametri, pa tolerira dijelove asemblera koji po imenu pristupaju okviru stoga (stack frame). DCC64 zauzima stroži stav: asembler mora biti u zasebnoj asemblerskoj funkciji, onoj u kojoj je cijelo tijelo napisano u asembleru, a konvencija pozivanja se rješava eksplicitno. Mješavina Pascala i ugrađenog asemblera uopće nije podržana.
Temeljni razlog je arhitektonski. U 64-bitnoj Windows konvenciji pozivanja (Microsoft ABI), prva četiri parametra dolaze u registrima RCX, RDX, R8 i R9 za cjelobrojne tipove, ili u XMM0 do XMM3 za brojeve s pomičnim zarezom. U uobičajenom prijenosu parametara nema sudjelovanja x87 FPU-a; x87 je tehnički dostupan, ali ABI ga ne koristi za prijenos argumenata. Asembler koji pretpostavlja da je vrijednost 'na FPU stogu' oslanja se na stanje koje 64-bitni ABI nikada ne stvara.
Dakle, stari isječak nema samo sintaktički problem. Čak i da ga DCC64 prihvati, pretpostavke o registrima bile bi pogrešne.
Pisanje ispravne 64-bitne asemblerske verzije
Kada stvarno trebate izvesti simbol _ftol s konvencijom cdecl radi binarne kompatibilnosti, funkcija mora biti napisana kao čista asemblerska rutina. Pod 64-bitnim ABI-jem parametar tipa Double dolazi u XMM0, a cjelobrojni rezultat mora biti u RAX prilikom povratka. Direktiva .NOFRAME govori kompajleru DCC64 da rutina sama upravlja svojim stogom, što je prikladno za ovako kratku rubnu (leaf) funkciju:
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 instrukcija SSE2 za pretvaranje float-a dvostruke preciznosti u označeni cijeli broj s odsijecanjem prema nuli, što je upravo ono što bi _ftol trebao raditi. To je jedna instrukcija, preuzima parametar izravno s mjesta gdje ga je ABI ostavio i smješta rezultat tamo gdje ga ABI očekuje. Nema potrebe za manipuliranjem kontrolnom riječi FPU-a.
Imajte na umu da ako ulaz premašuje raspon 32-bitnog označenog cijelog broja, CVTTSD2SI vraća nedefiniranu cjelobrojnu vrijednost ($80000000). To je isto ponašanje kao i kod x87 instrukcije fistp za ulaz izvan raspona. Prije nego što migraciju proglasite završenom, korisno je provjeriti mogu li vaši pozivatelji generirati takve vrijednosti.
Kada je Trunc bolje rješenje
Gore navedenu asemblersku verziju isplati se pisati samo ako imate stvarni zahtjev za binarnom kompatibilnošću: neki vanjski pozivatelj očekuje simbol _ftol s određenom konvencijom pozivanja, a te pozivatelje ne možete promijeniti. Ta je situacija rijetka. U većini slučajeva _ftol je bio privatna pomoćna funkcija koja se koristila samo unutar iste jedinice, i uopće nema vanjske ovisnosti o njezinom nazivu ili konvenciji.
U tom slučaju zamijenite ga običnim Pascalom:
function _ftol(f: Double): Integer; cdecl;
begin
Result := Trunc(f);
end;
Trunc zaokružuje prema nuli, što odgovara onome što je _ftol radio s kontrolnom riječi x87 postavljenom na način rada odsijecanja. Kompajlira se na DCC32 i DCC64 bez izmjena. Kompajler generira odgovarajuću instrukciju za svaku ciljnu platformu: na x64 će obično ionako generirati CVTTSD2SI, istu instrukciju kao i u ručno pisanoj verziji. Dobivate identično ponašanje, bez platformskih uvjeta i bez potrebe za održavanjem asemblerskog koda.
Jedina semantička razlika koju vrijedi provjeriti: Trunc izaziva iznimku EInvalidOp u zadanoj konfiguraciji Delphija kada je ulaz NaN (nije broj) ili beskonačnost. Instrukcija x87 fistp u izvornom kodu samo je zapisivala bitovni uzorak bez izazivanja iznimke. Ako vaš kod prosljeđuje neobične vrijednosti s pomičnim zarezom u ovu funkciju, a staro je ponašanje bilo tiho, upotrijebite provjere IsNaN i IsInfinite iz jedinice Math prije pozivanja funkcije Trunc.
Uvjetno kompajliranje kada oba cilja ostaju aktivna
Neki projekti moraju nastaviti isporučivati i 32-bitne i 64-bitne binarne datoteke. Ako se originalna asemblerska verzija mora zadržati za 32-bitnu, a nova implementacija osigurati za 64-bitnu platformu, upotrijebite uvjet 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 minimalni mehanički popravak i trebali biste ga smatrati privremenim. Baza koda koja sadrži asemblerski kod specifičan za arhitekturu u pomoćnoj funkciji čija je jedina svrha odsijecanje decimalnog broja u cijeli broj nosi nepotreban tehnički dug. 32-bitna grana može se u potpunosti ukloniti nakon što potvrdite da ništa ne ovisi o popratnim efektima FPU-a iz stare implementacije.
Ako se funkcija pojavljuje u komponenti koja se koristi u više jedinica, pretražite cijelu bazu koda za _ftol prije nego što odlučite kako je migrirati. Simbol s tim nazivom može biti deklariran na više mjesta; linker odabire jedan i tiho zanemaruje ostale, što znači da biste mogli popraviti jednu kopiju, a i dalje se povezati s drugom koja nije promijenjena.