Tehnični članak

Prenos inline zbirnega jezika _ftol iz 32-bitnega Delphija v DCC64

Prvotni idiom _ftol v 32-bitnem Delphiju je videti kot pametna enovrstičnica: ovojna funkcija Pascal, ki spusti kodo v inline zbirni jezik (inline assembly) za manipulacijo s kontrolno besedo (control word) x87 FPU, skrajšanje (truncate) vrednosti na skladu FPU in vračanje rezultata (pop). Pod DCC32 se je dolgo časa prevajala brez težav, zato je končala v številnih starejših grafičnih in PDF enotah, ne da bi se kdorkoli o njej spraševal

Če cilj prevajanja (build target) spremenite v 64-bitnega, se prevajalnik prekine z napako E1025 Unsupported language feature: 'ASM'. Ta napaka ni opozorilo o združljivosti. Pomeni, da DCC64 sploh ne bo prevedel rutine, ne glede na to, kako dobro je zbirni jezik deloval prej

Izvirnik 32-bitne različice je bil običajno videti takole:

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

Tisti blok asm znotraj telesa Pascal begin...end je točno to, kar DCC64 zavrača. Prevajalnika imata različna pravila glede tega, kje je dovoljen zbirni jezik, in ta meja je pomembna

Zakaj DCC64 postavlja mejo drugače

DCC32 dovoljuje inline zbirni jezik znotraj navadnih Pascal rutin. Prevajalnik pozna 32-bitno klicno konvencijo (calling convention) in lahko sklepa, kje se nahajajo lokalne spremenljivke in parametri, zato tolerira fragmente zbirnega jezika, ki po imenu sežejo v okvir sklada (stack frame). DCC64 zavzema strožje stališče: zbirni jezik mora biti v namenski funkciji (assembler function), kjer je celotno telo v zbirnem jeziku, klicna konvencija pa je obravnavana eksplicitno. Mešanje Pascala in asm kode sploh ni podprto

Osnovni razlog je arhitekturni. V 64-bitni Windows klicni konvenciji (Microsoft ABI) pridejo prvi štirje parametri v RCX, RDX, R8 in R9 za celoštevilske tipe (integer types) ali v XMM0 do XMM3 za tipe s plavajočo vejico (floating-point). x87 FPU ni vključen v normalno prenašanje parametrov; x87 je tehnično na voljo, vendar ga ABI ne uporablja za prenos argumentov. Zbirni jezik, ki predpostavlja, da je vrednost "na skladu FPU", sklepa o stanju, ki ga 64-bitni ABI nikoli ne ustvari

Zato stari fragment nima samo sintaktične težave. Tudi če bi ga DCC64 sprejel, bi bile predpostavke o registrih napačne

Pisanje ustrezne 64-bitne različice v zbirnem jeziku

Kadar resnično morate izvoziti simbol _ftol s konvencijo cdecl zaradi binarne združljivosti, mora biti funkcija napisana kot čista zbirno-jezikovna rutina (pure assembler routine). V 64-bitnem ABI parameter Double prispe v XMM0, celoštevilski rezultat pa mora biti ob vrnitvi v RAX. Direktiva .NOFRAME pove DCC64, da rutina sama upravlja svoj sklad, kar je primerno za listno funkcijo (leaf function), ki je tako kratka:

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 instrukcija SSE2 za pretvorbo plavajoče vejice dvojne natančnosti v označeno celo število z obrezovanjem proti nič (truncation toward zero), kar je točno to, kar naj bi _ftol naredil. To je ena instrukcija, vzame parameter neposredno od tam, kjer ga je pustil ABI, in postavi rezultat tam, kjer ga ABI pričakuje. Žongliranje s kontrolno besedo FPU ni potrebno

Upoštevajte, da če vnos preseže obseg 32-bitnega označenega celega števila, CVTTSD2SI vrne celoštevilsko nedoločeno vrednost ($80000000). To je enako vedenje kot x87 fistp pri vnosu izven dosega. Preden prenos razglasite za končanega, je vredno preveriti, ali vaši klicatelji lahko ustvarijo takšne vrednosti

Kdaj je Trunc boljša rešitev

Zgornjo zbirno-jezikovno različico je vredno napisati le, ko imate dejansko zahtevo po binarni združljivosti: neki zunanji klicatelj pričakuje simbol _ftol z določeno klicno konvencijo in teh klicateljev ne morete spremeniti. Ta situacija je neobičajna. Večino časa je bil _ftol zasebni pomočnik, ki se je uporabljal samo znotraj iste enote in sploh ni bilo zunanje odvisnosti od njegovega imena ali konvencije

Za ta primer ga zamenjajte z navadnim Pascalom:

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

Trunc obreže (truncate) proti ničli, kar se ujema s tem, kar je naredil _ftol z nadzorno besedo x87, nastavljeno na način obrezovanja. Pod DCC32 in DCC64 se prevaja brez sprememb. Prevajalnik generira ustrezno instrukcijo za vsak cilj: na x64 bo običajno vseeno izdal CVTTSD2SI, isto instrukcijo kot pri ročno napisani različici. Dobite enako obnašanje, brez pogojev za platforme in nobenega zbirnega jezika za vzdrževanje

Edina pomenska razlika, ki jo velja preveriti: Trunc sproži izjemo EInvalidOp v privzeti konfiguraciji Delphija, ko je vnos NaN ali neskončnost. x87 fistp v prvotni kodi je samo napisal bitni vzorec (bit pattern), ne da bi karkoli dvignil. Če vaša koda tej funkciji podaja nenavadne vrednosti s plavajočo vejico in je bilo staro vedenje tiho, jo pred klicem Trunc zaščitite z IsNaN in IsInfinite iz Math

Pogojno prevajanje, ko oba cilja ostaneta aktivna

Nekateri projekti morajo še naprej pošiljati tako 32-bitne kot 64-bitne binarne datoteke. Če je treba obdržati prvotno zbirno-jezikovno različico za 32-bit in zagotoviti novo izvedbo za 64-bit, uporabite pogojnik 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 minimalni mehanski popravek in ga je vredno obravnavati kot začasnega. Baza kode (codebase), ki prenaša zbirni jezik, specifičen za posamezno arhitekturo, v pomočniku, katerega edini namen je obrezovanje s plavajočo vejico v celo število, nosi nepotreben dolg. 32-bitna veja lahko popolnoma izgine, ko potrdite, da se nič ne zanaša na FPU stranske učinke stare izvedbe

Če se funkcija pojavi v komponenti, ki se uporablja v več enotah, pred odločitvijo o načinu migracije preiščite celotno zbirko kode za _ftol. Simbol s tem imenom je lahko deklariran na več mestih; povezovalnik izbere enega in tiho prezre ostale, kar pomeni, da bi lahko popravili eno kopijo in se še vedno povezali z drugo, ki se je ni nihče dotaknil