是否可以在堆栈上间接加载值类型

时间:2020-03-05 18:57:00  来源:igfitidea点击:

在Microsoft IL中,要在值类型上调用方法,我们需要间接引用。假设我们有一个名为" il"的ILGenerator,并且当前在堆栈顶部有一个Nullable,如果我们要检查它是否具有值,则可以发出以下内容:

var local = il.DeclareLocal(typeof(Nullable<int>));
il.Emit(OpCodes.Stloc, local);
il.Emit(OpCodes.Ldloca, local);
var method = typeof(Nullable<int>).GetMethod("get_HasValue");
il.EmitCall(OpCodes.Call, method, null);

但是,最好跳过将其另存为局部变量,而只需在堆栈中已存在的变量的地址上调用该方法,例如:

il.Emit(/* not sure */);
var method = typeof(Nullable<int>).GetMethod("get_HasValue");
il.EmitCall(OpCodes.Call, method, null);

ldind指令系列看起来很有希望(尤其是ldind_ref),但我找不到足够的文档来知道这是否会导致值装箱,我怀疑这可能会导致装箱。

我看过Ccompiler的输出,但是它使用局部变量来实现这一点,这使我相信第一种方法可能是唯一的方法。有人有更好的主意吗?

编辑:添加说明

尝试直接调用该方法是不可行的,如以下程序中注释掉各行所示(错误将是"操作可能会使运行时不稳定")。取消注释行,我们将看到它确实按预期工作,并返回" True"。

var m = new DynamicMethod("M", typeof(bool), Type.EmptyTypes);
var il = m.GetILGenerator();
var ctor = typeof(Nullable<int>).GetConstructor(new[] { typeof(int) });
il.Emit(OpCodes.Ldc_I4_6);
il.Emit(OpCodes.Newobj, ctor);
//var local = il.DeclareLocal(typeof(Nullable<int>));
//il.Emit(OpCodes.Stloc, local);
//il.Emit(OpCodes.Ldloca, local);
var getValue = typeof(Nullable<int>).GetMethod("get_HasValue");
il.Emit(OpCodes.Call, getValue);
il.Emit(OpCodes.Ret);
Console.WriteLine(m.Invoke(null, null));

因此,我们不能简单地使用堆栈上的值来调用该方法,因为它是一个值类型(尽管如果是引用类型,则可以)。

我想要达到的目的(或者想知道是否可能)是替换注释掉的三行内容,但保持程序正常运行,而无需使用临时本地语言。

解决方案

回答

如果变量已经在堆栈上,则可以继续进行操作,只发出方法调用即可。

似乎构造函数不会以类型形式将变量压入堆栈。深入研究IL之后,在构造变量之后,似乎有两种使用变量的方法。

我们可以在调用构造函数之前将将引用存储到评估堆栈的变量加载,然后在调用构造函数之后再次加载该变量,如下所示:

DynamicMethod method = new DynamicMethod("M", typeof(bool), Type.EmptyTypes);
ILGenerator il = method.GetILGenerator();
Type nullable = typeof(Nullable<int>);
ConstructorInfo ctor = nullable.GetConstructor(new Type[] { typeof(int) });
MethodInfo getValue = nullable.GetProperty("HasValue").GetGetMethod();
LocalBuilder value = il.DeclareLocal(nullable);         

// load the variable to assign the value from the ctor to
il.Emit(OpCodes.Ldloca_S, value);
// load constructor args
il.Emit(OpCodes.Ldc_I4_6);
il.Emit(OpCodes.Call, ctor);
il.Emit(OpCodes.Ldloca_S, value);

il.Emit(OpCodes.Call, getValue);
il.Emit(OpCodes.Ret);
Console.WriteLine(method.Invoke(null, null));

另一个选择是按照我们显示的方式进行。我看到的唯一原因是ctor方法返回void,因此它们不会像其他方法那样将其值放在堆栈上。如果新对象不在堆栈中,我们可以调用Setloc似乎很奇怪。

回答

在仔细研究了选项并作了进一步的考虑之后,我认为我们认为无法做到是正确的。如果检查MSIL指令的堆栈行为,则可以看到没有操作在堆栈上保留其操作数。由于这是"获取堆栈条目的地址"操作的要求,因此我相当有信心不存在。

剩下的就是dup + box或者stloc + ldloca。正如我们所指出的那样,后者可能更有效。

@greg:许多指令将其结果保留在堆栈中,但没有指令将其任何操作数保留在堆栈中,这对于"获取堆栈元素地址"指令是必需的。