Technisch artikel

_ftol inline assembly porten van 32-bit Delphi naar DCC64

Het oorspronkelijke _ftol-idioom in 32-bit Delphi oogt als een slimme one-liner: een Pascal-functiewed wrapper die inline assembly gebruikt om het x87 FPU-stuurwoord te manipuleren, de waarde op de FPU-stack af te kappen en het resultaat eruit te halen. Het werd jarenlang zonder problemen gebouwd met DCC32, wat precies de reden is waarom het in zoveel oudere grafische en PDF-units terechtkwam zonder dat iemand er vraagtekens bij plaatste.

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

Het 32-bit origineel zag er doorgaans 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 hanteren verschillende regels over waar assembly is toegestaan, en die grens is van belang.

Waarom DCC64 een andere grens trekt

DCC32 staat inline assembly toe binnen gewone Pascal-routines. De compiler kent de 32-bit aanroepconventie en kan redeneren over waar lokale variabelen en parameters op de stack staan, waardoor het assembly-fragmenten tolereert die via naam in het stackframe grijpen. DCC64 hanteert een strengere houding: assembly moet in een toegewijde assembleerfunctie staan, waarbij de volledige body uit assembly bestaat en de aanroepconventie expliciet wordt afgehandeld. Gemengde Pascal-plus-asm wordt helemaal niet ondersteund.

De onderliggende reden is architectureel. In de 64-bit Windows aanroepconventie (Microsoft ABI) komen de eerste vier parameters aan in RCX, RDX, R8 en R9 voor integer-typen, of in XMM0 tot en met XMM3 voor floating-point. Er is geen x87 FPU-betrokkenheid bij normale parameteroverdracht; x87 is technisch beschikbaar maar de ABI gebruikt het niet voor het doorgeven van argumenten. Assembly die ervan uitgaat dat een waarde "op de FPU-stack" staat, redeneert over een toestand die de 64-bit ABI nooit aanmaakt.

Het oude fragment heeft dus niet alleen een syntaxisprobleem. Zelfs als DCC64 het zou accepteren, zouden de registeraannames onjuist zijn.

Een correcte 64-bit assembler-versie schrijven

Wanneer u werkelijk een _ftol-symbool met cdecl-conventie wilt exporteren voor binaire compatibiliteit, moet de functie als een pure assembler-routine worden geschreven. Onder de 64-bit ABI komt een Double-parameter aan in XMM0, en het integer-resultaat moet bij terugkeer in RAX staan. De .NOFRAME-instructie deelt DCC64 mee dat de routine zijn eigen stack beheert, wat passend is voor een zo korte leaf-functie:

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-precisie float naar een signed integer met afkapping richting nul, wat precies is wat _ftol hoort te doen. Het is één instructie, neemt de parameter rechtstreeks van waar de ABI deze heeft achtergelaten, en plaatst het resultaat waar de ABI het verwacht. Geen FPU-stuurwoordmanipulatie nodig.

Let op: als de invoer buiten het bereik van een 32-bit signed integer valt, geeft CVTTSD2SI de integer-onbepaalde waarde terug ($80000000). Dat is hetzelfde gedrag als de x87 fistp bij invoer buiten bereik. Of uw aanroepers dergelijke waarden kunnen produceren, is de moeite waard om te bevestigen voordat u de migratie als voltooid beschouwt.

Wanneer Trunc het betere antwoord is

De assembler-versie hierboven is alleen de moeite waard als u een echte binaire compatibiliteitsvereiste heeft: een externe aanroeper verwacht het symbool _ftol met een specifieke aanroepconventie en u kunt die aanroepers niet wijzigen. Die situatie is zeldzaam. Meestal was _ftol een privéhulpfunctie die alleen binnen dezelfde unit werd gebruikt, zonder externe afhankelijkheid van de naam of conventie ervan.

Vervang het in dat geval door gewone Pascal:

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

Trunc kapt af richting nul, wat overeenkomt met wat _ftol deed met het x87-stuurwoord in afkappingsmodus. Het compileert op DCC32 en DCC64 zonder aanpassing. De compiler genereert de juiste instructie voor elk doel: op x64 zal het doorgaans toch CVTTSD2SI uitstoten, dezelfde instructie als de handgeschreven versie. U krijgt identiek gedrag, geen platformconditionals en geen assembly om te onderhouden.

Het enige semantische verschil dat de moeite waard is om te controleren: Trunc gooit een EInvalidOp-uitzondering in Delphi's standaardconfiguratie wanneer de invoer een NaN of oneindig is. De x87 fistp in de originele code schreef gewoon een bitpatroon zonder iets te gooien. Als uw code ongebruikelijke zwevende-kommawaarden in deze functie invoert en het oude gedrag stilzwijgend was, gebruik dan IsNaN en IsInfinite uit Math als beveiliging voordat u Trunc aanroept.

Conditionele compilatie wanneer beide doelen actief blijven

Sommige projecten moeten zowel 32-bit als 64-bit binaries blijven leveren. Als de originele assembler-versie bewaard moet blijven voor 32-bit en een nieuwe implementatie moet worden geboden voor 64-bit, 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 het is de moeite waard om die als tijdelijk te beschouwen. Een codebase die architectuurspecifieke assembly meesleept in een helperfunctie waarvan het enige doel float-naar-integer afkapping is, heeft onnodige technische schuld. De 32-bit branch kan volledig verdwijnen zodra u bevestigt dat niets afhankelijk is van de FPU-neveneffecten van de oude implementatie.

Als de functie voorkomt in een component dat over meerdere units wordt gebruikt, doorzoek de gehele codebase naar _ftol voordat u beslist hoe te migreren. Een symbool met die naam kan op meer dan één plaats zijn gedeclareerd; de linker kiest er één en negeert de andere stilzwijgend, wat betekent dat u één kopie kunt repareren maar toch tegen een andere koppelt die nog niet is aangepast.