¿Convertir IntPtr a Int64: conv.u8 o conv.i8?

8

Estoy trabajando en una ILGeneratorextensión para ayudar a emitir fragmentos IL usando Expression. Todo estuvo bien, hasta que trabajé en la parte de conversión de enteros. Hay algo realmente contra-intuitivo para mí, como:

  • Use conv.i8para convertir Int32aUInt64
  • Use conv.u8para convertir UInt32aInt64

Todo se debe a que la pila de evaluación no realiza un seguimiento de la firma de enteros. Entiendo completamente la razón, es un poco difícil de manejar.

Ahora quiero apoyar la conversión que implica IntPtr. Tiene que ser más complicado, ya que su longitud es variable. Decidí ver cómo el compilador de C # lo implementa.

Ahora concéntrese en lo particular IntPtra la Int64conversión. Aparentemente, el comportamiento deseado debería ser: no operativo en sistemas de 64 bits o extensión de señal en sistemas de 32 bits.

Como en C # el native intestá envuelto por la IntPtrestructura, tengo que mirar el cuerpo de su Int64 op_Explicit(IntPtr)método. DnSpy desmonta lo siguiente de .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
}

¡Es raro que conv.u8aparezca aquí! Realizará una extensión cero en sistemas de 32 bits. Confirmé eso con el siguiente 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

Sin embargo, al mirar las instrucciones x86 del siguiente método de C #:

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

¡Resulta que lo que realmente sucede es una señalización!

Me di cuenta de que Int64 op_Explicit(IntPtr)tiene un Intrinsicatributo. ¿Es el caso que el cuerpo del método es completamente ignorado por el tiempo de ejecución JIT y se reemplaza por alguna implementación interna?

Pregunta FINAL: ¿Tengo que referirme a los métodos de conversión IntPtrpara implementar mis conversiones?

Apéndice Mi ILAsmimplementación:

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
fuente
Es un caso complicado, no hay una solución ideal. Lo que lo hace tropezar más que nada es no ver desde el IL que hay dos conversiones distintas . El reparto (int) utilizado en plataformas de 32 bits no es apropiado para sabores de 64 bits.
Hans Passant
@HansPassant Tienes razón. En el modo x86 obtengo una matriz de bytes IL diferente Int64 op_Explicit(IntPtr)que en el modo x64. ¿Cómo se logra esto? Investigué la ruta del archivo desde el que System.Private.CoreLibse carga el ensamblado (por Assembly.Location), pero son iguales entre x86 y x64.
kevinjwz
No es la misma ruta, c: \ archivos de programa vs c: \ archivos de programa (x86). Pero ese no es el punto, es un nerviosismo intrínseco muy diferente. No es fácil de ver, tendría que usar un depurador no administrado.
Hans Passant
@HansPassant Nuevamente tienes razón. No sabía que la opción "Preferir 32 bits" se ignora después de .Net Core 3.0 y quizás eso me confundió. De hecho, hay diferentes archivos de ensamblaje.
kevinjwz
Escribiré una respuesta yo mismo.
kevinjwz

Respuestas:

3

He cometido un error. Int64 op_Explicit(IntPtr)Tiene dos versiones. La versión de 64 bits se encuentra en "C: \ Archivos de programa \ dotnet ...", y su implementación es:

.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
}

La versión de 32 bits se encuentra en "C: \ Archivos de programa (x86) \ dotnet ...", y su implementación es:

.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
}

¡Rompecabezas resuelto!

Aún así, creo que es posible usar una implementación idéntica tanto en la compilación de 32 bits como en la de 64 bits. Uno conv.i8hará el trabajo aquí.

De hecho, podría simplificar mi tarea de emitir IntPtrconversiones, porque en tiempo de ejecución, se conoce la longitud de 'IntPtr' (32 o 64, que yo sepa), y la mayoría de los métodos emitidos no se guardarán ni se reutilizarán. Pero todavía me gustaría una solución independiente del tiempo de ejecución, y creo que ya he encontrado una.

kevinjwz
fuente