Convert IntPtr to Int64: conv.u8 or conv.i8?
Asked Answered
K

1

8

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);
}
Kyoko answered 27/3, 2020 at 7:26 Comment(5)
It is a tricky corner-case, there is no ideal solution. What is tripping you up most of all is not seeing from the IL that there are two distinct conversions. The (int) cast used in 32-bit platforms is not appropriate for 64-bit flavors.Hagai
@HansPassant You are right. In x86 mode I get different IL byte array from Int64 op_Explicit(IntPtr) than in x64 mode. How is this achieved? I investigated the file path from which System.Private.CoreLib assembly is loaded (by Assembly.Location), but they are the same between x86 and x64.Kyoko
Not the same path, c:\program files vs c:\program files (x86). But that's not the point, it is an intrinsic so different jitters. Not easy to see, you'd have to use an unmanaged debugger.Hagai
@HansPassant Again you are right. I didn't know that the "Prefer 32-bit" option is ignored after .Net Core 3.0 and maybe I was confused by that. Indeed there are different assembly files.Kyoko
I will write an answer myself.Kyoko
K
3

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.

Kyoko answered 27/3, 2020 at 16:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.