Technisch artikel

_ftol inline assembly porten van 32-bit Delphi naar DCC64

Het originele _ftol-idiom in 32-bit Delphi bestaat vanwege C-objectbestanden. Als een unit .obj-bestanden linkt die door een 32-bit C-compiler zijn gemaakt, roepen die objecten voor elke float-naar-integer-cast de runtime-helper _ftol aan, met de waarde op de top van de x87-stack in ST(0) in plaats van als parameter meegegeven. De Delphi-vervanger declareert daarom een dummy-Double-parameter, popt ST(0) erin met een inline-assembly-blok van twee regels, en kapt het daarna in Pascal af. Onder DCC32 bouwde dat jarenlang gewoon, en juist daarom belandde het ongevraagd in zoveel oudere graphics- en PDF-units

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

Delphi-diagram dat DCC32, dat inline asm binnen Pascal-routines compileert, vergelijkt met DCC64, dat afbreekt met E1025 en onder de Win64-ABI alleen pure assemblerfuncties toestaat
DCC32 tolereert een asm-blok binnen een Pascal-routine terwijl DCC64 antwoordt met E1025, omdat de Win64-ABI argumenten in registers verplaatst in plaats van op de x87-stack

Een correcte 64-bit assembler versie schrijven

Controleer eerst of er helemaal een 64-bits _ftol nodig is. De Win64-C-compilers — MSVC en de op Clang gebaseerde C++Builder-toolchain — zetten floats inline om naar integers met SSE2-instructies en genereren geen aanroepen van _ftol, dus 64-bits objectbestanden verwijzen zelden naar het symbool. Wilt u echt een _ftol-symbool exporteren met cdecl-conventie voor binaire compatibiliteit, dan moet de functie als pure assembler-routine worden geschreven. Onder de 64-bits ABI komt een Double-parameter binnen in XMM0 en komt een Integer-resultaat terug in EAX, de lage helft van RAX. De directive .NOFRAME vertelt DCC64 dat de routine zijn eigen stack beheert, wat voor een leaf-functie van deze lengte passend is:

function _ftol: Integer; cdecl;
// Double-waarde verwacht in XMM0 volgens de 64-bits ABI
asm
  .NOFRAME
  cvttsd2si  eax, xmm0   // afkappen naar een 32-bit integer, resultaat in eax
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

Het 32-bit doelregister telt. Als de invoer het bereik van een 32-bit signed integer overschrijdt, geeft CVTTSD2SI met EAX als doel de integer indefinite-waarde terug ($80000000). Met RAX in plaats daarvan krijgt u een 64-bit-conversie, en invoer tussen 231 en 263 komt terug als wat hun lage 32 bits toevallig zijn. Dat is hetzelfde gedrag als de x87-fistp bij out-of-range-invoer. Het is het bevestigen waard of uw aanroepers zulke waarden kunnen produceren voordat u de migratie als afgerond uitroept

Delphi: Datapad-diagram met een Double-parameter die aankomt in XMM0 en door één CVTTSD2SI-instructie wordt omgezet in een afgekapte integer teruggegeven in EAX, waarbij out-of-range-waarden het integer indefinite-patroon opleveren
Onder DCC64 komt de double aan in XMM0 en plaatst CVTTSD2SI het afgeknipte 32-bit resultaat in EAX als één enkele SSE2-instructie

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

Delphi-beslissingsdiagram voor het porteren van _ftol: vervang het door gewone Trunc wanneer geen externe aanroeper het cdecl-symbool nodig heeft, houd anders een pure assembler-export aan, en beveilig NaN- of oneindigheidsinvoer tegen EInvalidOp
Migreer naar gewone Trunc tenzij externe aanroepers afhankelijk zijn van het geëxporteerde symbool, en beveilig in beide gevallen NaN- of infinity-invoer tegen EInvalidOp

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-bits pad: DCC32 accepteert 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