代码之家  ›  专栏  ›  技术社区  ›  Greg Beech

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

  •  8
  • Greg Beech  · 技术社区  · 16 年前

    在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),但我找不到足够的文档来知道这是否会导致值的装箱,我怀疑这可能会导致值装箱。

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

    ****编辑:附加注释****

    尝试直接调用该方法(如以下程序中注释掉的行)不起作用(错误将是“操作可能会破坏运行时的稳定性”)。取消注释这些行,您将看到它确实按预期工作,返回“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));
    

    因此,你不能简单地用堆栈上的值调用方法,因为它是一种值类型(尽管如果它是引用类型,你可以)。

    我想实现的(或想知道是否可能)是替换显示为注释掉的三行,但保持程序正常工作,而不使用临时本地。

    4 回复  |  直到 16 年前
        1
  •  2
  •   Abe Heidebrecht    16 年前

    我想通了!幸运的是,我读到了 unbox opcode并注意到它推送了 住址 的价值。 unbox.any 推送实际值。因此,为了调用值类型的方法,而不必将其存储在局部变量中,然后加载其地址,您可以简单地 box 紧随其后 拆箱 。使用您的最后一个示例:

    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);
    il.Emit(OpCodes.Box, typeof(Nullable<int>)); // box followed by unbox
    il.Emit(OpCodes.Unbox, typeof(Nullable<int>));
    var getValue = typeof(Nullable<int>).GetMethod("get_HasValue");
    il.Emit(OpCodes.Call, getValue);
    il.Emit(OpCodes.Ret);
    Console.WriteLine(m.Invoke(null, null));
    

    这样做的缺点是,装箱会导致装箱对象的内存分配,因此它比使用局部变量(已经分配)慢一点。但是,它使您不必确定、声明和引用所需的所有局部变量。

        2
  •  2
  •   Mayoor    9 年前

    如果变量已经在堆栈上,则可以继续执行并发出方法调用。

    构造函数似乎没有以类型化的形式将变量推送到堆栈上。在深入研究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,这确实看起来很奇怪。

        3
  •  1
  •   Nick Johnson    16 年前

    在仔细考虑了这些选择之后,我认为你认为这是不可能的。如果检查MSIL指令的堆栈行为,可以看到没有操作将其操作数留在堆栈上。由于这将是“获取堆栈条目的地址”操作的要求,我相当有信心不存在这样的操作。

    这就剩下了dup+box或stloc+ldloca。正如你所指出的,后者可能更有效。

    @格雷格:许多指令都会留下 后果 在堆栈上,但没有留下任何指令 操作数 在堆栈上,这是“获取堆栈元素地址”指令所必需的。

        4
  •  0
  •   Mark    13 年前

    刚刚写了一个类,它做了OP要求的事情。..这是C#编译器生成的IL代码:

      IL_0008:  ldarg.0
      IL_0009:  ldarg.1
      IL_000a:  newobj     instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
      IL_000f:  stfld      valuetype [mscorlib]System.Nullable`1<int32> ConsoleApplication3.Temptress::_X
      IL_0014:  nop
      IL_0015:  ret