A linguagem original _ftol no Delphi de 32 bits parece uma linha inteligente: um wrapper de função Pascal que se transforma em assembly inline para manipular a palavra de controle da FPU x87, truncar o valor na pilha da FPU e retirar o resultado. Ele foi compilado perfeitamente no DCC32 por um longo tempo, e é exatamente por isso que ele acabou em tantas unidades de gráficos e PDF antigas sem que ninguém o questionasse
Mude o destino de compilação para 64 bits e o compilador terminará com E1025 Unsupported language feature: 'ASM'. Esse erro não é um aviso de compatibilidade. Ele significa que o DCC64 não compilará a rotina de forma alguma, independentemente de quão bem o assembly tenha funcionado antes
O original de 32 bits normalmente parecia com isso:
function _ftol(f: Double): Integer; cdecl;
begin
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
end;
Esse bloco asm dentro de um corpo begin...end em Pascal é exatamente o que o DCC64 recusa. Os dois compiladores têm regras diferentes sobre onde o assembly é permitido, e o limite é importante
Por que o DCC64 traça a linha de maneira diferente
O DCC32 permite assembly inline dentro de rotinas Pascal comuns. O compilador conhece a convenção de chamada de 32 bits e pode raciocinar sobre onde as variáveis locais e os parâmetros residem, portanto, tolera fragmentos de assembly que alcançam o quadro da pilha pelo nome. O DCC64 adota uma posição mais rígida: o assembly deve estar em uma função de montador dedicada, onde o corpo inteiro é assembly e a convenção de chamada é manipulada explicitamente. O misto de Pascal com asm não é compatível de forma alguma
O motivo subjacente é arquitetônico. Na convenção de chamada do Windows de 64 bits (ABI da Microsoft), os quatro primeiros parâmetros chegam em RCX, RDX, R8 e R9 para os tipos inteiros, ou em XMM0 a XMM3 para pontos flutuantes. Não há envolvimento da 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. Um assembly que supõe que um valor está "na pilha da FPU" está raciocinando sobre um estado que a ABI de 64 bits nunca cria
Portanto, o fragmento antigo não tem apenas um problema de sintaxe. Mesmo se o DCC64 o aceitasse, as suposições do registrador estariam incorretas
Escrevendo uma versão de montador de 64 bits adequada
Quando você realmente precisa exportar um símbolo _ftol com a convenção cdecl para compatibilidade binária, a função deve ser escrita como uma rotina puramente de montador. Na ABI de 64 bits, um parâmetro Double chega em XMM0 e o resultado inteiro deve estar em RAX no retorno. A diretiva .NOFRAME informa ao DCC64 que a rotina gerencia sua própria pilha, o que é apropriado para uma função folha 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 conversão de um float de precisão dupla em um número inteiro assinado com truncamento em direção a zero, que é exatamente o que _ftol deve fazer. É apenas uma instrução, pega o parâmetro diretamente de onde a ABI o deixou e coloca o resultado onde a ABI o espera. Não é necessário nenhum malabarismo com a palavra de controle da FPU
Observe que, se a entrada exceder o intervalo de um inteiro assinado de 32 bits, CVTTSD2SI retornará o valor indefinido do inteiro ($80000000). É o mesmo comportamento do fistp x87 na entrada fora do intervalo. Vale a pena confirmar se seus chamadores podem produzir esses valores antes de declarar a migração como concluída
Quando Trunc é a melhor resposta
A versão do montador acima só vale a pena ser escrita quando você tiver um requisito de compatibilidade binária real: algum chamador externo espera o símbolo _ftol com uma convenção de chamada específica e você não pode alterar esses chamadores. Essa situação é incomum. Na maioria das vezes, o _ftol era um auxiliar particular usado apenas na mesma unidade e não há nenhuma dependência externa em relação a seu nome ou convenção
Para esse caso, substitua-o pelo Pascal simples:
function _ftol(f: Double): Integer; cdecl;
begin
Result := Trunc(f);
end;
O Trunc se trunca em direção ao zero, o que corresponde ao que o _ftol estava fazendo com a palavra de controle x87 definida para o modo de truncamento. Ele é compilado no DCC32 e no DCC64 sem nenhuma modificação. O compilador gera a instrução adequada para cada alvo: no x64, ele normalmente emitirá o CVTTSD2SI de qualquer maneira, a mesma instrução da versão escrita à mão. Você obtém comportamento idêntico, sem condicionais de plataforma e sem assembly a ser mantido
Uma diferença semântica que vale a pena conferir: O Trunc gera uma exceção EInvalidOp na configuração padrão do Delphi quando a entrada é um NaN ou um infinito. O x87 fistp no código original apenas gravava um padrão de bits sem gerar nada. Se o seu código alimentar essa função com valores de ponto flutuante não usuais e o comportamento antigo for silencioso, proteja com IsNaN e IsInfinite do Math antes de chamar o Trunc
Compilação condicional quando ambos os destinos continuam ativos
Alguns projetos precisam continuar fornecendo binários de 32 e 64 bits. Se a versão original do montador precisar ser mantida para 32 bits e uma nova implementação for fornecida para 64 bits, use 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;
Essa é a correção mecânica mínima e vale a pena encará-la como temporária. Uma base de código que carrega assembly específico da arquitetura em um auxiliar cujo único propósito é o truncamento de ponto flutuante para inteiro carrega um débito desnecessário. A ramificação de 32 bits poderá desaparecer inteiramente quando você confirmar que nada depende dos efeitos colaterais da FPU da antiga implementação
Se a função aparecer em um componente usado em várias unidades, pesquise o _ftol em toda a base de código antes de decidir como migrar. Um símbolo com esse nome pode ser declarado em mais de um lugar; o vinculador escolhe um e ignora silenciosamente os outros, o que significa que você pode corrigir uma cópia e ainda se vincular a uma cópia diferente que não foi tocada