O idiom original do _ftol no Delphi de 32 bits existe por causa de arquivos de objeto C. Quando uma unit linka arquivos .obj produzidos por um compilador C de 32 bits, esses objetos chamam o helper de runtime _ftol a cada conversão de float para inteiro, com o valor deixado no topo da pilha x87 em ST(0) em vez de passado como parâmetro. O substituto do Delphi portanto declara um parâmetro dummy Double, desempilha o ST(0) nele com um bloco de assembly inline de duas linhas e então o trunca em Pascal. Compilou bem sob o DCC32 por muito tempo, que é exatamente por que ele acabou em tantas units antigas de gráficos e PDF sem ninguém questionar
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
Primeiro verifique se um _ftol de 64 bits é necessário de fato. Os compiladores C de 64 bits para Win64, tanto o MSVC quanto a toolchain do C++Builder baseada em Clang, convertem floats para inteiros inline com instruções SSE2 e não emitem chamadas ao _ftol, então arquivos de objeto de 64 bits raramente referenciam o símbolo. Quando você realmente precisa exportar um símbolo _ftol com convenção cdecl por compatibilidade binária, a função deve ser escrita como uma rotina de assembler puro. Sob o ABI de 64 bits, um parâmetro Double chega em XMM0, e um resultado Integer volta em EAX, a metade baixa do RAX. A diretiva .NOFRAME diz ao DCC64 que a rotina gerencia a própria pilha, o que é apropriado para uma leaf function tão curta:
function _ftol: Integer; cdecl;
// valor Double esperado em XMM0 conforme o ABI de 64 bits
asm
.NOFRAME
cvttsd2si eax, xmm0 // trunca para um inteiro de 32 bits, resultado em eax
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
O destino de 32 bits importa. Se a entrada excede a faixa de um inteiro com sinal de 32 bits, o CVTTSD2SI com EAX como destino retorna o valor integer indefinite ($80000000). Escrever em RAX em vez disso produziria uma conversão de 64 bits, e entradas entre 231 e 263 voltariam como quaisquer que sejam seus 32 bits baixos. Esse é o mesmo comportamento do fistp do x87 com entrada fora da faixa. Vale confirmar se seus callers podem produzir valores assim antes de declarar a migração 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}
// caminho de 32 bits: o DCC32 aceita asm inline
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