Converter IntPtr em Int64: conv.u8 ou conv.i8?

8

Estou trabalhando em uma ILGeneratorextensão para ajudar a emitir fragmentos de IL usando Expression. Tudo estava bem, até eu trabalhar na parte de conversão de números inteiros. Há algo realmente contra-intuitivo para mim, como:

  • Use conv.i8para converter Int32paraUInt64
  • Use conv.u8para converter UInt32paraInt64

Eles são todos porque a pilha de avaliação não controla a assinatura inteira. Entendo perfeitamente o motivo, é um pouco complicado de lidar.

Agora, quero apoiar a conversão envolvida IntPtr. Tem que ser mais complicado, já que seu comprimento é variável. Decidi ver como o compilador C # o implementa.

Agora concentre-se no particular IntPtrda Int64conversão. Aparentemente, o comportamento desejado deve ser: ausência de operação em sistemas de 64 bits ou extensão de sinal em sistemas de 32 bits.

Como no C # o native inté envolvido pela IntPtrestrutura, tenho que examinar o corpo do seu Int64 op_Explicit(IntPtr)método. O seguinte é desmontado pelo dnSpy do .NET core 3.1.1:

.method public hidebysig specialname static 
    int64 op_Explicit (
        native int 'value'
    ) cil managed 
{
    .custom instance void System.Runtime.CompilerServices.IntrinsicAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void System.Runtime.Versioning.NonVersionableAttribute::.ctor() = (
        01 00 00 00
    )
    .maxstack 8

    IL_0000: ldarga.s  'value'
    IL_0002: ldfld     void* System.IntPtr::_value
    IL_0007: conv.u8
    IL_0008: ret
}

É estranho que conv.u8apareça aqui! Ele executará uma extensão de zero em sistemas de 32 bits. Confirmei que com o seguinte código:

delegate long ConvPtrToInt64(void* ptr);
var f = ILAsm<ConvPtrToInt64>(
    Ldarg, 0,
    Conv_U8,
    Ret
);
Console.WriteLine(f((void*)(-1)));  // print 4294967295 on x86

No entanto, ao examinar as instruções x86 do seguinte método C #:

static long Convert(IntPtr intp) => (long)intp;
;from SharpLab
C.Convert(IntPtr)
    L0000: mov eax, ecx
    L0002: cdq
    L0003: ret

Acontece que o que realmente acontece é uma extensão de sinal!

Notei que Int64 op_Explicit(IntPtr)tem um Intrinsicatributo. É o caso de o corpo do método ser completamente ignorado pelo JIT de tempo de execução e substituído por alguma implementação interna?

Pergunta FINAL: Preciso me referir aos métodos de conversão IntPtrpara implementar minhas conversões?

Apêndice Minha ILAsmimplementação:

static T ILAsm<T>(params object[] insts) where T : Delegate =>
    ILAsm<T>(Array.Empty<(Type, string)>(), insts);

static T ILAsm<T>((Type type, string name)[] locals, params object[] insts) where T : Delegate
{
    var delegateType = typeof(T);
    var mi = delegateType.GetMethod("Invoke");
    Type[] paramTypes = mi.GetParameters().Select(p => p.ParameterType).ToArray();
    Type returnType = mi.ReturnType;

    var dm = new DynamicMethod("", returnType, paramTypes);
    var ilg = dm.GetILGenerator();

    var localDict = locals.Select(tup => (name: tup.name, local: ilg.DeclareLocal(tup.type)))
        .ToDictionary(tup => tup.name, tup => tup.local);

    var labelDict = new Dictionary<string, Label>();
    Label GetLabel(string name)
    {
        if (!labelDict.TryGetValue(name, out var label))
        {
            label = ilg.DefineLabel();
            labelDict.Add(name, label);
        }
        return label;
    }

    for (int i = 0; i < insts.Length; ++i)
    {
        if (insts[i] is OpCode op)
        {
            if (op.OperandType == InlineNone)
            {
                ilg.Emit(op);
                continue;
            }
            var operand = insts[++i];
            if (op.OperandType == InlineBrTarget || op.OperandType == ShortInlineBrTarget)
                ilg.Emit(op, GetLabel((string)operand));
            else if (operand is string && (op.OperandType == InlineVar || op.OperandType == ShortInlineVar))
                ilg.Emit(op, localDict[(string)operand]);
            else
                ilg.Emit(op, (dynamic)operand);
        }
        else if (insts[i] is string labelName)
            ilg.MarkLabel(GetLabel(labelName));
        else
            throw new ArgumentException();
    }
    return (T)dm.CreateDelegate(delegateType);
}
kevinjwz
fonte
É um caso difícil, não há solução ideal. Acima de tudo, o que está atrapalhando você não é perceber que há duas conversões distintas . A conversão (int) usada nas plataformas de 32 bits não é apropriada para os tipos de 64 bits.
Hans Passant
@HansPassant Você está certo. No modo x86, recebo uma matriz de bytes IL diferente daquela Int64 op_Explicit(IntPtr)do modo x64. Como isso é alcançado? Investiguei o caminho do arquivo do qual o System.Private.CoreLibassembly é carregado (por Assembly.Location), mas eles são os mesmos entre x86 e x64.
kevinjwz 27/03
Não é o mesmo caminho, c: \ arquivos de programas vs c: \ arquivos de programas (x86). Mas esse não é o ponto, é um nervosismo intrínseco e tão diferente. Não é fácil ver, você precisaria usar um depurador não gerenciado.
Hans Passant 27/03
@HansPassant Novamente, você está certo. Eu não sabia que a opção "Preferir 32 bits" é ignorada após o .Net Core 3.0 e talvez eu tenha ficado confusa com isso. De fato, existem diferentes arquivos de montagem.
kevinjwz 27/03
Eu mesmo escreverei uma resposta.
kevinjwz 27/03

Respostas:

3

Eu cometi um erro. Int64 op_Explicit(IntPtr)tem duas versões. A versão de 64 bits está localizada em "C: \ Arquivos de Programas \ dotnet ..." e sua implementação é:

.method public hidebysig specialname static 
    int64 op_Explicit (
        native int 'value'
    ) cil managed 
{
    .maxstack 8

    IL_0000: ldarga.s  'value'
    IL_0002: ldfld     void* System.IntPtr::_value
    IL_0007: conv.u8
    IL_0008: ret
}

A versão de 32 bits está localizada em "C: \ Arquivos de Programas (x86) \ dotnet ..." e sua implementação é:

.method public hidebysig specialname static 
    int64 op_Explicit (
        native int 'value'
    ) cil managed 
{
    .maxstack 8

    IL_0000: ldarga.s  'value'
    IL_0002: ldfld     void* System.IntPtr::_value
    IL_0007: conv.i4
    IL_0008: conv.i8
    IL_0009: ret
}

Quebra-cabeça resolvido!

Ainda assim, acho que é possível usar uma implementação idêntica na compilação de 32 e 64 bits. Um conv.i8fará o trabalho aqui.

Na verdade, eu poderia simplificar minha tarefa de emitir IntPtrconversões, porque em tempo de execução, a duração do 'IntPtr' é conhecida (32 ou 64, pelo que sei), e a maioria dos métodos emitidos não será salva e reutilizada. Mas ainda gostaria de uma solução independente de tempo de execução, e acho que já encontrei uma.

kevinjwz
fonte