Technický článek

Převod inline assembleru _ftol z 32bitového Delphi do DCC64

Původní idiom _ftol ve 32bitovém Delphi vypadá jako chytrý jednořádkový kód: wrapper funkce v Pascalu, který se ponoří do inline assembleru, aby zmanipuloval řídicí slovo x87 FPU, zkrátil (truncate) hodnotu na zásobníku FPU a vyjmul výsledek. Dlouhou dobu se to pod DCC32 bez problémů kompilovalo, což je přesně ten důvod, proč to skončilo v tolika starších grafických a PDF jednotkách, aniž by o tom někdo pochyboval

Přepněte cíl sestavení na 64 bitů a kompilátor se ukončí s chybou E1025 Unsupported language feature: 'ASM'. Tato chyba není varováním o kompatibilitě. Znamená to, že DCC64 rutinu vůbec nezkompiluje, bez ohledu na to, jak dobře tento assembler fungoval dříve

Původní 32bitová verze typicky vypadala nějak takto:

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

Tento blok asm uvnitř těla begin...end v Pascalu je přesně to, co DCC64 odmítá. Tyto dva kompilátory mají odlišná pravidla ohledně toho, kde je povolen assembler, a na této hranici záleží

Proč DCC64 rýsuje čáru jinde

DCC32 povoluje inline assembler uvnitř běžných rutin Pascalu. Kompilátor zná 32bitovou konvenci volání a dokáže odvodit, kde žijí lokální proměnné a parametry, takže toleruje fragmenty assembleru, které sahají do rámce zásobníku jménem. DCC64 zaujímá přísnější postoj: assembler musí být ve vyhrazené funkci assembleru, kde celé tělo tvoří assembler a konvence volání se zpracovává explicitně. Smíšený kód Pascal-plus-asm není vůbec podporován

Základní důvod je architektonický. V konvenci volání pro 64bitové Windows (Microsoft ABI) dorazí první čtyři parametry v registrech RCX, RDX, R8 a R9 pro celočíselné typy, nebo v XMM0XMM3 pro reálná čísla (floating-point). Běžné předávání parametrů vůbec nezahrnuje x87 FPU; koprocesor x87 je technicky k dispozici, ale ABI jej pro přenos argumentů nepoužívá. Assembler, který předpokládá, že hodnota je „na zásobníku FPU“, uvažuje o stavu, který 64bitové ABI nikdy nevytváří

Takže onen starý fragment nemá jen problém se syntaxí. I kdyby jej DCC64 přijal, předpoklady o registrech by byly chybné

Zápis správné 64bitové verze v assembleru

Když skutečně potřebujete exportovat symbol _ftol s konvencí cdecl pro binární kompatibilitu, funkce musí být zapsána jako čistá rutina v assembleru. Podle 64bitového ABI parametr typu Double dorazí v XMM0 a výsledek (celé číslo) musí být při návratu v RAX. Direktiva .NOFRAME říká DCC64, že si rutina spravuje svůj vlastní zásobník, což je vhodné pro takto krátkou listovou (leaf) funkci:

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 je instrukce SSE2 pro převod reálného čísla s dvojitou přesností (double) na znaménkové celé číslo s oříznutím k nule, což je přesně to, co má _ftol dělat. Je to jediná instrukce, bere parametr přímo odtud, kde ho ABI zanechalo, a umístí výsledek tam, kde ho ABI očekává. Není potřeba žádné žonglování s řídicím slovem FPU

Všimněte si, že pokud vstup překročí rozsah 32bitového celého čísla se znaménkem, CVTTSD2SI vrátí neurčitou celočíselnou hodnotu ($80000000). To je stejné chování jako u instrukce x87 fistp při vstupu mimo rozsah. Než prohlásíte migraci za hotovou, stojí za to ověřit, zda vaši volající mohou takové hodnoty produkovat

Kdy je lepším řešením Trunc

Výše uvedenou verzi v assembleru má smysl psát pouze tehdy, když máte skutečný požadavek na binární kompatibilitu: nějaký externí volající očekává symbol _ftol se specifickou konvencí volání a vy tyto volající nemůžete změnit. Taková situace je neobvyklá. Ve většině případů byl _ftol jen privátním pomocníkem používaným výhradně v rámci jedné jednotky a neexistuje vůbec žádná externí závislost na jeho názvu nebo konvenci

Pro takový případ jej nahraďte prostým kódem v Pascalu:

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

Trunc ořezává směrem k nule, což odpovídá tomu, co dělal _ftol s řídicím slovem x87 nastaveným na režim oříznutí. Zkompiluje se na DCC32 i DCC64 bez úprav. Kompilátor vygeneruje pro každý cíl vhodnou instrukci: na x64 obvykle stejně emituje CVTTSD2SI, tedy tutéž instrukci jako ručně psaná verze. Získáte totožné chování, žádné podmínky platforem a žádný assembler k udržování

Jediný sémantický rozdíl, který stojí za to zkontrolovat: ve výchozí konfiguraci Delphi vyvolá Trunc výjimku EInvalidOp, pokud je vstupem NaN (Not-a-Number) nebo nekonečno. Instrukce x87 fistp v původním kódu jen zapsala bitový vzor bez toho, aby něco vyvolala. Pokud váš kód do této funkce posílá neobvyklé reálné hodnoty a staré chování bylo tiché, pojistěte jej před zavoláním Trunc funkcemi IsNaN a IsInfinite z jednotky Math

Podmíněná kompilace, když oba cíle zůstávají aktivní

Některé projekty musí i nadále dodávat 32bitové i 64bitové binární soubory. Pokud musí být původní verze v assembleru zachována pro 32 bitů a poskytnuta nová implementace pro 64 bitů, použijte podmínku 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;

To je minimální mechanická oprava a je na ni třeba pohlížet jako na dočasnou. Kódová základna, která ve svém nitru nese na architekturu vázaný assembler v pomocné funkci, jejímž jediným účelem je oříznutí čísla s plovoucí řádovou čárkou na celé číslo, s sebou vleče zbytečný dluh. Jakmile potvrdíte, že nic nezávisí na vedlejších efektech FPU ve staré implementaci, může 32bitová větev zcela zmizet

Pokud se tato funkce objeví v komponentě používané ve více jednotkách, prohledejte celou kódovou základnu a najděte _ftol, než se rozhodnete, jak migrovat. Symbol s takovým názvem lze deklarovat na více než jednom místě; linker si jeden vybere a ty ostatní tiše ignoruje, což znamená, že můžete opravit jednu kopii, ale přesto linkovat (propojovat) jinou, které se nikdo nedotkl