El modismo original _ftol en Delphi de 32 bits parece una ingeniosa línea única: un contenedor de función Pascal que recurre al ensamblador en línea para manipular la palabra de control de la FPU x87, truncar el valor en la pila de la FPU y extraer el resultado. Se compiló sin problemas en DCC32 durante mucho tiempo, que es exactamente la razón por la que terminó en tantas unidades gráficas y PDF antiguas sin que nadie lo cuestionara
Al cambiar el destino de compilación a 64 bits, 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 generalmente se veía así:
function _ftol(f: Double): Integer; cdecl;
begin
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
end;
Ese bloque asm dentro del cuerpo de un begin...end de Pascal es exactamente lo que rechaza DCC64. Los dos compiladores tienen reglas diferentes sobre dónde se permite el código ensamblador, y el límite importa
Por qué DCC64 establece el límite de manera diferente
DCC32 permite ensamblador en línea dentro de rutinas Pascal ordinarias. El compilador conoce la convención de llamada de 32 bits y puede razonar sobre dónde residen las variables locales y los parámetros, por lo que tolera fragmentos de ensamblador que acceden al marco de la pila por nombre. DCC64 adopta una posición más estricta: el ensamblador debe estar en una función de ensamblador dedicada, una donde todo el cuerpo sea ensamblador y la convención de llamada se maneje explícitamente. La mezcla de Pascal y asm no es compatible en absoluto
La razón subyacente es arquitectónica. En la convención de llamada de Windows de 64 bits (ABI de Microsoft), los cuatro primeros 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 lo usa para el transporte de argumentos. El ensamblador que asume que un valor está "en la pila de la 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 de los registros serían incorrectas
Escribiendo una versión adecuada en ensamblador de 64 bits
Cuando usted realmente necesita exportar un símbolo _ftol con la convención cdecl para compatibilidad binaria, la función debe escribirse como una rutina de ensamblador pura. 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 administra 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 valor de punto flotante de doble precisión a un entero con signo con truncamiento hacia cero, que es exactamente lo que se supone que debe hacer _ftol. Es una sola instrucción, toma el parámetro directamente de donde la ABI lo dejó y coloca el resultado donde la ABI lo espera. No se necesitan malabares con la palabra de control de la FPU
Tenga en cuenta que si la entrada excede el rango de un entero con signo de 32 bits, CVTTSD2SI devuelve el valor indefinido de entero ($80000000). Ese es el mismo comportamiento que fistp de x87 en entradas fuera de rango. Vale la pena confirmar si sus invocadores pueden producir tales valores antes de declarar finalizada la migración
Cuándo Trunc es la mejor respuesta
Solo vale la pena escribir la versión en ensamblador anterior cuando usted tiene un requisito real de compatibilidad binaria: algún invocador externo espera el símbolo _ftol con una convención de llamada específica y no puede cambiar esos invocadores. Esa situación es poco común. La mayoría de las veces, _ftol era un ayudante privado que se usaba solo dentro de la misma unidad y no hay dependencia externa de su nombre o convención en absoluto
Para ese caso, reemplácelo con Pascal simple:
function _ftol(f: Double): Integer; cdecl;
begin
Result := Trunc(f);
end;
Trunc trunca hacia cero, lo que coincide con lo que _ftol estaba haciendo con la palabra de control x87 configurada en modo de truncamiento. Se compila en DCC32 y DCC64 sin modificaciones. El compilador genera la instrucción adecuada para cada plataforma: en x64 normalmente emitirá CVTTSD2SI de todos modos, la misma instrucción que la versión escrita a mano. Usted obtiene un comportamiento idéntico, sin condicionales de plataforma y sin código 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 un NaN o infinito. La instrucción fistp de x87 en el código original simplemente escribía un patrón de bits sin lanzar nada. Si su código pasa valores inusuales de punto flotante a esta función y el comportamiento antiguo era silencioso, protéjalo con IsNaN e IsInfinite de Math antes de llamar a Trunc
Compilación condicional cuando ambos destinos siguen activos
Algunos proyectos deben seguir publicando binarios tanto de 32 bits como de 64 bits. Si la versión original en ensamblador se debe conservar para 32 bits y se debe proporcionar una nueva implementación para 64 bits, utilice 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 contiene ensamblador específico de la arquitectura en un ayudante cuyo único propósito es el truncamiento de punto flotante a entero conlleva una deuda innecesaria. La rama de 32 bits puede desaparecer por completo una vez que confirme que nada depende de los efectos secundarios de la FPU de la antigua implementación
Si la función aparece en un componente utilizado en varias unidades, busque _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 y silenciosamente ignora los demás, lo que significa que usted podría arreglar una copia y aún enlazar contra otra diferente que no ha sido tocada