Как изменить коробочную структуру с помощью 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"

person Marc Gravell    schedule 21.09.2013    source источник
comment
Меня удивило, что вы можете переместить код SetValue в новый метод и передать упакованное значение и при этом изменить значение исходной структуры. Упаковывание, а затем использование отражения, похоже, указывает на то, что сама структура передается по ссылке. Интересный.   -  person Kirk Woll    schedule 22.09.2013
comment
@KirkWoll нет, мы не изменяем исходную структуру - мы изменяем структуру в штучной упаковке. Обратите внимание на первую строку, где мы присваиваем значение переменной типа object — так что оно сразу упаковывается.   -  person Marc Gravell    schedule 23.09.2013
comment
ах, да, я вижу это теперь — я видел это и раньше, но мне и в голову не приходило, что это будет иметь значение. :) Не понимал, что коробочные изменяемые структуры будут работать таким образом при передаче.   -  person Kirk Woll    schedule 23.09.2013
comment
В связи с этим я пытаюсь изменить примитив в штучной упаковке или тип значения, см. ​​stackoverflow.com/questions/44724042/, но не смог этого сделать.   -  person nietras    schedule 23.06.2017


Ответы (5)


Что ж, это было весело.

Кажется, использование Ldflda и Stind_* работает. На самом деле это в основном Распаковать (см. историю для версии, которая работает с 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
}
person Kevin Montrose    schedule 21.09.2013
comment
Теперь круг завершен! - person Marc Gravell; 22.09.2013
comment
Интересно, что ключевой частью здесь, в которой я ошибся, может быть использование unbox вместо unbox-any ; все еще играю - person Marc Gravell; 22.09.2013
comment
Можно намного короче и без отражения :) См. мою правку. - person leppie; 22.09.2013
comment
@leppie, ты прав, ответ скорректирован, чтобы перейти прямо к собственности. - person Kevin Montrose; 22.09.2013

Ну вот:

Просто используйте 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
person leppie    schedule 21.09.2013
comment
Интересно - будем разбираться. Предположительно, дескриптор нуждается в Free() ? - person Marc Gravell; 22.09.2013
comment
@MarcGravell: Понятия не имею ;p Но возможно. - person leppie; 22.09.2013
comment
Обратите внимание, что вы не можете получить закрепленный дескриптор любой структуры, в которой есть управляемые члены, так что это не общее решение. - person ghord; 08.08.2015

Вы можете сделать это еще проще. Попробуйте это в .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();
    }
}

Я знаю, что это не отражение, но все равно весело.

person Ivan Zlatanov    schedule 23.09.2013
comment
Цель этого сценария состоит в том, что имя члена известно только во время выполнения как string; а иначе: действительно. Конечно, вы также можете подделать dynamic API во время выполнения, но это имеет довольно много накладных расходов по сравнению с опцией распаковки, поэтому для моей цели это не подходит. Однако все, что вы говорите, остается правдой. - person Marc Gravell; 23.09.2013

Даже без небезопасного кода чистый 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 указывает на значение внутри объекта ящика, поэтому можно изменить упакованное значение.

person controlflow    schedule 21.09.2013
comment
Я знаю, как блоки и интерфейсы работают, но это просто не предназначено для того же сценария, над которым я работаю. Я не могу предположить интерфейс. - person Marc Gravell; 22.09.2013

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

person ghord    schedule 08.08.2015