How to mutate a boxed struct using IL
Asked Answered
B

5

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"
Benco answered 21/9, 2013 at 21:40 Comment(4)
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.Finland
@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.Benco
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.Finland
Related to this I am trying to mutate a boxed primitive or value type, see #44724542 but failing to do so.Vesuvius
M
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
}
Manslayer answered 21/9, 2013 at 22:44 Comment(4)
The circle is now complete!Benco
Interestingly - the key part in here that I was getting wrong may be the use of unbox instead of unbox-any ; still playingBenco
Can be much shorter, and without using reflection :) See my edit.Resume
@Resume you're right, answer adjusted to go straight to the property.Manslayer
R
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
Resume answered 21/9, 2013 at 22:31 Comment(3)
Interesting - will investigate. Presumably the handle needs a Free() ?Benco
@MarcGravell: No idea ;p But probably.Resume
Note that you cannot get a pinned handle to any structure that has managed members, so this is not a general solution.Behavior
P
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.

Paquito answered 23/9, 2013 at 8:59 Comment(1)
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.Benco
R
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.

Rubierubiginous answered 21/9, 2013 at 23:23 Comment(1)
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.Benco
B
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:

Behavior answered 8/8, 2015 at 20:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.