A instrução original _ftol no Delphi de 32 bits parece um truque inteligente de uma única linha: um wrapper de função Pascal que recorre a assembly inline para manipular a palavra de controlo do FPU x87, truncar o valor na pilha do FPU e remover o resultado. Compilou sem problemas sob o DCC32 durante muito tempo, sendo precisamente por isso que acabou em tantas unidades antigas de gráficos e PDF sem que ninguém a questionasse.
Mude o alvo de compilação para 64 bits e o compilador termina com E1025 Unsupported language feature: 'ASM'. Este erro não é um aviso de compatibilidade. Significa que o DCC64 não compilará a rotina de todo, independentemente de quão bem o assembly funcionasse anteriormente.
O original de 32 bits assemelhava-se tipicamente a isto:
function _ftol(f: Double): Integer; cdecl;
begin
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
end;
Aquele bloco asm dentro do corpo begin...end de um Pascal é exatamente o que o DCC64 recusa. Os dois compiladores têm regras diferentes sobre onde o assembly é permitido, e essa fronteira faz a diferença.
Porquê o DCC64 define os limites de forma diferente
O DCC32 permite assembly inline dentro de rotinas Pascal comuns. O compilador conhece a convenção de chamada de 32 bits e consegue determinar onde residem as variáveis locais e os parâmetros, pelo que tolera fragmentos de assembly que acedem à stack frame pelo nome. O DCC64 assume uma postura mais estrita: o assembly deve estar numa função assembler dedicada, onde todo o corpo é assembly e a convenção de chamada é gerida explicitamente. A mistura de Pascal com asm não é de todo suportada.
A razão subjacente é arquitetural. Na convenção de chamada do Windows de 64 bits (Microsoft ABI), os primeiros quatro parâmetros chegam em RCX, RDX, R8 e R9 para tipos inteiros, ou em XMM0 a XMM3 para vírgula flutuante. Não há envolvimento do FPU x87 na passagem normal de parâmetros; o x87 está tecnicamente disponível, mas a ABI não o utiliza para transporte de argumentos. O assembly que assume que um valor está "na pilha do FPU" está a assumir um estado que a ABI de 64 bits nunca cria.
Portanto, o fragmento antigo não tem apenas um problema de sintaxe. Mesmo que o DCC64 o aceitasse, os pressupostos dos registadores estariam incorretos.
Escrever uma versão assembler de 64 bits adequada
Quando precisa genuinamente de exportar um símbolo _ftol com a convenção cdecl para compatibilidade binária, a função tem de ser escrita como uma rotina assembler pura. Sob a ABI de 64 bits, um parâmetro Double chega em XMM0, e o resultado inteiro tem de estar em RAX no retorno. A diretiva .NOFRAME indica ao DCC64 que a rotina gere a sua própria pilha, o que é apropriado para uma função folha (leaf function) tão curta:
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 é a instrução SSE2 para converter um float de dupla precisão num inteiro com sinal com truncatura em direção a zero, que é exatamente o que se espera que _ftol faça. É uma única instrução, que recebe o parâmetro diretamente de onde a ABI o deixou e coloca o resultado onde a ABI o espera. Não é necessário fazer malabarismos com a palavra de controlo do FPU.
Note que se a entrada exceder o intervalo de um inteiro com sinal de 32 bits, CVTTSD2SI devolve o valor inteiro indefinido ($80000000). Esse é o mesmo comportamento do fistp do x87 para entradas fora do intervalo. Convém confirmar se os seus chamadores podem produzir tais valores antes de declarar a migração concluída.
Quando Trunc é a melhor resposta
A versão assembler acima só vale a pena ser escrita quando tem um requisito real de compatibilidade binária: algum chamador externo espera o símbolo _ftol com uma convenção de chamada específica e não pode alterar esses chamadores. Essa situação é invulgar. Na maioria das vezes, o _ftol era um auxiliar privado utilizado apenas dentro da própria unidade, não existindo qualquer dependência externa do seu nome ou convenção.
Para esse caso, substitua-o por Pascal simples:
function _ftol(f: Double): Integer; cdecl;
begin
Result := Trunc(f);
end;
Trunc trunca em direção a zero, o que corresponde ao que o _ftol fazia com a palavra de controlo do x87 definida para o modo de truncatura. Compila no DCC32 e no DCC64 sem modificação. O compilador gera a instrução adequada para cada alvo: em x64, emitirá tipicamente CVTTSD2SI de qualquer forma, a mesma instrução da versão escrita à mão. Obtém um comportamento idêntico, sem condicionais de plataforma e sem assembly para manter.
A única diferença semântica que vale a pena verificar: o Trunc gera uma exceção EInvalidOp na configuração padrão do Delphi quando a entrada é um NaN ou infinito. O fistp do x87 no código original apenas escrevia um padrão de bits sem gerar qualquer exceção. Se o seu código passar valores invulgares de vírgula flutuante a esta função e o comportamento antigo for silencioso, proteja com IsNaN e IsInfinite de Math antes de chamar Trunc.
Compilação condicional quando ambos os alvos permanecem ativos
Alguns projetos têm de continuar a disponibilizar binários de 32 bits e 64 bits. Se a versão original em assembler tiver de ser mantida para 32 bits e for fornecida uma nova implementação para 64 bits, utilize a 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;
Esta é a correção mecânica mínima e convém tratá-la como temporária. Uma base de código que carrega assembly específico da arquitetura num auxiliar cujo único propósito é a truncatura de float para inteiro está a acumular dívida técnica desnecessária. O ramo de 32 bits pode ser totalmente removido assim que confirmar que nada depende dos efeitos secundários do FPU da implementação antiga.
Se a função aparecer num componente utilizado em múltiplas unidades, pesquise em toda a base de código por _ftol antes de decidir como migrar. Um símbolo com esse nome pode ser declarado em mais do que um local; o linker escolhe um e ignora silenciosamente os outros, o que significa que pode corrigir uma cópia e ainda assim ligar contra uma diferente que não foi tocada.