L'idioma originale _ftol in Delphi a 32 bit assomiglia a una trovata da una sola riga: un wrapper di funzione Pascal che scende nell'assembly inline per manipolare la control word della FPU x87, troncare il valore sullo stack della FPU ed estrarre (pop) il risultato. È stato compilato correttamente sotto DCC32 per lungo tempo, ed è esattamente il motivo per cui è finito in così tante vecchie unità grafiche e PDF senza che nessuno se lo chiedesse.
Passando il target di compilazione a 64 bit, il compilatore si arresta con E1025 Unsupported language feature: 'ASM'. Questo errore non è un avviso di compatibilità. Significa che DCC64 non compilerà affatto la routine, a prescindere da quanto bene funzionasse l'assembly in precedenza.
L'originale a 32 bit si presentava tipicamente in questo modo:
function _ftol(f: Double): Integer; cdecl;
begin
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
end;
Quel blocco asm all'interno di un corpo Pascal begin...end è esattamente ciò che DCC64 rifiuta. I due compilatori hanno regole diverse su dove sia consentito l'assembly, e questo confine è fondamentale.
Perché DCC64 definisce il confine in modo diverso
DCC32 consente l'assembly inline all'interno di normali routine Pascal. Il compilatore conosce la convenzione di chiamata a 32 bit e può determinare la posizione delle variabili locali e dei parametri, tollerando quindi frammenti di assembly che accedono al record di attivazione dello stack per nome. DCC64 assume una posizione più rigida: l'assembly deve trovarsi in una funzione assembler dedicata, in cui l'intero corpo è scritto in assembly e la convenzione di chiamata è gestita esplicitamente. Il codice misto Pascal ed assembly non è affatto supportato.
Il motivo di fondo è architetturale. Nella convenzione di chiamata di Windows a 64 bit (Microsoft ABI), i primi quattro parametri arrivano in RCX, RDX, R8 e R9 per i tipi interi, oppure da XMM0 a XMM3 per i valori a virgola mobile. Non c'è alcun coinvolgimento della FPU x87 nel passaggio standard dei parametri; l'x87 è tecnicamente disponibile, ma l'ABI non lo utilizza per il trasporto degli argomenti. L'assembly che presuppone che un valore sia "sullo stack della FPU" fa riferimento a uno stato che l'ABI a 64 bit non crea mai.
Quindi il vecchio frammento non ha semplicemente un problema di sintassi. Anche se DCC64 lo accettasse, le assunzioni sui registri sarebbero errate.
Scrivere una versione assembler a 64 bit corretta
Quando si ha la reale necessità di esportare un simbolo _ftol con convenzione cdecl per motivi di compatibilità binaria, la funzione deve essere scritta come una routine interamente assembler. Sotto l'ABI a 64 bit, un parametro Double arriva in XMM0 e il risultato intero deve trovarsi in RAX al ritorno. La direttiva .NOFRAME indica a DCC64 che la routine gestisce autonomamente il proprio stack, il che è appropriato per una funzione foglia così breve:
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 è l'istruzione SSE2 per convertire un float a doppia precisione in un intero con segno con troncamento verso lo zero, che è esattamente ciò che dovrebbe fare _ftol. È una sola istruzione, prende il parametro direttamente da dove l'ha lasciato l'ABI e posiziona il risultato dove l'ABI se lo aspetta. Non è necessaria alcuna manipolazione della control word della FPU.
Si noti che se l'input supera l'intervallo di un intero con segno a 32 bit, CVTTSD2SI restituisce il valore intero indefinito ($80000000). Questo è lo stesso comportamento di fistp dell'x87 in caso di input fuori intervallo. Prima di dichiarare completata la migrazione, vale la pena verificare se i chiamanti possono produrre tali valori.
Quando Trunc rappresenta la soluzione migliore
La versione assembler descritta sopra vale la pena di essere scritta solo quando si ha un reale requisito di compatibilità binaria: ad esempio, quando un chiamante esterno si aspetta il simbolo _ftol con una specifica convenzione di chiamata e non è possibile modificare tali chiamanti. Questa situazione è insolita. La maggior parte delle volte, _ftol era un helper privato utilizzato solo all'interno della stessa unità, senza alcuna dipendenza esterna dal suo nome o dalla sua convenzione.
In questo caso, è sufficiente sostituirlo con del semplice codice Pascal:
function _ftol(f: Double): Integer; cdecl;
begin
Result := Trunc(f);
end;
Trunc tronca verso lo zero, in linea con quanto faceva _ftol con la control word dell'x87 impostata in modalità di troncamento. Compila sia su DCC32 sia su DCC64 senza modifiche. Il compilatore genera l'istruzione appropriata per ciascun target: su x64 emetterà solitamente CVTTSD2SI, la stessa istruzione della versione scritta a mano. Si ottiene così lo stesso comportamento, senza costrutti condizionali di piattaforma e senza assembly da mantenere.
L'unica differenza semantica che vale la pena verificare è che Trunc solleva un'eccezione EInvalidOp nella configurazione predefinita di Delphi quando l'input è un NaN o un infinito. L'istruzione fistp dell'x87 nel codice originale scriveva semplicemente un pattern di bit senza sollevare eccezioni. Se il codice passa valori a virgola mobile insoliti a questa funzione e il vecchio comportamento era silenzioso, è opportuno inserire una protezione con IsNaN e IsInfinite del modulo Math prima di chiamare Trunc.
Compilazione condizionale quando entrambi i target rimangono attivi
Alcuni progetti devono continuare a distribuire binari sia a 32 sia a 64 bit. Se la versione assembler originale deve essere conservata per i 32 bit e deve essere fornita una nuova implementazione per i 64 bit, si può utilizzare la direttiva condizionale 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;
Questa rappresenta la correzione meccanica minima ed è opportuno considerarla temporanea. Una base di codice che contiene assembly specifico per l'architettura in un helper il cui unico scopo è il troncamento da virgola mobile a intero accumula debito tecnico non necessario. Il ramo a 32 bit può essere rimosso del tutto una volta confermato che nulla dipende dagli effetti collaterali sulla FPU della vecchia implementazione.
Se la funzione compare in un componente utilizzato in più unità, è consigliabile cercare _ftol nell'intera base di codice prima di decidere come procedere con la migrazione. Un simbolo con quel nome può essere dichiarato in più punti; il linker ne sceglie uno ignorando silenziosamente gli altri, il che significa che si potrebbe correggere una copia ma continuare a effettuare il collegamento a una copia diversa che non è stata modificata.