是否可以在堆栈上间接加载值类型
在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:许多指令将其结果保留在堆栈中,但没有指令将其任何操作数保留在堆栈中,这对于"获取堆栈元素地址"指令是必需的。