如何使用 IL 改变盒装结构



想象一下我们有一个可变的struct(是的,不要开始):

public struct MutableStruct
{
    public int Foo { get; set; }
    public override string ToString()
    {
        return Foo.ToString();
    }
}

使用反射,我们可以获取此struct的盒装实例,并在盒子内对其进行突变:

// this is basically what we want to emulate
object obj = new MutableStruct { Foo = 123 };
obj.GetType().GetProperty("Foo").SetValue(obj, 456);
System.Console.WriteLine(obj); // "456"

我想做的是编写一些可以与此相同的 IL - 但速度更快。我是一个元编程迷;p

取消装箱 - 任何值并使用常规 IL 改变值是微不足道的 - 但你不能在之后调用 box 它,因为这会创建一个不同的盒子。我我们在这里需要做的是将其复制到现有框中。我已经调查了ldobj/stobj,但这些似乎不能完成这项工作(除非我错过了什么)。

那么:是否存在这样做的机制?还是我必须将自己限制在反射上才能执行盒装struct的就地更新?

或者换句话说:什么... evil goes here...

var method = new DynamicMethod("evil", null,
    new[] { typeof(object), typeof(object) });
var il = method.GetILGenerator();
// ... evil goes here...
il.Emit(OpCodes.Ret);
Action<object, object> action = (Action<object, object>)
    method.CreateDelegate(typeof(Action<object, object>));
action(obj, 789);
System.Console.WriteLine(obj); // "789"

嗯,这很有趣。

使用LdfldaStind_*似乎有效。实际上,它主要是Unbox(请参阅历史记录以获取适用于LdfldaStind_*的版本)。

以下是我在 LinqPad 中共同尝试的内容,以证明这一点。

public struct MutableStruct
{
    public int Foo { get; set; }
    public override string ToString()
    {
        return Foo.ToString();
    }
}
void Main()
{
    var foo = typeof(MutableStruct).GetProperty("Foo");
    var setFoo = foo.SetMethod;
    var dynMtd = new DynamicMethod("Evil", typeof(void), new [] { typeof(object), typeof(int) });
    var il = dynMtd.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);                       // object
    il.Emit(OpCodes.Unbox, typeof(MutableStruct));  // MutableStruct&
    il.Emit(OpCodes.Ldarg_1);                       // MutableStruct& int
    il.Emit(OpCodes.Call, setFoo);                  // --empty--
    il.Emit(OpCodes.Ret);                           // --empty--
    var del = (Action<object, int>)dynMtd.CreateDelegate(typeof(Action<object, int>));
    var mut = new MutableStruct { Foo = 123 };
    var boxed= (object)mut;
    del(boxed, 456);
    var unboxed = (MutableStruct)boxed;
    // unboxed.Foo = 456, mut.Foo = 123
}

你来了:

只需使用unsafe :)

static void Main(string[] args)
{
  object foo = new MutableStruct {Foo = 123};
  Console.WriteLine(foo);
  Bar(foo);
  Console.WriteLine(foo);
}
static unsafe void Bar(object foo)
{
  GCHandle h = GCHandle.Alloc(foo, GCHandleType.Pinned);
  MutableStruct* fp = (MutableStruct*)(void*)  h.AddrOfPinnedObject();
  fp->Foo = 789;
}

IL 实现留给读者作为练习。

更新:

基于凯文的回答,这里有一个最小的工作示例:

ldarg.0
unbox      MutableStruct
ldarg.1
call       instance void MutableStruct::set_Foo(int32)
ret

您可以更轻松地执行此操作。在具有动态功能的 .NET 4.5 下尝试此操作。

struct Test
{
    public Int32 Number { get; set; }

    public override string ToString()
    {
        return this.Number.ToString();
    }
}

class Program
{
    static void Main( string[] args )
    {
        Object test = new Test();
        dynamic proxy = test;
        proxy.Number = 1;
        Console.WriteLine( test );
        Console.ReadLine();
    }
}

我知道这不是反思,但仍然很有趣。

即使没有不安全的代码,纯 C#:

using System;
internal interface I {
  void Increment();
}
struct S : I {
  public readonly int Value;
  public S(int value) { Value = value; }
  public void Increment() {
    this = new S(Value + 1); // pure evil :O
  }
  public override string ToString() {
    return Value.ToString();
  }
}
class Program {
  static void Main() {
    object s = new S(123);
    ((I) s).Increment();
    Console.WriteLine(s); // prints 124
  }
}
在 C# 中,值类型

实例方法内部this引用实际上是ref -parameter(或值类型构造函数中的out -parameter ,这就是为什么this不能被捕获到闭包中,就像任何方法中的ref/out参数一样)并且可以修改。

当对未装箱的值调用结构实例方法时,this赋值将有效地替换调用站点的值。当在盒装实例上调用实例方法(通过虚拟调用或接口调用,如上例所示)时,ref -parameter 指向 box 对象内部的值,因此可以修改装箱值。

我已经发布了一个使用表达式树在另一个线程中设置字段的解决方案。将代码更改为使用属性是微不足道的:

最新更新