Det opprinnelige idiomet _ftol i 32-biters Delphi ser ut som en smart én-linjer: en Pascal-funksjonsinnpakning som slipper inn i inline-assembly for å manipulere x87 FPU-kontrollordet, avkorte verdien på FPU-stakken og hente ut resultatet. Det bygget fint under DCC32 i lang tid, noe som er akkurat grunnen til at det endte opp i så mange eldre grafikk- og PDF-enheter uten at noen stilte spørsmål ved det
Bytt byggemålet til 64-biters, og kompilatoren avsluttes med E1025 Unsupported language feature: 'ASM'. Den feilen er ikke en kompatibilitetsadvarsel. Det betyr at DCC64 ikke vil kompilere rutinen i det hele tatt, uavhengig av hvor godt assembly-koden fungerte før
Originalversjonen i 32-biters så typisk omtrent slik ut:
function _ftol(f: Double): Integer; cdecl;
begin
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
end;
Den asm-blokken inni et Pascal begin...end-legeme er akkurat det DCC64 avviser. De to kompilatorene har forskjellige regler for hvor assembly er tillatt, og grensen har betydning
Hvorfor DCC64 trekker grensen annerledes
DCC32 tillater inline assembly inni vanlige Pascal-rutiner. Kompilatoren kjenner 32-biters kallkonvensjonen og kan resonnere om hvor lokale variabler og parametere befinner seg, slik at den tolererer assembly-fragmenter som strekker seg inn i stakkrammen ved navn. DCC64 inntar en strengere posisjon: assembly må være i en dedikert assembler-funksjon, en der hele kroppen er assembly og kallkonvensjonen håndteres eksplisitt. Blandet Pascal pluss asm støttes ikke i det hele tatt
Den underliggende årsaken er arkitektonisk. I den 64-biters Windows kallkonvensjonen (Microsoft ABI) kommer de fire første parameterne i RCX, RDX, R8 og R9 for heltallstyper, eller i XMM0 til og med XMM3 for flyttall. Det er ingen x87 FPU-involvering i normal parameteroverføring; x87 er teknisk tilgjengelig, men ABI-et bruker den ikke for argumenttransport. Assembly som forutsetter at en verdi er "på FPU-stakken" resonnerer ut fra en tilstand som 64-biters ABI-et aldri skaper
Så det gamle fragmentet har ikke bare et syntaksproblem. Selv om DCC64 godtok det, ville registerforutsetningene være feil
Å skrive en riktig 64-biters assembler-versjon
Når du oppriktig trenger å eksportere et _ftol-symbol med kallkonvensjonen cdecl for binær kompatibilitet, må funksjonen skrives som en ren assembler-rutine. Under det 64-biters ABI-et kommer en Double-parameter i XMM0, og heltallsresultatet må ligge i RAX ved retur. Direktivet .NOFRAME forteller DCC64 at rutinen administrerer sin egen stakk, noe som passer for en bladfunksjon som er såpass kort:
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 er SSE2-instruksjonen for å konvertere et dobbel-presisjon flyttall til et signert heltall med avkorting (truncation) mot null, noe som er nøyaktig hva _ftol er ment å gjøre. Det er én instruksjon, tar parameteren direkte fra der ABI-et la den, og plasserer resultatet der ABI-et forventer det. Ingen sjonglering med x87-kontrollordet er nødvendig
Merk at hvis inndataene overskrider området for et 32-biters signert heltall, returnerer CVTTSD2SI heeltallets udefinerte verdi ($80000000). Dette er den samme atferden som x87 fistp på inndata utenfor rekkevidde. Hvorvidt innringerne dine kan produsere slike verdier er verdt å bekrefte før migreringen erklæres ferdig
Når Trunc er det beste svaret
Assembler-versjonen ovenfor er bare verdt å skrive når du faktisk har et krav om binær kompatibilitet: en eller annen ekstern innringer forventer symbolet _ftol med en spesifikk kallkonvensjon, og du kan ikke endre disse innringerne. Den situasjonen er uvanlig. For det meste var _ftol en privat hjelper brukt kun innad i samme enhet, og det finnes ingen ekstern avhengighet til dens navn eller konvensjon i det hele tatt
For det tilfellet erstatter du den med ren Pascal:
function _ftol(f: Double): Integer; cdecl;
begin
Result := Trunc(f);
end;
Trunc avkorter mot null, noe som samsvarer med hva _ftol gjorde da x87-kontrollordet var satt i avkortingsmodus. Den kompileres på DCC32 og DCC64 uten modifikasjoner. Kompilatoren genererer den passende instruksjonen for hvert mål: på x64 vil den typisk uansett utstede CVTTSD2SI, den samme instruksjonen som den håndskrevne versjonen. Du får identisk oppførsel, ingen plattformbetingelser, og ingen assembly å vedlikeholde
Den ene semantiske forskjellen verdt å sjekke er: Trunc utløser et EInvalidOp-unntak i Delphis standardkonfigurasjon når inndataene er NaN eller uendelig. x87 fistp i den opprinnelige koden skrev bare et bitmønster uten å utløse noe. Hvis koden din mater uvanlige flyttallsverdier inn i denne funksjonen og den gamle oppførselen var taus, må du sikre med IsNaN og IsInfinite fra Math før du kaller Trunc
Betinget kompilering når begge mål forblir aktive
Noen prosjekter må fortsette å levere både 32-biters og 64-biters binærfiler. Hvis den opprinnelige assembler-versjonen må beholdes for 32-biters og en ny implementering gis for 64-biters, bruk betingelsen 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 er den minste mekaniske rettelsen, og det er verdt å behandle den som midlertidig. En kodebase som bærer arkitekturspesifikk assembly i en hjelper hvis eneste formål er avkorting (truncation) fra flyttall til heltall, bærer på unødvendig gjeld. 32-biters grenen kan forsvinne helt så snart du bekrefter at ingenting avhenger av FPU-bieffektene fra den gamle implementeringen
Hvis funksjonen dukker opp i en komponent brukt på tvers av flere enheter, søk gjennom hele kodebasen etter _ftol før du bestemmer deg for hvordan du skal migrere. Et symbol med det navnet kan deklareres på mer enn ett sted; lenkeren (linker) plukker en og ignorerer de andre i stillhet, noe som betyr at du kanskje fikser en kopi og fremdeles lenker mot en annen som ikke er rørt