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