Как изменить коробочную структуру с помощью 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 - но вы не можете просто вызвать его потом, потому что это создаст другое поле. Я предполагаю, что нам нужно скопировать это поверх существующего поля. Я исследовал 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"

5 ответов

Решение

Ну, это было весело.

С помощью Ldflda а также Stind_* похоже на работу. На самом деле, это в основном Unbox (см. Историю для версии, которая работает с Ldflda а также Stind_*).

Вот что я взломал вместе в 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-параметр (или out-параметр в конструкторе типа значения, и именно поэтому this не может быть захвачен в замыкания, так же, как ref/out параметры в любых методах) и могут быть изменены.

Когда метод экземпляра структуры вызывается для распакованного значения, this Назначение будет эффективно заменить значение на сайте вызова. Когда метод экземпляра вызывается в упакованном экземпляре (через виртуальный вызов или интерфейсный вызов, как в примере выше), ref-параметр указывает на значение внутри объекта бокса, поэтому есть возможность изменить коробочное значение.

Я разместил решение, используя деревья выражений для настройки полей в другом потоке. Тривиально изменить код, чтобы использовать вместо него свойства:

Другие вопросы по тегам