Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convert IntPtr to Int64: conv.u8 or conv.i8?

I'm working on an ILGenerator extension to help emit IL fragments using Expression. Everything was fine, until I worked on the integer conversion part. There are something really counter-intuitive to me, like:

  • Use conv.i8 to convert Int32 to UInt64
  • Use conv.u8 to convert UInt32 to Int64

They're all because the evaluation stack doesn't keep track of integer signedness. I fully understand the reason, it's just a little tricky to handle.

Now I want to support conversion involving IntPtr. It has to be trickier, since its length is variable. I decided to look at how C# compiler implements it.

Now focus on the particular IntPtr to Int64 conversion. Apparently the desired behavior should be: no-op on 64-bit systems, or sign-extending on 32-bit systems.

Since in C# the native int is wrapped by the IntPtr struct, I have to look at the body of its Int64 op_Explicit(IntPtr) method. The following is disassembled by dnSpy from .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
}

It's weird that conv.u8 appears here! It will perform a zero-extending on 32-bit systems. I confirmed that with the following code:

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

However, when looking at x86 instructions of the following C# method:

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

It turns out that what really happens is a sign-extending!

I noticed that Int64 op_Explicit(IntPtr) has an Intrinsic Attribute. Is it the case that the method body is completely ignored by the runtime JIT and is replaced by some internal implementation?

FINAL question: Do I have to refer to the conversion methods of IntPtr to implement my conversions?

Appendix My ILAsm implementation:

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);
}
like image 471
kevinjwz Avatar asked Mar 27 '20 07:03

kevinjwz


1 Answers

I have made a mistake. Int64 op_Explicit(IntPtr) has two versions. The 64-bit version is located in "C:\Program Files\dotnet...", and its implementation is:

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

The 32-bit version is located in "C:\Program Files (x86)\dotnet...", and its implementation is:

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

Puzzle solved!

Still, I think it's possible to use one identical implementation in both 32-bit and 64-bit build. One conv.i8 will do the work here.

Indeed, I could simplify my task of emitting IntPtr conversions, because at runtime, the length of 'IntPtr' is known, (either 32 or 64 to my knowledge), and most emitted methods will not be saved and reused. But I still would like a runtime-independent solution, and I think I already have found one.

like image 194
kevinjwz Avatar answered Sep 24 '22 03:09

kevinjwz