How to mutate a boxed value type (primitive or struct) in C#/IL
Asked Answered
C

2

3

Related to How to mutate a boxed struct using IL I am trying to change the value of a boxed value type but in a generic way, so trying to implement the following method:

void MutateValueType<T>(object o, T v) where T : struct

So the following should be possible:

var oi = (object)17;
MutateValueType<int>(oi, 43);
Console.WriteLine(oi); // 43

var od = (object)17.7d;
MutateValueType<double>(od, 42.3);
Console.WriteLine(od); // 42.3

I am failing to get it this to work on .NET Framework (see comment by @hvd that the implementation without typeof(Program).Module works on other runtimes). I have implemented this as seen below. However, this fails when calling the delegate del with a:

System.Security.VerificationException: 'Operation could destabilize the runtime.'

Here is the implementation I have come up with:

public static void MutateValueType<T>(object o, T v)
{
    var dynMtd = new DynamicMethod("EvilMutateValueType", 
        typeof(void), new Type[] { typeof(object), typeof(T) });
    var il = dynMtd.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);           // object
    il.Emit(OpCodes.Unbox, typeof(T));  // T&
    il.Emit(OpCodes.Ldarg_1);           // T (argument value)
    il.Emit(OpCodes.Stobj, typeof(T));  // stobj !!T
    il.Emit(OpCodes.Ret);               

    var del = (Action<object, T>)dynMtd.CreateDelegate(typeof(Action<object, T>));
    del(o, v);
}

The above should be equivalent to the below IL, that works, but still the above fails, so the question is why this doesn't work.

  .method public hidebysig static void Mutate<T>(object o, !!T Value) cil managed aggressiveinlining
  {
    .maxstack  2
    ldarg.0
    unbox !!T
    ldarg.1
    stobj !!T
    ret
  }
Catercousin answered 23/6, 2017 at 14:35 Comment(6)
I'm not sure its possible to completely replace the contents of the box, and not sure why you think it should be (at least, in a generic fashion).Deirdre
Because unbox simply returns a managed pointer to the value, or in other words address of memory containing the value type. Assigning a new value to this should be possible. This should correspond to a method returning ref e.g. ref T Unbox<T>(object o), in fact I think this could be implemented in IL and then used to the above.Catercousin
@Deirdre Consider a simple struct Evil { int i; public override string ToString() { ++i; return i.ToString(); } }, then object o = new Evil();. Now, the spec requires o.ToString() != o.ToString() -- that is, the spec requires the runtime to support in-place updates of boxed values. Given that the runtime is required to support that anyway, and given that my example doesn't make use of anything that actually requires co-operation from the value type in question, it seems clear to me that what the OP is after should be possible.Kucik
@hvd - the spec requires that members of structs be updatable. It doesn't require that the entire struct be replacable. I.e. it doesn't require that the this assignment possible in struct constructors be generally available.Deirdre
@Deirdre In my example, I can just as easily put this = new Evil { i = i + 1 }; to modify the entire struct. Struct assignment has never been limited to constructors.Kucik
FWIW, the exact code in this question works, without any modifications, in Mono and in .NET Core.Kucik
K
5

The difference is that DynamicMethod by default requires verifiable code, whereas your own code (including custom IL) is by default allowed to be unverifiable.

You can treat the DynamicMethod as part of your own module, allowing it to contain unverifiable IL, by specifying the module:

var dynMtd = new DynamicMethod("EvilMutateValueType",
    typeof(void), new Type[] { typeof(object), typeof(T) }, typeof(Program).Module);
// Use whatever class you have available here.              ^^^^^^^^^^^^^^^^^^^^^^

Despite some other issues in PEVerify making it hard to get good diagnostics, it looks like this is intended to at least not be verifiable:

III.1.8.1.2.2 Controlled-mutability managed pointers

The readonly. prefix and unbox instructions can produce what is called a controlled-mutability managed pointer. Unlike ordinary managed pointer types, a controlled-mutability managed pointer is not verifier-assignable-to (§III.1.8.1.2.3) ordinary managed pointers; e.g., it cannot be passed as a byref argument to a method. At control flow points, a controlled-mutability managed pointer can be merged with a managed pointer of the same type to yield a controlled-mutability managed pointer.

Controlled-mutability managed pointers can only be used in the following ways:

  1. As the object parameter for an ldfld, ldflda, stfld, call, callvirt, or constrained. callvirt instruction.
  2. As the pointer parameter to a ldind.* or ldobj instruction.
  3. As the source parameter to a cpobj instruction.

All other operations (including stobj, stind.*, initobj, and mkrefany) are invalid.

[...]

But it looks like it's intended to still be correct:

III.4.29 stobj – store a value at an address

[...]

Correctness:

Correct CIL ensures that dest is a pointer to T and the type of src is verifier-assignable-to T.

[...]

Note that there is no restriction on controlled-mutability managed pointers here, any pointer to T is allowed.

Therefore, ensuring that no verification happens for your IL is the right way to go.

Kucik answered 25/6, 2017 at 16:21 Comment(1)
awesome this works. Thank you very much. Perhaps you could add a note at the end with the final complete code for this, if you like.Catercousin
C
4

One solution, is by making an Unbox method in IL that calls unbox and returns a ref to the type contained in the object:

.method public hidebysig static !!T&  Unbox<T>(object o) cil managed aggressiveinlining
{
  .maxstack  1
  ldarg.0
  unbox !!T
  ret
}

And then using this like:

public static void MutateValueType<T>(object o, T v)
{
    ref T ub = ref Unsafe.Unbox<T>(o);
    ub = v;
}

This correctly outputs:

        var oi = (object)17;
        MutateValueType<int>(oi, 43);
        Console.WriteLine(oi); // 43

        var od = (object)17.7d;
        MutateValueType<double>(od, 42.3);
        Console.WriteLine(od); // 42.3

NOTE: This requires C# 7 support for ref returns.

This might be added to say https://github.com/DotNetCross/Memory.Unsafe but it must also be possible with il.Emit also, so I am looking for that.

Catercousin answered 23/6, 2017 at 15:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.