【问题标题】:How to mutate a boxed value type (primitive or struct) in C#/IL如何在 C#/IL 中改变盒装值类型(原始或结构)
【发布时间】:2017-06-23 14:35:46
【问题描述】:

How to mutate a boxed struct using IL相关我正在尝试以通用方式更改装箱值类型的值,因此尝试实现以下方法:

void MutateValueType<T>(object o, T v) where T : struct

所以以下应该是可能的:

var oi = (object)17;
MutateValueType<int>(oi, 43);
Console.WriteLine(oi); // 43

var od = (object)17.7d;
MutateValueType<double>(od, 42.3);
Console.WriteLine(od); // 42.3

我无法让它在 .NET Framework 上运行(请参阅 @hvd 的评论,即没有 typeof(Program).Module 的实现适用于其他运行时)。我已经实现了这一点,如下所示。但是,当调用代理 del 时失败:

System.Security.VerificationException: 'Operation could destabilize the runtime.'

这是我想出的实现:

public static void MutateValueType<T>(object o, T v)
{
    var dynMtd = new DynamicMethod("EvilMutateValueType", 
        typeof(void), new Type[] { typeof(object), typeof(T) });
    var il = dynMtd.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);           // object
    il.Emit(OpCodes.Unbox, typeof(T));  // T&
    il.Emit(OpCodes.Ldarg_1);           // T (argument value)
    il.Emit(OpCodes.Stobj, typeof(T));  // stobj !!T
    il.Emit(OpCodes.Ret);               

    var del = (Action<object, T>)dynMtd.CreateDelegate(typeof(Action<object, T>));
    del(o, v);
}

上面应该等效于下面的IL,可以工作,但上面仍然失败,所以问题是为什么这不起作用。

  .method public hidebysig static void Mutate<T>(object o, !!T Value) cil managed aggressiveinlining
  {
    .maxstack  2
    ldarg.0
    unbox !!T
    ldarg.1
    stobj !!T
    ret
  }

【问题讨论】:

  • 我不确定它是否可以完全替换盒子的内容,也不确定你为什么认为它应该是(至少,以一般的方式)。
  • 因为unbox 只是返回一个指向值的托管指针,或者换句话说,包含值类型的内存地址。应该可以为此分配一个新值。这应该对应于返回 ref 的方法,例如ref T Unbox&lt;T&gt;(object o),其实我觉得这可以在IL中实现,然后习惯上面的。
  • @Damien_The_Unbeliever 考虑一个简单的struct Evil { int i; public override string ToString() { ++i; return i.ToString(); } },然后是object o = new Evil();。现在,规范需要o.ToString() != o.ToString()——也就是说,规范需要运行时支持盒装值的就地更新。鉴于无论如何都需要运行时来支持这一点,并且鉴于我的示例没有使用任何实际需要与所讨论的值类型合作的东西,我似乎很清楚 OP 所追求的应该是可能的.
  • @hvd - 规范要求结构的 成员 是可更新的。它不要求 entire 结构是可替换的。 IE。它不需要struct 构造函数中可能的this 赋值普遍可用。
  • @Damien_The_Unbeliever 在我的示例中,我可以很容易地使用this = new Evil { i = i + 1 }; 来修改整个结构。结构赋值从未局限于构造函数。

标签: c# .net cil reflection.emit


【解决方案1】:

不同之处在于DynamicMethod 默认需要可验证代码,而您自己的代码(包括自定义 IL)默认允许不可验证。

您可以将DynamicMethod 视为您自己模块的一部分,通过指定模块允许它包含无法验证的IL:

var dynMtd = new DynamicMethod("EvilMutateValueType",
    typeof(void), new Type[] { typeof(object), typeof(T) }, typeof(Program).Module);
// Use whatever class you have available here.              ^^^^^^^^^^^^^^^^^^^^^^

尽管 PEVerify 中的一些其他问题导致难以获得良好的诊断,但看起来这至少是不可验证的:

III.1.8.1.2.2 受控可变性托管指针

readonly. 前缀和unbox 指令可以产生所谓的受控可变性托管指针。与普通托管指针类型不同,受控可变托管指针不是 verifier-assignable-to (§III.1.8.1.2.3) 普通托管指针;例如,它不能 作为 byref 参数传递给方法。在控制流点,受控可变托管指针可以与相同类型的托管指针合并以产生受控可变托管指针。

受控可变托管指针只能以下列方式使用:

  1. 作为 ldfldldfldastfldcallcallvirtconstrained. callvirt 指令的对象参​​数。
  2. 作为指向ldind.*ldobj 指令的指针参数。
  3. 作为cpobj 指令的源参数。

所有其他操作(包括stobjstind.*initobjmkrefany)均无效。

[...]

但看起来它仍然是正确的:

III.4.29 stobj - 在地址中存储值

[...]

正确性:

正确的 CIL 确保 dest 是指向 T 的指针,并且 src 的类型是 verifier-assignable-to T .

[...]

请注意,此处对受控可变性托管指针没有限制,任何指向 T 的指针都是允许的。

因此,确保不对您的 IL 进行验证是正确的方法。

【讨论】:

  • 太棒了。非常感谢你。如果您愿意,也许您可​​以在最后添加一个注释,并附上最终的完整代码。
【解决方案2】:

一种解决方案是在 IL 中创建一个 Unbox 方法,该方法调用 unbox 并将 ref 返回到对象中包含的类型:

.method public hidebysig static !!T&  Unbox<T>(object o) cil managed aggressiveinlining
{
  .maxstack  1
  ldarg.0
  unbox !!T
  ret
}

然后像这样使用:

public static void MutateValueType<T>(object o, T v)
{
    ref T ub = ref Unsafe.Unbox<T>(o);
    ub = v;
}

这正确输出:

        var oi = (object)17;
        MutateValueType<int>(oi, 43);
        Console.WriteLine(oi); // 43

        var od = (object)17.7d;
        MutateValueType<double>(od, 42.3);
        Console.WriteLine(od); // 42.3

注意:这需要 C# 7 支持 ref 返回。

这可能会被添加到https://github.com/DotNetCross/Memory.Unsafe,但它也必须与il.Emit 一起使用,所以我正在寻找它。

【讨论】:

    猜你喜欢
    • 2013-09-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多