Generate dynamic method to set a field of a struct instead of using reflection
Asked Answered
V

6

11

Let's say I have the following code which update a field of a struct using reflection. Since the struct instance is copied into the DynamicUpdate method, it needs to be boxed to an object before being passed.

struct Person
{
    public int id;
}

class Test
{
    static void Main()
    {
        object person = RuntimeHelpers.GetObjectValue(new Person());
        DynamicUpdate(person);
        Console.WriteLine(((Person)person).id); // print 10
    }

    private static void DynamicUpdate(object o)
    {
        FieldInfo field = typeof(Person).GetField("id");
        field.SetValue(o, 10);
    }
}

The code works fine. Now, let's say I don't want to use reflection because it's slow. Instead, I want to generate some CIL directly modifying the id field and convert that CIL into a reusable delegate (say, using Dynamic Method feature). Specially, I want to replace the above code with s/t like this:

static void Main()
{
    var action = CreateSetIdDelegate(typeof(Person));
    object person = RuntimeHelpers.GetObjectValue(new Person());
    action(person, 10);
    Console.WriteLine(((Person)person).id); // print 10
}

private static Action<object, object> CreateSetIdDelegate(Type t)
{
    // build dynamic method and return delegate
}    

My question: is there any way to implement CreateSetIdDelegate excepts from using one of the following techniques?

  1. Generate CIL that invoke the setter using reflection (as the 1st code segment in this post). This makes no sense, given the requirement is to get rid of reflection, but it's a possible implementation so I just mention.
  2. Instead of using Action<object, object>, use a custom delegate whose signature is public delegate void Setter(ref object target, object value).
  3. Instead of using Action<object, object>, use Action<object[], object> with the 1st element of the array being the target object.

The reason I don't like 2 & 3 is because I don't want to have different delegates for the setter of object and setter of struct (as well as not wanting to make the set-object-field delegate more complicated than necessary, e.g. Action<object, object>). I reckon that the implementation of CreateSetIdDelegate would generate different CIL depending whether the target type is struct or object, but I want it to return the same delegate offering the same API to user.

Ventage answered 13/8, 2009 at 14:50 Comment(4)
Is using a mutable struct really your best option here? It's almost always a pain for many reasons, and it seems you're running into some of them...Aggression
Have you considered compiling an expression tree instead of emitting IL? It should be much easier.Brook
@Jon: actually I'm building a fast reflection API (fasterflect.codeplex.com) so support for struct reflection operations would be desirable by some people.Ventage
@Mehrdad: I haven't tried that. Not sure about the performance between compiling the ET to a delegate and converting a dynamic method to a delegate - I suppose the latter is better but haven't tried in fact.Ventage
W
14

EDIT again: This works structs now.

There's a gorgeous way to do it in C# 4, but you'll have to write your own ILGenerator emit code for anything before that. They added an ExpressionType.Assign to the .NET Framework 4.

This works in C# 4 (tested):

public delegate void ByRefStructAction(ref SomeType instance, object value);

private static ByRefStructAction BuildSetter(FieldInfo field)
{
    ParameterExpression instance = Expression.Parameter(typeof(SomeType).MakeByRefType(), "instance");
    ParameterExpression value = Expression.Parameter(typeof(object), "value");

    Expression<ByRefStructAction> expr =
        Expression.Lambda<ByRefStructAction>(
            Expression.Assign(
                Expression.Field(instance, field),
                Expression.Convert(value, field.FieldType)),
            instance,
            value);

    return expr.Compile();
}

Edit: Here was my test code.

public struct SomeType
{
    public int member;
}

[TestMethod]
public void TestIL()
{
    FieldInfo field = typeof(SomeType).GetField("member");
    var setter = BuildSetter(field);
    SomeType instance = new SomeType();
    int value = 12;
    setter(ref instance, value);
    Assert.AreEqual(value, instance.member);
}
Wide answered 14/8, 2009 at 7:21 Comment(2)
Nice use of C# 4's statement ET. +1. But the use of ref is s/t I want to avoid (see the 2nd bullet in my question) because I don't want to build separate delegates for struct setter & class setter.Ventage
For a struct, you have to pass it by ref to make changes to the original. Since you can't add ref or out to the Action<> delegates, you have the following options: 1) Use a class instead of struct, 2) Use custom delegates, or 3) Engineer it so you don't have to make changes to the struct, and pass the struct by [boxed] value. :)Wide
D
11

I ran into a similar issue, and it took me most of a weekend, but I finally figured it out after a lot of searching, reading, and disassembling C# test projects. And this version only requires .NET 2, not 4.

public delegate void SetterDelegate(ref object target, object value);
private static Type[] ParamTypes = new Type[]
{
    typeof(object).MakeByRefType(), typeof(object)
};
private static SetterDelegate CreateSetMethod(MemberInfo memberInfo)
{
    Type ParamType;
    if (memberInfo is PropertyInfo)
        ParamType = ((PropertyInfo)memberInfo).PropertyType;
    else if (memberInfo is FieldInfo)
        ParamType = ((FieldInfo)memberInfo).FieldType;
    else
        throw new Exception("Can only create set methods for properties and fields.");

    DynamicMethod setter = new DynamicMethod(
        "",
        typeof(void),
        ParamTypes,
        memberInfo.ReflectedType.Module,
        true);
    ILGenerator generator = setter.GetILGenerator();
    generator.Emit(OpCodes.Ldarg_0);
    generator.Emit(OpCodes.Ldind_Ref);

    if (memberInfo.DeclaringType.IsValueType)
    {
#if UNSAFE_IL
        generator.Emit(OpCodes.Unbox, memberInfo.DeclaringType);
#else
        generator.DeclareLocal(memberInfo.DeclaringType.MakeByRefType());
        generator.Emit(OpCodes.Unbox, memberInfo.DeclaringType);
        generator.Emit(OpCodes.Stloc_0);
        generator.Emit(OpCodes.Ldloc_0);
#endif // UNSAFE_IL
    }

    generator.Emit(OpCodes.Ldarg_1);
    if (ParamType.IsValueType)
        generator.Emit(OpCodes.Unbox_Any, ParamType);

    if (memberInfo is PropertyInfo)
        generator.Emit(OpCodes.Callvirt, ((PropertyInfo)memberInfo).GetSetMethod());
    else if (memberInfo is FieldInfo)
        generator.Emit(OpCodes.Stfld, (FieldInfo)memberInfo);

    if (memberInfo.DeclaringType.IsValueType)
    {
#if !UNSAFE_IL
        generator.Emit(OpCodes.Ldarg_0);
        generator.Emit(OpCodes.Ldloc_0);
        generator.Emit(OpCodes.Ldobj, memberInfo.DeclaringType);
        generator.Emit(OpCodes.Box, memberInfo.DeclaringType);
        generator.Emit(OpCodes.Stind_Ref);
#endif // UNSAFE_IL
    }
    generator.Emit(OpCodes.Ret);

    return (SetterDelegate)setter.CreateDelegate(typeof(SetterDelegate));
}

Note the "#if UNSAFE_IL" stuff in there. I actually came up with 2 ways to do it, but the first one is really... hackish. To quote from Ecma-335, the standards document for IL:

"Unlike box, which is required to make a copy of a value type for use in the object, unbox is not required to copy the value type from the object. Typically it simply computes the address of the value type that is already present inside of the boxed object."

So if you want to play dangerously, you can use OpCodes.Unbox to change your object handle into a pointer to your structure, which can then be used as the first parameter of a Stfld or Callvirt. Doing it this way actually ends up modifying the struct in place, and you don't even need to pass your target object by ref.

However, note that the standard doesn't guarantee that Unbox will give you a pointer to the boxed version. Particularly, it suggests that Nullable<> can cause Unbox to create a copy. Anyway, if that happens, you'll probably get a silent failure, where it sets the field or property value on a local copy which is then immediately discarded.

So the safe way to do it is pass your object by ref, store the address in a local variable, make the modification, and then rebox the result and put it back in your ByRef object parameter.

I did some rough timings, calling each version 10,000,000 times, with 2 different structures:

Structure with 1 field: .46 s "Unsafe" delegate .70 s "Safe" delegate 4.5 s FieldInfo.SetValue

Structure with 4 fields: .46 s "Unsafe" delegate .88 s "Safe" delegate 4.5 s FieldInfo.SetValue

Notice that the boxing makes the the "Safe" version speed decrease with structure size, whereas the other two methods are unaffected by structure size. I guess at some point the boxing cost would overrun the reflection cost. But I wouldn't trust the "Unsafe" version in any important capacity.

Dedans answered 5/10, 2009 at 0:39 Comment(5)
Nice answer. For my particular problem (i.e. to implement my library fasterflect.codeplex.com), I didn't want to make use of 'ref' at all. Instead I require the calling code to pass in some struct wrapper which is a value type.Ventage
It won't let me comment on Hugh's answer below, so I'll answer here. Basically to set a field on a struct, you have to box the struct first, like this: object x = new MyStruct(); Then you pass the boxed object in by ref. If you already know what type you're dealing with ahead of time, you don't need this code at all. And if you don't know what type it is, that means it's probably in an object reference already. Hope that makes sense.Dedans
@BryceWagner can this be done with parameter known at run-time? Will changing typeof(object).MakeByRefType(), typeof(object) to typeof(knownType).MakeByRefType(), typeof(knownFieldType) be enough? I just don't know the emitting part and still can't use the .net 4.0.Kele
@Kele If you do that, your delegate type will have to be SetterDelegate(ref knownType obj, knownFieldType value). And if you know enough to create your delegate with those types, I'm pretty sure don't need DynamicMethod to make your delegate for you.Dedans
@BryceWagner see, I'm still having troubles building an expression tree (.net 3.5) which would set fields of structs. Your code is the only code on the internet which works perfectly in that manner, however I would like to know if it is possible to remove boxing/unboxing from this code as I'm able to build several variations of it (with various types), yet still have a need of doing that dynamically.Kele
C
5

After some experiments:

public delegate void ClassFieldSetter<in T, in TValue>(T target, TValue value) where T : class;

public delegate void StructFieldSetter<T, in TValue>(ref T target, TValue value) where T : struct;

public static class FieldSetterCreator
{
    public static ClassFieldSetter<T, TValue> CreateClassFieldSetter<T, TValue>(FieldInfo field)
        where T : class
    {
        return CreateSetter<T, TValue, ClassFieldSetter<T, TValue>>(field);
    }

    public static StructFieldSetter<T, TValue> CreateStructFieldSetter<T, TValue>(FieldInfo field)
        where T : struct
    {
        return CreateSetter<T, TValue, StructFieldSetter<T, TValue>>(field);
    }

    private static TDelegate CreateSetter<T, TValue, TDelegate>(FieldInfo field)
    {
        return (TDelegate)(object)CreateSetter(field, typeof(T), typeof(TValue), typeof(TDelegate));
    }

    private static Delegate CreateSetter(FieldInfo field, Type instanceType, Type valueType, Type delegateType)
    {
        if (!field.DeclaringType.IsAssignableFrom(instanceType))
            throw new ArgumentException("The field is declared it different type");
        if (!field.FieldType.IsAssignableFrom(valueType))
            throw new ArgumentException("The field type is not assignable from the value");

        var paramType = instanceType.IsValueType ? instanceType.MakeByRefType() : instanceType;
        var setter = new DynamicMethod("", typeof(void),
                                        new[] { paramType, valueType },
                                        field.DeclaringType.Module, true);

        var generator = setter.GetILGenerator();
        generator.Emit(OpCodes.Ldarg_0);
        generator.Emit(OpCodes.Ldarg_1);
        generator.Emit(OpCodes.Stfld, field);
        generator.Emit(OpCodes.Ret);

        return setter.CreateDelegate(delegateType);
    }
}

The main difference from the expression tree approach is that readonly fields can also be changed.

Crosspatch answered 16/5, 2014 at 15:1 Comment(0)
R
3

This code works for structs without using ref:

private Action<object, object> CreateSetter(FieldInfo field)
{
    var instance = Expression.Parameter(typeof(object));
    var value = Expression.Parameter(typeof(object));

    var body =
        Expression.Block(typeof(void),
            Expression.Assign(
                Expression.Field(
                    Expression.Unbox(instance, field.DeclaringType),
                    field),
                Expression.Convert(value, field.FieldType)));

    return (Action<object, object>)Expression.Lambda(body, instance, value).Compile();
}

Here is my test code:

public struct MockStruct
{
    public int[] Values;
}

[TestMethod]
public void MyTestMethod()
{
    var field = typeof(MockStruct).GetField(nameof(MockStruct.Values));
    var setter = CreateSetter(field);
    object mock = new MockStruct(); //note the boxing here. 
    setter(mock, new[] { 1, 2, 3 });
    var result = ((MockStruct)mock).Values; 
    Assert.IsNotNull(result);
    Assert.IsTrue(new[] { 1, 2, 3 }.SequenceEqual(result));
}
Readymix answered 8/8, 2015 at 20:8 Comment(0)
S
1

You may want to have a look at dynamic methods (reflection does not have to be slow!)...

Gerhard has a nice post about that: http://jachman.wordpress.com/2006/08/22/2000-faster-using-dynamic-method-calls/ (archived here.)

Semiporcelain answered 14/8, 2009 at 6:59 Comment(1)
That's what I'm doing (and asking in this question); I want to replace reflection by using dynamic method and the question asks if I can share the same delegate creation API for both structs & classes.Ventage
W
0

You could fairly easily modify this to work with structs. It's currently dictionary based but your situation is easier.

http://www.damonpayne.com/2009/09/07/TwoWayBindingToNameValuePairs.aspx

Whimsy answered 11/1, 2010 at 1:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.