Artigo Técnico

Portar Assembly Inline _ftol do Delphi de 32 bits para DCC64

O idioma original _ftol em Delphi de 32 bits assemelha-se a uma engenhosa instrução de linha única: um wrapper de função Pascal que mergulha no assembly inline para manipular a palavra de controlo x87 FPU, truncar o valor na pilha FPU e extrair o resultado. Este formato compilou perfeitamente no DCC32 durante bastante tempo, sendo essa a razão exata pela qual acabou em tantas unidades mais antigas de gráficos e PDF sem que alguém o questionasse

Mude o destino da compilação para 64 bits e o compilador termina com a mensagem E1025 Unsupported language feature: 'ASM'. Esse erro não constitui um aviso de compatibilidade. O que significa é que o DCC64 não irá sequer compilar a rotina de todo, independentemente da eficácia com que o assembly tenha funcionado no passado

A versão original de 32 bits apresentava, regra geral, o seguinte aspeto:

function _ftol(f: Double): Integer; cdecl;
begin
  asm
    lea   eax, f
    fstp  qword ptr [eax]
  end;
  Result := Trunc(f);
end;

Esse bloco asm no interior de um corpo begin...end em Pascal é exatamente aquilo que o DCC64 recusa. Os dois compiladores possuem regras diferentes relativamente à zona em que o uso do assembly é autorizado, e a fronteira entre eles tem relevância

A razão pela qual o DCC64 estabelece o limite de forma diferente

O DCC32 permite assembly inline no interior de rotinas Pascal comuns. O compilador conhece a convenção de chamada de 32 bits e deduz onde residem as variáveis locais e os parâmetros, pelo que tolera fragmentos de assembly que acedem à "stack frame" por nome. O DCC64 adota uma posição mais restrita: o assembly tem de estar numa função assembler dedicada, na qual todo o corpo é assembly e a convenção de chamada é tratada explicitamente. A mistura de Pascal com asm não é de todo suportada

O motivo subjacente é de natureza arquitetural. 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 os tipos de vírgula flutuante. A FPU x87 não tem qualquer envolvimento na passagem normal de parâmetros; a FPU x87 está tecnicamente disponível, mas a ABI não a utiliza para transporte de argumentos. Um código em assembly que presuma que um valor se encontra "na pilha da FPU" está a raciocinar sobre um estado que a ABI de 64 bits nunca chega a criar

Logo, o antigo fragmento não tem apenas um problema de sintaxe. Mesmo que o DCC64 o aceitasse, os pressupostos subjacentes aos registos estariam errados

Escrever uma versão assembler adequada de 64 bits

Quando tem uma necessidade genuína de exportar um símbolo _ftol com a convenção cdecl por motivos de compatibilidade binária, a função tem de ser escrita como uma rotina puramente em assembler. Ao abrigo da ABI de 64 bits, um parâmetro Double chega em XMM0 e o resultado inteiro tem de estar presente em RAX no momento do retorno. A diretiva .NOFRAME informa o DCC64 de que a rotina procede à gestão da sua própria pilha (stack), o que se revela adequado para uma função folha tão curta quanto esta:

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 número de vírgula flutuante de precisão dupla num inteiro com sinal através de truncamento em direção ao zero, que é exatamente o que _ftol supostamente deve fazer. Trata-se de apenas uma instrução, obtém o parâmetro de forma direta no local onde a ABI o deixou e coloca o resultado no local exato em que a ABI o espera. Não é necessário nenhum tipo de malabarismo com a palavra de controlo da FPU

Tenha em atenção que, se a entrada ultrapassar o intervalo de um inteiro de 32 bits com sinal, CVTTSD2SI devolve o valor de um inteiro indefinido ($80000000). É exatamente este o comportamento que se regista no caso do x87 fistp face a uma entrada fora dos limites. Confirme sempre, e antes de dar a migração como terminada, se quem efetua as chamadas é capaz de gerar tais valores

Quando Trunc é a melhor resposta

A versão em assembler apresentada acima só vale a pena quando se tem uma verdadeira exigência de compatibilidade binária: um interveniente de chamada externa aguarda o símbolo _ftol com uma convenção de chamada específica, e o programador não pode alterar esses intervenientes de chamada (callers). Trata-se de uma situação fora do comum. Na maior parte do tempo, o _ftol não passou de um simples auxiliar de natureza privada confinado e operante em exclusivo no seio da mesma unidade, e não subsiste nela e na sua conceção qualquer dependência externa associada ao seu nome ou convenção de chamada

Para esses casos, substitua-a pelo simples Pascal:

function _ftol(f: Double): Integer; cdecl;
begin
  Result := Trunc(f);
end;

A instrução Trunc trunca em direção ao zero, procedimento este que coincide com o que a instrução _ftol realizava com a palavra de controlo do x87 configurada no modo de truncamento. Compila tanto no DCC32 quanto no DCC64, não carecendo sequer de modificação da sua estrutura. O compilador tratará de gerar a instrução conveniente para cada um dos destinos: no destino x64, emitirá habitualmente CVTTSD2SI, configurando a mesmíssima instrução observável numa versão escrita manualmente. A conduta é estritamente idêntica, as condicionantes da plataforma deixam de constituir um impedimento, e deixa de existir assembly para ser mantido

A única diferença semântica que vale a pena analisar é a seguinte: Trunc produz invariavelmente uma exceção EInvalidOp na configuração predefinida do Delphi quando a entrada é do tipo NaN ou corresponde a um infinito. O anterior x87 fistp presente no código original limitava-se a gravar um padrão de bits sem despoletar nada. Caso o código submeta valores invulgares de vírgula flutuante a esta função e o antigo comportamento se mantiver silencioso, salvaguarde, chamando à função as instruções IsNaN e IsInfinite da Math antes de invocar Trunc

Compilação condicional quando ambos os destinos (targets) permanecem ativos

Alguns projetos são forçados a distribuir de forma contínua binários em edições quer de 32 quer de 64 bits. Caso pretenda manter a implementação original em assembly da variante de 32 bits e introduzir a nova rotina destinada e vocacionada estritamente aos sistemas a 64 bits, efetue-o recorrendo neste panorama perante as ferramentas condicionais e use 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 consiste na correção mecânica mínima e justifica ser tratada como provisória. Uma base de código que mantiver um código assembly específico de arquitetura a funcionar como mero auxiliar em prol e visando unicamente o truncamento de float para inteiro carrega um débito técnico dispensável. O ramo orientado à variante 32 bits acaba unicamente por se poder suprimir do código se e quando o programador confirmar plenamente não existir dependência perante os efeitos colaterais resultantes inerentes da intervenção sob alçada respeitante aos trâmites focando a FPU face aos ditames consagrados pelo modelo antigo

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 lugar; o linker escolhe um e ignora os restantes silenciosamente, o que significa que o programador pode corrigir uma cópia e continuar a ligar (link) a uma diferente que não sofreu qualquer alteração