Oryginalny idiom _ftol w 32-bitowym środowisku Delphi wygląda na sprytną jedno-linijkową obudowę w postaci funkcji Pascal, która przechodzi we wbudowany kod asemblera w celu manipulacji słowem kontrolnym koprocesora x87 FPU, obcięcia wartości na stosie FPU i zdjęcia wyniku. Kompilowało się to bez problemów z kompilatorem DCC32 przez długi czas, co z kolei jest powodem, dlaczego znalazło się w wielu starszych modułach graficznych oraz PDF, bez jakiegokolwiek kwestionowania
Zmień cel kompilacji na architekturę 64-bitową, a kompilator przerwie pracę zwracając błąd E1025 Unsupported language feature: 'ASM'. Błąd ten nie jest ostrzeżeniem o kompatybilności. Oznacza on, że kompilator DCC64 w ogóle nie skompiluje procedury, bez względu na to jak dobrze kod asemblera działał w przeszłości
32-bitowa oryginalna wersja zazwyczaj wyglądała mniej więcej w ten sposób:
function _ftol(f: Double): Integer; cdecl;
begin
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
end;
Ten blok asm wewnątrz ciała begin...end w języku Pascal to dokładnie to, czego kompilator DCC64 odmawia przyjęcia. Obydwa kompilatory mają odmienne reguły dotyczące tego, gdzie dozwolone jest wykorzystywanie asemblera, a granica ta ma ogromne znaczenie
Dlaczego kompilator DCC64 kreśli granicę w inny sposób
DCC32 pozwala na wbudowany asembler wewnątrz zwykłych procedur Pascala. Kompilator zna konwencję wywołań 32-bitowego środowiska i potrafi wywnioskować, gdzie żyją zmienne lokalne oraz parametry, więc toleruje fragmenty asemblera, które sięgają do ramki stosu po ich nazwach. DCC64 przyjmuje bardziej rygorystyczne stanowisko: asembler musi znajdować się w dedykowanej funkcji asemblera, jednej w której całe ciało stanowi asembler, a konwencja wywołania jest obsługiwana w sposób jawny. Mieszany kod Pascala z asemblerem nie jest w ogóle obsługiwany
Powodem leżącym u podstaw jest architektura. W konwencji wywołań 64-bitowego systemu Windows (Microsoft ABI), pierwsze cztery parametry docierają w rejestrach RCX, RDX, R8 oraz R9 dla typów całkowitych, lub w rejestrach od XMM0 do XMM3 dla typów zmiennoprzecinkowych. W normalnym przekazywaniu parametrów nie bierze się udziału koprocesora x87; x87 rzecz ujmując technologicznie jest dostępne, ale ABI nie korzysta z niego do transportowania argumentów. Asembler, który zakłada, że wartość znajduje się na stosie FPU, opiera swoją logikę na stanie, którego 64-bitowe ABI nigdy nie utworzy
Z tego względu, stary fragment kodu nie posiada tylko problemu ze składnią. Nawet gdyby kompilator DCC64 go zaakceptował, założenia dotyczące rejestru byłyby błędne
Pisanie prawidłowej 64-bitowej wersji asemblera
W sytuacji w której rzeczywiście potrzebujesz wyeksportować symbol _ftol z konwencją cdecl z potrzeby na kompatybilność binarną, funkcja musi być napisana jako czysta asemblerowa procedura. W konwencji wywołań 64-bitowego ABI parametr typu Double dociera w XMM0, a wynik całkowity musi znaleźć się w rejestrze RAX na powrocie. Dyrektywa .NOFRAME informuje kompilator DCC64, że procedura zarządza swoim własnym stosem, co jest właściwe dla liściowej funkcji tak krótkiej:
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 to instrukcja SSE2 służąca do konwertowania zmiennoprzecinkowych wartości podwójnej precyzji na liczbę całkowitą ze znakiem wraz z obcięciem w kierunku zera, co jest dokładnie tym, co powinno robić _ftol. Jest to jedna instrukcja, która pobiera parametr bezpośrednio z miejsca, w którym ABI go pozostawiło i umieszcza wynik tam, gdzie oczekuje go ABI. Nie ma potrzeby żonglowania słowem kontrolnym koprocesora FPU
Zwróć uwagę, że jeśli dane wejściowe przekraczają zakres 32-bitowej liczby całkowitej ze znakiem, instrukcja CVTTSD2SI zwraca nieokreśloną wartość całkowitą ($80000000). To jest dokładnie to samo zachowanie co koprocesor x87 w przypadku fistp w obliczu danych wyjściowych wykraczających poza zakres. To czy twoje wywołujące oprogramowanie może wygenerować takie wartości, warto potwierdzić przed uznaniem procesu migracji za ukończony
Kiedy Trunc jest lepszym rozwiązaniem
Powyższa wersja napisana w asemblerze jest warta zachodu jedynie wtedy, gdy posiadasz rzeczywisty wymóg na zgodność binarną: na przykład ktoś oczekuje z zewnątrz wystawienia symbolu _ftol w specyficznej konwencji wywołania, a ty po prostu nie masz możliwości by zmodyfikować kod na takich usługobiorcach. Taka sytuacja jest niezwykle rzadko spotykana. W większości przypadków, _ftol stanowił po prostu prywatnego pomocnika używanego tylko wewnątrz tego samego modułu i nie ma na dobrą sprawę żadnych zewnętrznych zależności powiązanych w jakikolwiek sposób z jego nazwą czy też z samą konwencją
Dla tego typu zastosowań podmień tę funkcję na tę napisaną w czystym Pascalu:
function _ftol(f: Double): Integer; cdecl;
begin
Result := Trunc(f);
end;
Polecenie Trunc ucina wartości w kierunku zera, co pasuje bezpośrednio do tego, czym wcześniej zajmowało się _ftol z ustawionym na tryb obcinania kontrolnym poleceniem pod system dla x87. Bez problemu skompiluje się to w kodzie dla DCC32 oraz DCC64 i nie wymaga od ciebie żadnych modyfikacji. Kompilator sam wygeneruje dla ciebie najodpowiedniejszą instrukcję dla każdego środowiska docelowego: w przypadku architektury x64 tak czy siak wyemituje instrukcję CVTTSD2SI, a więc dokładnie to samo, co w ręcznie wprowadzonym wariancie. Uzyskujesz identyczne zachowanie bez uwarunkowań platformowych, i nie musisz utrzymać asemblerowego kodu
Jedyna różnica semantyczna warta sprawdzenia: polecenie Trunc wywołuje wyjątek EInvalidOp w domyślnej konfiguracji Delphi, gdy dane wejściowe to wartości NaN lub nieskończoność. Instrukcja fistp w oryginalnym kodzie koprocesora x87 po prostu zapisywała wzorzec bitowy bez wywoływania czegokolwiek. Jeśli twój kod dostarcza do tej funkcji niezwykłe zmiennoprzecinkowe wartości, a stare zachowanie było ciche, zabezpiecz się korzystaniem z IsNaN i IsInfinite z modułu Math przed wywołaniem Trunc
Kompilacja warunkowa gdy oba środowiska docelowe pozostają aktywne
Niektóre projekty muszą nadal dostarczać binarki dla środowisk 32- oraz 64-bitowych. Jeśli oryginalna wersja asemblerowa musi zostać zachowana dla 32 bitów, a nowa implementacja dostarczona dla 64 bitów, użyj warunku 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 jest minimalna mechaniczna poprawka, a którą warto potraktować jako tymczasowe rozwiązanie. Baza kodu, która przenosi specyficzny dla architektury asembler w pomocniku, którego jedynym celem jest obcinanie ze zmiennoprzecinkowych do całkowitych, niesie ze sobą niepotrzebny dług. 32-bitowa gałąź może całkowicie zniknąć, jak tylko potwierdzisz, że nic nie zależy od efektów ubocznych FPU ze starej implementacji
Jeśli funkcja pojawia się w komponencie wykorzystywanym w wielu modułach, przeszukaj całą bazę kodu w poszukiwaniu _ftol przed podjęciem decyzji, jak przenieść kod. Symbol o takiej nazwie może zostać zadeklarowany w więcej niż jednym miejscu; konsolidator dobierze jeden i dyskretnie zignoruje resztę, co oznacza, że możesz naprawić jedną kopię i nadal linkować do innej, która nie została tknięta