Originalni _ftol idiom u 32-bitnom Delphiju izgleda kao pametna funkcija od jedne linije: Pascal omotač (wrapper) za funkciju koji se prebacuje u ugrađeni (inline) asembler kako bi manipulirao kontrolnom riječi x87 FPU-a, skratio vrijednost na FPU stogu i povukao (pop) rezultat. Dugo se bez problema kompajlirao pod DCC32, što je upravo razlog zašto je završio u tolikim starijim grafičkim i PDF jedinicama bez da ga je itko dovodio u pitanje
Prebacite cilj kompajliranja na 64-bitni i kompajler prekida rad s upozorenjem E1025 Unsupported language feature: 'ASM'. Ta greška nije upozorenje o kompatibilnosti. To znači da DCC64 uopće neće kompajlirati rutinu, bez obzira na to koliko je asembler prije dobro radio
32-bitni original obično je izgledao otprilike ovako:
function _ftol(f: Double): Integer; cdecl;
begin
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
end;
Taj blok asm unutar Pascal begin...end tijela točno je ono što DCC64 odbija. Dva kompajlera imaju različita pravila o tome gdje je dopušten asembler i ta je granica važna
Zašto DCC64 povlači crtu drugačije
DCC32 dopušta inline asembler unutar običnih Pascal rutina. Kompajler poznaje 32-bitnu konvenciju pozivanja (calling convention) i može zaključiti gdje se nalaze lokalne varijable i parametri, pa tolerira fragmente asemblera koji posežu u okvir stoga (stack frame) prema nazivu. DCC64 zauzima stroži stav: asembler mora biti u posvećenoj asemblerskoj funkciji, onoj u kojoj je cijelo tijelo asembler, a konvencija pozivanja obrađuje se eksplicitno. Mješavina Pascala i asm-a uopće nije podržana
Razlog tome leži u arhitekturi. U 64-bitnoj Windows konvenciji pozivanja (Microsoft ABI), prva četiri parametra pristižu u RCX, RDX, R8 i R9 za tipove cijelih brojeva ili u XMM0 kroz XMM3 za pomični zarez (floating-point). U uobičajenom prosljeđivanju parametara x87 FPU nije uključen; x87 je tehnički dostupan, ali ABI ga ne koristi za transport argumenata. Asembler koji pretpostavlja da je vrijednost "na FPU stogu" donosi zaključke o stanju koje 64-bitni ABI nikada ne stvara
Tako da stari fragment nema samo problem sa sintaksom. Čak i kad bi ga DCC64 prihvatio, pretpostavke o registrima bile bi pogrešne
Pisanje ispravne 64-bitne verzije asemblera
Kada doista trebate eksportirati _ftol simbol s cdecl konvencijom radi binarne kompatibilnosti, funkcija mora biti napisana kao čista asemblerska rutina (pure assembler routine). Prema 64-bitnom ABI-ju, Double parametar pristiže u XMM0, a rezultat cijelog broja mora biti u RAX pri povratku. Direktiva .NOFRAME govori DCC64 da rutina sama upravlja svojim stogom, što je prikladno za funkciju tipa "list" (leaf function) koja je ovako kratka:
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 SSE2 instrukcija za pretvorbu float-a dvostruke preciznosti u cijeli broj s predznakom uz skraćivanje prema nuli, što je upravo ono što bi _ftol trebao činiti. Riječ je o jednoj instrukciji, parametar uzima direktno s mjesta na kojem ga je ABI ostavio te sprema rezultat ondje gdje ga ABI očekuje. Nije potrebno žongliranje kontrolnom riječi FPU-a
Imajte na umu da, ako unos premaši raspon 32-bitnog cijelog broja s predznakom (signed integer), CVTTSD2SI vraća neodređenu vrijednost cijelog broja (integer indefinite value, $80000000). To je isto ponašanje kao kod x87 fistp pri unosu izvan raspona. Vrijedi provjeriti mogu li vaši pozivatelji (callers) proizvesti takve vrijednosti prije nego što objavite da je migracija završena
Kada je Trunc bolji odgovor
Prethodno navedenu verziju asemblera isplati se pisati samo kada imate stvaran zahtjev za binarnom kompatibilnošću: neki vanjski pozivatelj očekuje simbol _ftol sa specifičnom konvencijom pozivanja, a te pozivatelje ne možete mijenjati. Ta je situacija rijetka. Najčešće, _ftol je bio privatni pomoćnik (private helper) korišten isključivo unutar iste jedinice, i na njegov naziv ili konvenciju uopće ne postoji vanjska ovisnost
U tom slučaju, zamijenite ga običnim Pascalom:
function _ftol(f: Double): Integer; cdecl;
begin
Result := Trunc(f);
end;
Trunc vrši skraćivanje (truncation) prema nuli, što odgovara onome što je radio _ftol dok je kontrolna riječ x87 bila postavljena na način skraćivanja (truncation mode). Kompajlira se na DCC32 i DCC64 bez modifikacija. Kompajler generira odgovarajuću instrukciju za svaku ciljanu arhitekturu: na x64 i onako će obično emitirati CVTTSD2SI, što je ista instrukcija kao u ručno napisanoj verziji. Dobivate identično ponašanje, nema uvjeta platforme (platform conditionals) i ne morate održavati asembler
Postoji jedna semantička razlika koju vrijedi provjeriti: Trunc pokreće (raises) EInvalidOp iznimku (exception) u zadanoj konfiguraciji Delphija kada je unos NaN ili beskonačnost (infinity). Instrukcija x87 fistp u originalnom kodu samo je zapisivala bitovni uzorak (bit pattern) bez podizanja (raising) ikakve greške. Ako vaš kôd prosljeđuje neobične vrijednosti pomičnog zareza u ovu funkciju i staro je ponašanje bilo tiho, osigurajte (guard) to s IsNaN i IsInfinite iz Math prije pozivanja Trunc-a
Uvjetno kompajliranje kada obje ciljane arhitekture ostaju aktivne
Neki projekti moraju nastaviti isporučivati i 32-bitne i 64-bitne binarne datoteke. Ako se izvorna verzija asemblera mora zadržati za 32-bitnu, a za 64-bitnu arhitekturu osigurati nova implementacija, koristite CPUX64 uvjet:
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;
Ovo predstavlja najmanji mehanički popravak te ga vrijedi tretirati kao privremenog. Baza koda koja održava specifični asemblerski kôd vezan uz pojedinu arhitekturu u nekom pomoćniku čija je jedina namjena skraćivanje (truncation) decimalnog broja na cijeli (float-to-integer) sa sobom vuče nepotreban teret. 32-bitna grana može se u potpunosti ukloniti nakon što potvrdite da ništa ne ovisi o nuspojavama FPU-a stare implementacije
Ako se funkcija pojavljuje u komponenti koja se koristi u više jedinica, pretražite cijelu bazu kodova za _ftol prije nego što odlučite kako obaviti migraciju. Simbol pod tim nazivom može biti deklariran na više od jednog mjesta; povezivač (linker) odabire jedan te tiho zanemaruje ostale, što znači da biste mogli ispraviti jednu kopiju i i dalje vršiti povezivanje na onu koja nije izmijenjena