Technisch artikel

Het overzetten van _ftol Inline Assembly van 32-bit Delphi naar DCC64

Het oorspronkelijke _ftol idioom in 32-bit Delphi ziet eruit als een slimme oneliner: een Pascal functie-wrapper die overschakelt op inline assembly om het x87 FPU control word te manipuleren, de waarde op de FPU-stack af te kappen en het resultaat te poppen. Het compileerde lange tijd prima onder DCC32, wat precies de reden is waarom het in zoveel oudere grafische en PDF units terechtkwam zonder dat iemand er vragen bij stelde

Zet het bouwdoel op 64-bit en de compiler stopt met E1025 Unsupported language feature: 'ASM'. Die fout is geen compatibiliteitswaarschuwing. Het betekent dat DCC64 de routine helemaal niet compileert, ongeacht hoe goed de assembly voorheen werkte

Het 32-bit origineel zag er meestal ongeveer zo uit:

function _ftol(f: Double): Integer; cdecl;
begin
  asm
    lea   eax, f
    fstp  qword ptr [eax]
  end;
  Result := Trunc(f);
end;

Dat asm-blok binnen een Pascal begin...end body is precies wat DCC64 weigert. De twee compilers hebben verschillende regels over waar assembly is toegestaan, en de grens doet ertoe

Waarom DCC64 de grens anders trekt

DCC32 staat inline assembly toe binnen gewone Pascal routines. De compiler kent de 32-bit calling convention en kan redeneren over waar lokale variabelen en parameters leven, dus het tolereert assembly-fragmenten die op naam in het stackframe grijpen. DCC64 neemt een strikter standpunt in: assembly moet in een speciale assemblerfunctie staan, een waarbij de hele body uit assembly bestaat en de calling convention expliciet wordt afgehandeld. Gemengde Pascal-plus-asm wordt helemaal niet ondersteund

De onderliggende reden is architecturaal. In de 64-bit Windows calling convention (Microsoft ABI) arriveren de eerste vier parameters in RCX, RDX, R8 en R9 voor integertypes, of in XMM0 tot en met XMM3 voor floating-point. Er is geen x87 FPU betrokkenheid bij normale parameterdoorgifte; x87 is technisch beschikbaar maar de ABI gebruikt het niet voor argumenttransport. Assembly die ervan uitgaat dat een waarde "op de FPU-stack" staat, redeneert over een toestand die de 64-bit ABI nooit creëert

Dus het oude fragment heeft niet alleen een syntaxprobleem. Zelfs als DCC64 het zou accepteren, zouden de registeraannames fout zijn

Een correcte 64-bit assembler versie schrijven

Wanneer u daadwerkelijk een _ftol symbool met de cdecl conventie moet exporteren voor binaire compatibiliteit, moet de functie geschreven worden als een pure assembler routine. Onder de 64-bit ABI arriveert een Double parameter in XMM0, en het integer-resultaat moet bij terugkeer in RAX staan. De .NOFRAME richtlijn vertelt DCC64 dat de routine zijn eigen stack beheert, wat passend is voor een 'leaf function' die zo kort is:

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 is de SSE2 instructie voor het converteren van een double-precision float naar een signed integer met afkapping naar nul, wat precies is wat _ftol hoort te doen. Het is één instructie, haalt de parameter direct van waar de ABI het heeft achtergelaten, en plaatst het resultaat waar de ABI het verwacht. Er is geen gegoochel met het FPU control-word nodig

Merk op dat als de invoer het bereik van een 32-bit signed integer overschrijdt, CVTTSD2SI de integer indefinite waarde ($80000000) retourneert. Dat is hetzelfde gedrag als de x87 fistp bij out-of-range invoer. Of uw aanroepers zulke waarden kunnen produceren is de moeite waard om te bevestigen voordat u de migratie als voltooid beschouwt

Wanneer Trunc het betere antwoord is

De bovenstaande assemblerversie is alleen het schrijven waard wanneer u een daadwerkelijke binaire compatibiliteitsvereiste heeft: een externe aanroeper verwacht het symbool _ftol met een specifieke calling convention, en u kunt die aanroepers niet wijzigen. Die situatie is ongebruikelijk. Meestal was _ftol een privé-helper die alleen binnen dezelfde unit werd gebruikt, en is er helemaal geen externe afhankelijkheid van de naam of conventie

Vervang het in dat geval door gewone Pascal:

function _ftol(f: Double): Integer; cdecl;
begin
  Result := Trunc(f);
end;

Trunc kapt af naar nul, wat overeenkomt met wat _ftol deed toen het x87 control word in truncatie-modus was ingesteld. Het compileert zonder wijzigingen op DCC32 en DCC64. De compiler genereert de geschikte instructie voor elk doel: op x64 zendt het typisch sowieso CVTTSD2SI uit, dezelfde instructie als de handgeschreven versie. U krijgt identiek gedrag, geen platform-conditionals en geen assembly om te onderhouden

Het enige semantische verschil dat het controleren waard is: Trunc werpt een EInvalidOp uitzondering op in de standaardconfiguratie van Delphi wanneer de invoer een NaN of oneindigheid is. De x87 fistp in de originele code schreef gewoon een bitpatroon zonder iets op te werpen. Als uw code ongebruikelijke floating-point waarden aan deze functie levert en het oude gedrag stil was, beveilig dit dan met IsNaN en IsInfinite van Math voordat u Trunc aanroept

Voorwaardelijke compilatie wanneer beide doelen actief blijven

Sommige projecten moeten zowel 32-bit als 64-bit binaries blijven leveren. Als de originele assemblerversie behouden moet blijven voor 32-bit en een nieuwe implementatie voor 64-bit nodig is, gebruik dan de CPUX64 conditional:

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;

Dat is de minimale mechanische oplossing en is het waard om als tijdelijk te beschouwen. Een codebase die architectuurspecifieke assembly met zich meedraagt in een helper waarvan het enige doel float-naar-integer afkapping is, draagt onnodige schuld met zich mee. De 32-bit vertakking kan volledig verdwijnen zodra u bevestigt dat er niets afhangt van de FPU-bijwerkingen van de oude implementatie

Als de functie voorkomt in een component die in meerdere units wordt gebruikt, zoek dan in de hele codebase naar _ftol voordat u beslist hoe te migreren. Een symbool met die naam kan op meerdere plaatsen gedeclareerd zijn; de linker kiest er één en negeert stilletjes de anderen, wat betekent dat u één kopie zou kunnen herstellen en toch tegen een andere zou kunnen linken die niet is aangeraakt