Estoy trabajando en una ILGenerator
extensió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.i8
para convertirInt32
aUInt64
- Use
conv.u8
para convertirUInt32
aInt64
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 IntPtr
a la Int64
conversió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 int
está envuelto por la IntPtr
estructura, 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.u8
aparezca 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 Intrinsic
atributo. ¿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 IntPtr
para implementar mis conversiones?
Apéndice Mi ILAsm
implementació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);
}
Int64 op_Explicit(IntPtr)
que en el modo x64. ¿Cómo se logra esto? Investigué la ruta del archivo desde el que System.Private.CoreLib
se carga el ensamblado (por Assembly.Location
), pero son iguales entre x86 y x64.