El idioma original de _ftol en Delphi de 32 bits parece una ingeniosa solución en una línea: un wrapper de función Pascal que cae en ensamblador inline para manipular la palabra de control de la FPU x87, truncar el valor en la pila FPU y extraer el resultado. Compilaba perfectamente bajo DCC32 durante mucho tiempo, que es exactamente la razón por la que acabó en tantas unidades gráficas y PDF antiguas sin que nadie lo cuestionara.
Cambiad el objetivo de compilación a 64 bits y el compilador termina con E1025 Unsupported language feature: 'ASM'. Ese error no es una advertencia de compatibilidad. Significa que DCC64 no compilará la rutina en absoluto, independientemente de lo bien que funcionara el ensamblador antes.
El original de 32 bits normalmente tenía un aspecto similar a este:
function _ftol(f: Double): Integer; cdecl;
begin
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
end;
Ese bloque asm dentro de un cuerpo Pascal begin...end es exactamente lo que DCC64 rechaza. Los dos compiladores tienen reglas diferentes sobre dónde se permite el ensamblador, y el límite importa.
Por qué DCC64 traza la línea de forma diferente
DCC32 permite el ensamblador inline dentro de rutinas Pascal ordinarias. El compilador conoce la convención de llamada de 32 bits y puede razonar sobre dónde viven las variables locales y los parámetros, por lo que tolera fragmentos de ensamblador que acceden al marco de pila por nombre. DCC64 adopta una posición más estricta: el ensamblador debe estar en una función ensambladora dedicada, una donde todo el cuerpo sea ensamblador y la convención de llamada se gestione explícitamente. Pascal mezclado con asm no está soportado en absoluto.
La razón subyacente es arquitectónica. En la convención de llamada de Windows de 64 bits (ABI de Microsoft), los primeros cuatro parámetros llegan en RCX, RDX, R8 y R9 para tipos enteros, o en XMM0 a XMM3 para punto flotante. No hay participación de la FPU x87 en el paso normal de parámetros; x87 está técnicamente disponible pero la ABI no la usa para transportar argumentos. El ensamblador que asume que un valor está «en la pila FPU» está razonando sobre un estado que la ABI de 64 bits nunca crea.
Así que el antiguo fragmento no solo tiene un problema de sintaxis. Incluso si DCC64 lo aceptara, las suposiciones sobre registros serían incorrectas.
Escribir una versión de ensamblador de 64 bits correcta
Cuando realmente necesitáis exportar un símbolo _ftol con convención cdecl para compatibilidad binaria, la función debe escribirse como una rutina puramente ensambladora. Bajo la ABI de 64 bits, un parámetro Double llega en XMM0, y el resultado entero debe estar en RAX al retornar. La directiva .NOFRAME le dice a DCC64 que la rutina gestiona su propia pila, lo cual es apropiado para una función hoja tan corta:
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 es la instrucción SSE2 para convertir un flotante de doble precisión a un entero con signo con truncamiento hacia cero, que es exactamente lo que se supone que hace _ftol. Es una instrucción, toma el parámetro directamente de donde lo dejó la ABI y coloca el resultado donde la ABI lo espera. No se necesita ningún malabarismo con la palabra de control de la FPU.
Notad que si la entrada supera el rango de un entero con signo de 32 bits, CVTTSD2SI devuelve el valor entero indefinido ($80000000). Ese es el mismo comportamiento que fistp de x87 con entrada fuera de rango. Vale la pena confirmar si vuestros llamantes pueden producir tales valores antes de declarar terminada la migración.
Cuándo Trunc es la mejor respuesta
La versión ensambladora anterior solo vale la pena escribirla cuando tenéis un requisito de compatibilidad binaria real: algún llamante externo espera el símbolo _ftol con una convención de llamada específica, y no podéis cambiar esos llamantes. Esa situación es poco habitual. La mayor parte del tiempo, _ftol era un helper privado usado solo dentro de la misma unidad, y no hay ninguna dependencia externa de su nombre o convención.
Para ese caso, reemplazadlo con Pascal simple:
function _ftol(f: Double): Integer; cdecl;
begin
Result := Trunc(f);
end;
Trunc trunca hacia cero, lo cual coincide con lo que hacía _ftol con la palabra de control x87 configurada en modo de truncamiento. Compila en DCC32 y DCC64 sin modificaciones. El compilador genera la instrucción apropiada para cada objetivo: en x64 normalmente emitirá CVTTSD2SI de todos modos, la misma instrucción que la versión escrita a mano. Obtenéis un comportamiento idéntico, sin condicionales de plataforma y sin ensamblador que mantener.
La única diferencia semántica que vale la pena comprobar: Trunc lanza una excepción EInvalidOp en la configuración predeterminada de Delphi cuando la entrada es NaN o infinito. El fistp x87 en el código original simplemente escribía un patrón de bits sin lanzar nada. Si vuestro código alimenta valores de punto flotante inusuales a esta función y el comportamiento anterior era silencioso, proteged con IsNaN e IsInfinite de Math antes de llamar a Trunc.
Compilación condicional cuando ambos objetivos siguen activos
Algunos proyectos deben seguir distribuyendo binarios de 32 y 64 bits. Si la versión ensambladora original debe mantenerse para 32 bits y se debe proporcionar una nueva implementación para 64 bits, usad el condicional 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;
Esa es la corrección mecánica mínima, y vale la pena tratarla como temporal. Una base de código que lleva ensamblador específico de arquitectura en un helper cuyo único propósito es el truncamiento de flotante a entero lleva una deuda innecesaria. La rama de 32 bits puede desaparecer por completo una vez que confirméis que nada depende de los efectos secundarios de la FPU de la antigua implementación.
Si la función aparece en un componente usado en múltiples unidades, buscad _ftol en toda la base de código antes de decidir cómo migrar. Un símbolo con ese nombre puede declararse en más de un lugar; el enlazador elige uno e ignora silenciosamente los demás, lo que significa que podríais arreglar una copia y seguir enlazando contra otra diferente que no ha sido tocada.