Det ursprungliga _ftol-idiomet i 32-bitars Delphi ser ut som en smart enradare: ett Pascal-funktionsomslag som hoppar in i inline-assembler för att manipulera kontrollordet för x87 FPU, trunkera värdet på FPU-stacken och plocka ut (pop) resultatet. Det byggdes bra under DCC32 under lång tid, vilket är precis varför det hamnade i så många äldre grafik- och PDF-enheter utan att någon ifrågasatte det
Ändra byggmålet till 64-bitars och kompilatorn avslutar med E1025 Unsupported language feature: 'ASM'. Det felet är inte en kompatibilitetsvarning. Det betyder att DCC64 inte kommer att kompilera rutinen alls, oavsett hur bra assemblern fungerade tidigare
Det 32-bitars originalet såg vanligtvis ut ungefär så här:
function _ftol(f: Double): Integer; cdecl;
begin
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
end;
Det där asm-blocket inuti en Pascal begin...end-kropp är exakt vad DCC64 vägrar acceptera. De två kompilatorerna har olika regler om var assembler är tillåtet, och gränsen spelar roll
Varför DCC64 drar gränsen annorlunda
DCC32 tillåter inline-assembler inuti vanliga Pascal-rutiner. Kompilatorn känner till 32-bitars anropskonvention (calling convention) och kan resonera kring var lokala variabler och parametrar bor, så den tolererar assembler-fragment som når in i stackramen via namn. DCC64 intar en striktare position: assembler måste finnas i en dedikerad assemblerfunktion, en där hela kroppen är assembler och anropskonventionen hanteras uttryckligen. Blandad Pascal-plus-asm stöds inte alls
Den underliggande orsaken är arkitektonisk. I 64-bitars Windows-anropskonventionen (Microsoft ABI) anländer de fyra första parametrarna i RCX, RDX, R8 och R9 för heltalstyper, eller i XMM0 till XMM3 för flyttal. Det finns ingen inblandning av x87 FPU i normal parameteröverföring; x87 är tekniskt tillgängligt men ABI:t använder den inte för argumenttransport. Assembler som antar att ett värde är "på FPU-stacken" resonerar kring ett tillstånd som 64-bitars ABI:t aldrig skapar
Så det gamla fragmentet har inte bara ett syntaxproblem. Även om DCC64 accepterade det skulle registerantagandena vara felaktiga
Att skriva en ordentlig 64-bitars assemblerversion
När du genuint behöver exportera en _ftol-symbol med cdecl-konventionen för binär kompatibilitet, måste funktionen skrivas som en ren assembler-rutin. Under 64-bitars ABI:t anländer en Double-parameter i XMM0, och heltalsresultatet måste ligga i RAX vid återgång. Direktivet .NOFRAME talar om för DCC64 att rutinen hanterar sin egen stack, vilket är lämpligt för en lövfunktion (leaf function) av denna korta längd:
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 är SSE2-instruktionen för att konvertera ett flyttal med dubbel precision till ett teckenförsett heltal (signed integer) med trunkering mot noll, vilket är exakt vad _ftol förväntas göra. Det är en instruktion som tar parametern direkt från där ABI:t lämnade den, och placerar resultatet där ABI:t förväntar sig det. Inget jonglerande med FPU-kontrollord behövs
Observera att om inmatningen överskrider intervallet för ett 32-bitars teckenförsett heltal, returnerar CVTTSD2SI det obestämda heltalsvärdet ($80000000). Det är samma beteende som x87 fistp uppvisar vid inmatning utanför intervallet. Huruvida dina anropare kan producera sådana värden är värt att bekräfta innan migreringen förklaras klar
När Trunc är det bättre svaret
Assemblerversionen ovan är bara värd att skriva när du har ett faktiskt krav på binär kompatibilitet: någon extern anropare förväntar sig symbolen _ftol med en specifik anropskonvention, och du inte kan ändra de anroparna. Den situationen är ovanlig. Oftast var _ftol en privat hjälpare som bara användes inom samma enhet, och det finns inget externt beroende av dess namn eller konvention alls
I det fallet, ersätt den med vanlig Pascal:
function _ftol(f: Double): Integer; cdecl;
begin
Result := Trunc(f);
end;
Trunc trunkerar mot noll, vilket matchar vad _ftol gjorde med x87-kontrollordet inställt på trunkeringsläge. Det kompileras på DCC32 och DCC64 utan modifiering. Kompilatorn genererar lämplig instruktion för varje mål: på x64 kommer den vanligtvis att mata ut (emit) CVTTSD2SI ändå, samma instruktion som den handskrivna versionen. Du får identiskt beteende, inga plattformsvillkor och ingen assembler att underhålla
Den enda semantiska skillnaden värd att kontrollera: Trunc utlöser ett EInvalidOp-undantag i Delphis standardkonfiguration när inmatningen är NaN eller oändlighet (infinity). x87 fistp i den ursprungliga koden skrev bara ett bitmönster utan att utlösa något. Om din kod matar ovanliga flyttalsvärden in i den här funktionen och det gamla beteendet var tyst, skydda med IsNaN och IsInfinite från Math innan du anropar Trunc
Villkorlig kompilering när båda målen förblir aktiva
Vissa projekt måste fortsätta att leverera både 32-bitars och 64-bitars binärer. Om den ursprungliga assemblerversionen måste behållas för 32-bitars och en ny implementation tillhandahållas för 64-bitars, använd villkoret 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;
Det är den minimala mekaniska lösningen, och den är värd att behandla som tillfällig. En kodbas som bär på arkitekturspecifik assembler i en hjälpare vars enda syfte är trunkering av flyttal till heltal bär på onödig teknisk skuld (debt). Den 32-bitars grenen kan försvinna helt och hållet när du har bekräftat att inget beror på FPU-sidoeffekterna av den gamla implementationen
Om funktionen dyker upp i en komponent som används över flera enheter, sök i hela kodbasen efter _ftol innan du bestämmer hur du ska migrera. En symbol med det namnet kan deklareras på mer än en plats; länkaren väljer en och ignorerar de andra tyst, vilket innebär att du kan åtgärda en kopia och fortfarande länka mot en annan som inte har rörts