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:
conv.i8
to convert Int32
to UInt64
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);
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With