27

Imagine we have a mutable struct (yes, don't start):

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

Using reflection, we can take a boxed instance of this struct and mutate it inside the box:

// 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"

What I would like to do is to write some IL that can do the same as this - but faster. I'm a meta-programming junkie ;p

It is trivial to unbox-any the value and mutate the value using regular IL - but you can't just call box it afterwards because that will create a different box. I'm guessing that what we would need to do here is copy it over the existing box. I have investigated ldobj / stobj, but those don't seem to do the job (unless I'm missing something).

So: does a mechanism to do this exist? Or must I limit myself to reflection to perform in-place updates of boxed structs ?

Or in other words: what ... 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"
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • It surprised me that you can move the `SetValue` code into a new method and pass the boxed value in and still mutate the value of the original struct. Boxing it and then using reflection does seem to indicate that the struct itself is passed by reference. Interesting. – Kirk Woll Sep 21 '13 at 22:20
  • @KirkWoll no, we aren't mutating the "original struct" - we're mutating the boxed struct. See the first line where we assign the value to a variable of type `object` - so it is boxed immediately. – Marc Gravell Sep 23 '13 at 09:04
  • ah, yes, I see that now -- I saw it before too, but it hadn't occurred to me that it would make a difference. :) Didn't realize that boxed mutable structs would work that way when passed around. – Kirk Woll Sep 23 '13 at 13:27
  • Related to this I am trying to mutate a boxed primitive or value type, see https://stackoverflow.com/questions/44724042/how-to-mutate-a-boxed-value-type-primitive-or-struct-in-c-il/ but failing to do so. – nietras Jun 23 '17 at 14:36

5 Answers5

19

Well, that was fun.

Using Ldflda and Stind_* seems to work. Actually, it's mostly Unbox (see history for version that works with Ldflda and Stind_*).

Here's what I hacked together in LinqPad to prove it out.

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
}
Community
  • 1
  • 1
Kevin Montrose
  • 22,191
  • 9
  • 88
  • 137
6

Here you go:

Just use 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 implementation is left as an exercise to the reader.

Update:

Based on the Kevin's answer, here is a minimal working example:

ldarg.0
unbox      MutableStruct
ldarg.1
call       instance void MutableStruct::set_Foo(int32)
ret
leppie
  • 115,091
  • 17
  • 196
  • 297
2

You can do this even easier. Try this under .NET 4.5 where we have dynamic.

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();
    }
}

I know it's not reflection but still fun though.

Ivan Zlatanov
  • 5,146
  • 3
  • 29
  • 45
  • The purpose of this scenario is where the member-name is only known at runtime as a `string`; but otherwise: indeed. Of course, you can also spoof the `dynamic` API at runtime - but this has quite a bit of overhead compared to the "unbox" option, so for the purpose I intend, this isn't a good fit. Everything you say remains *true*, however. – Marc Gravell Sep 23 '13 at 09:03
1

Even without unsafe code, pure 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
  }
}

In C#, this reference inside value types instance methods actually is ref-parameter (or out-parameter in value type constructor, and that is why this can't be captured into closures, just like ref/out parameters in any methods) and can be modified.

When struct instance method is invoked on unboxed value, this assignment will effectively replace value at the call site. When instance method is invoked on boxed instance (via virtual call or interface call like in the example above), ref-parameter is pointed to the value inside the box object, so it is possible to modify boxed value.

controlflow
  • 6,667
  • 1
  • 31
  • 55
  • I'm aware of how boxes and interfaces *work* - but this simply doesn't target the same scenario that I'm working to. I can't assume an interface. – Marc Gravell Sep 21 '13 at 23:25
0

I've posted a solution using Expression Trees for setting fields in another thread. It's trivial to change the code to use properties instead:

Community
  • 1
  • 1
ghord
  • 13,260
  • 6
  • 44
  • 69