Tehnički članak

Portovanje _ftol ugradenog asemblera sa 32-bitnog Delphi-ja na DCC64

Originalni _ftol idiom u 32-bitnom Delphi-ju izgleda kao pametno resenje u jednom redu: Pascal omotac funkcije koji koristi ugradeni asembler za manipulaciju kontrolnom recju x87 FPU-a, iseca vrednost na FPU steku i preuzima rezultat. Ovo se dugo uspesno kompajliralo pod DCC32, sto je upravo razlog zasto je zavrsilo u toliko starih grafickih i PDF unita, a da to niko nije dovodio u pitanje

Promenite ciljnu platformu na 64-bitnu i kompajler ce se zaustaviti sa greskom E1025 Unsupported language feature: 'ASM'. Ta greska nije upozorenje o kompatibilnosti. To znaci da DCC64 uopste nece kompajlirati ovu rutinu, bez obzira na to koliko je asemblerski kod ranije dobro radio

Originalna 32-bitna verzija je obicno izgledala otprilike ovako:

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

Taj blok asm unutar Pascal tela begin...end je upravo ono sto DCC64 odbija. Ova dva kompajlera imaju razlicita pravila o tome gde je asemblerski kod dozvoljen, a ta granica je vazna

Zasto DCC64 povlaci granicu na drugaciji nacin

DCC32 dozvoljava ugradeni asembler unutar obicnih Pascal rutina. Kompajler poznaje 32-bitnu konvenciju pozivanja i moze da zakljuci gde se nalaze lokalne promenljive i parametri, pa toleriSe asemblerske fragmente koji pristupaju stek okviru preko naziva promenljivih. DCC64 zauzima strozi stav: asemblerski kod mora biti u namenskoj asemblerskoj funkciji, gde je celo telo funkcije asemblerski kod, a konvencija pozivanja se resava eksplicitno. Kombinovanje Pascal koda i asemblera uopste nije podrzano

Osnovni razlog je arhitektonske prirode. U 64-bitnoj Windows konvenciji pozivanja (Microsoft ABI), prva cetiri parametra stizu u registrima RCX, RDX, R8 i R9 za celobrojne tipove, ili u XMM0 do XMM3 za brojeve sa pokretnim zarezom. Uobicajeno prenosenje parametara uopste ne ukljucuje x87 FPU; x87 je tehnicki dostupan, ali ga ABI ne koristi za prenos argumenata. Asemblerski kod koji pretpostavlja da je vrednost "na FPU steku" oslanja se na stanje koje 64-bitni ABI nikada ne stvara

Dakle, stari fragment nema samo sintaksni problem. Cak i da ga DCC64 prihvati, pretpostavke o registrima bi bile pogresne

Pisanje ispravne 64-bitne asemblerske verzije

Kada zaista morate da eksportujete simbol _ftol sa konvencijom cdecl radi binarne kompatibilnosti, funkcija mora biti napisana kao cista asemblerska rutina. Pod 64-bitnim ABI-jem, parametar Double stize u XMM0, a celobrojni rezultat na povratku mora biti u RAX. Direktiva .NOFRAME govori kompajleru DCC64 da rutina sama upravlja svojim stekom, sto je prikladno za ovako kratku funkciju lisnu cvora:

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 SSE2 instrukcija za konverziju broja sa pokretnim zarezom dvostruke tacnosti u oznaceni ceo broj sa skracivanjem prema nuli, sto je upravo ono sto bi _ftol trebalo da radi. To je jedna instrukcija koja uzima parametar direktno odande gde ga je ABI ostavio i smesta rezultat tamo gde ga ABI ocekuje. Nema potrebe za zokiranjem kontrolnom recju FPU-a

Imajte na umu da ako ulazna vrednost prekoraci opseg 32-bitnog oznacenog celog broja, CVTTSD2SI vraca nedefinisanu celobrojnu vrednost ($80000000). To je isto ponasanje kao i kod x87 instrukcije fistp za ulaz van opsega. Pre nego sto migraciju proglasite zavrsenom, vredi proveriti da li kod koji poziva ovu funkciju moze da generiSe takve vrednosti

Kada je Trunc bolje resenje

Gorenavedenu asemblersku verziju vredi pisati samo kada imate stvarni zahtev za binarnom kompatibilnoscu: neki eksterni pozivalac ocekuje simbol _ftol sa specificnom konvencijom pozivanja, a vi te pozivaoce ne mozete da promenite. Ta situacija je retka. U vecini slucajeva, _ftol je bio privatna pomocna funkcija koja se koristila samo unutar istog unita i uopste ne postoji spoljna zavisnost od njenog naziva ili konvencije

U tom slucaju, zamenite ga cistim Pascal kodom:

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

Trunc skracuje vrednost prema nuli, sto odgovara onome sto je _ftol radio sa kontrolnom recju x87 postavljenom na rezim skracivanja. Kompajlira se na DCC32 i DCC64 bez ikakvih izmena. Kompajler generise odgovarajucu instrukciju za svaku platformu: na x64 ce obicno ionako emitovati CVTTSD2SI, istu instrukciju kao i u rucno pisanoj verziji. Dobijate identicno ponasanje, bez uslovnog kompajliranja za platforme i bez asemblerskog koda za odrzavanje

Jedna semanticka razlika koju vredi proveriti: Trunc izaziva izuzetak EInvalidOp u podrazumevanoj konfiguraciji Delphi-ja kada je ulaz NaN ili beskonacnost. Instrukcija fistp za x87 u originalnom kodu je samo upisivala bit-obrazac bez generisanja izuzetaka. Ako vas kod prosleduje neobicne vrednosti sa pokretnim zarezom ovoj funkciji, a staro ponasanje je bilo tiho, zastiTite poziv proverama IsNaN i IsInfinite iz unita Math pre nego sto pozovete Trunc

Uslovno kompajliranje kada su obe ciljne platforme aktivne

Neki projekti moraju da nastave sa isporukom i 32-bitnih i 64-bitnih binarnih fajlova. Ako se originalna asemblerska verzija mora zadrzati za 32-bitnu platformu, a nova implementacija obezbediti za 64-bitnu, koristite uslov 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 minimalna mehanicka ispravka i treba je smatrati privremenom. Baza koda koja nosi asemblerski kod specifican za arhitekturu u pomocnoj funkciji cija je jedina svrha skracivanje broja sa pokretnim zarezom u ceo broj nosi nepotreban tehnicki dug. 32-bitna grana moze se potpuno ukloniti cim potvrdite da nista ne zavisi od sporednih efekata FPU-a iz stare implementacije

Ako se funkcija pojavljuje u komponenti koja se koristi u vise unita, pretrazite celu bazu koda za _ftol pre nego sto odlucite kako da je migrirate. Simbol pod tim imenom moze biti deklarisan na vise mesta; linker bira jedan i tiho ignorise ostale, sto znaci da biste mogli da ispravite jednu kopiju, a da se i dalje povezujete sa drugom verzijom koja nije izmenjena