Replacing a class field's value via IL

Viewed 144

In my effort to learn and understand IL I'm trying to replace the value of a private field in an object, however it's not working.

public class Example
{
    private int _value;
}

private delegate void _memberUpdaterByRef(ref object source, object value);
private _memberUpdaterByRef GetWriterForField(FieldInfo field)
{
    // dynamically generate a new method that will emit IL to set a field value
    var type = field.DeclaringType;
    var dynamicMethod = new DynamicMethod(
        $"Set{field.Name}",
        typeof(void),
        new Type[] { typeof(object).MakeByRefType(), typeof(object) },
        type.Module,
        true
    );

    var gen = dynamicMethod.GetILGenerator();

    var typedSource = gen.DeclareLocal(field.DeclaringType);
    gen.Emit(OpCodes.Ldarg_0); // Load the instance of the object (argument 0) onto the stack
    gen.Emit(OpCodes.Ldind_Ref); // load as a reference type
    gen.Emit(OpCodes.Unbox_Any, field.DeclaringType);
    gen.Emit(OpCodes.Stloc_0); // pop typed arg0 into temp

    gen.Emit(OpCodes.Ldloca_S, typedSource);
    gen.Emit(OpCodes.Ldarg_1); // Load the instance of the object (argument 1) onto the stack
    gen.Emit(OpCodes.Unbox_Any, field.FieldType);
    gen.Emit(OpCodes.Stfld, field);

    gen.Emit(OpCodes.Ldarg_0); // Load the instance of the object (argument 0) onto the stack
    gen.Emit(OpCodes.Ldloc_0); // push temp
    gen.Emit(OpCodes.Box, field.DeclaringType);
    gen.Emit(OpCodes.Stind_Ref); // store object reference
    gen.Emit(OpCodes.Ret); // return void

    // create a delegate matching the parameter types
    return (_memberUpdaterByRef)dynamicMethod.CreateDelegate(typeof(_memberUpdaterByRef));
}

Given the following pseudo-code, the private field named _value does not change:

// field = Example._value
var writer = GetWriterForField(field);
writer(ref newInstance, 100); // Example._value = 0

I'm not really sure how to debug this, or what is incorrect about my IL syntax. I'm pretty much learning IL and grabbing bits from different sources trying to get this to work.

1 Answers

Using your original IL i found that overrall you had it right. However, in your implementation it appears that when you allocate the source object to a local and attempt to set its field, for some reason that field in the original object never was set.

Through some trial and error I found that you can avoid using a local variable all together by simply using the address(the ref object param) instead of storing it locally just to push it to the stack later on.

My best advice is to go over how the the OpCodes on MSDN are laid out. This was important for me, becuase after all I don't read or speak IL.

I found that OpCode MSDNs are laid out in a large list alphabetically in the OpCode class. This is absolutely useless becuase key features like which codes push the stack or which codes pop aren't placed together, and OpCodes that do mutiple things like pushing and popping multlple objects aren't grouped eiither. This leaves us no choice but to read every single OpCodes description and remember it. Fun!

When you finally find the one that sounds sort of similar to what you need take a look how it's own page is laid out.

Take stfld for example. The most important part is in the Remarks section called 'The Stack Transitional behaviour'. this section tells us what is required to get that OpCode to do what it says on the box.

It says:

  • An object reference or pointer is pushed onto the stack.
  • A value is pushed onto the stack.
  • The value and the object reference/pointer are popped from the stack; the value of field in the object is replaced with the supplied value.

The way I read this - in order for me to understand it is,
"I'm the one who needs to push the object to the stack, then I'm the one who needs to push the value to the stack."

Then when I call generator.Emit(OpCode.stfld,field) the last line tells me what to expect on the stack afterwards. Which is nothing since it pops both values and replaces nothing. Which is exactly what we want!

Armed with the knowledge to read their, albiet terrible layout (in addition to missing examples for each OpCode on their individual pages), we can sort of figure out a way to accomplish working with the raw IL.

I went ahead and got a working example for you hopefully the comments should break it down.

public _memberUpdaterByRef GetWriterForField(FieldInfo field)
{
    Type[] args = { typeof(object).MakeByRefType(), typeof(object) };
    var method = new DynamicMethod(
        $"Set{field.Name}",
        typeof(void),
        args,
        field.DeclaringType.Module
    );

    var gen = method.GetILGenerator();

    // because arg 0 is a ref, ldarg pushes arg0's address to the stack instead of the object/value
    gen.Emit(OpCodes.Ldarg_0);

    // in order to set the value of a field on on the object we cant use it's address
    // pop the address and push the instance to the stack
    gen.Emit(OpCodes.Ldind_Ref);

    // push the value that the field should be set to, to the stack
    gen.Emit(OpCodes.Ldarg_1);

    // becuase the value may be either a reference or value type, unbox it
    gen.Emit(OpCodes.Unbox_Any, field.FieldType);

    // pop the instance of the object, pop the value, and set the value of the field
    gen.Emit(OpCodes.Stfld, field);

    // no remaining objects on the stack, ret to exit
    gen.Emit(OpCodes.Ret);

    // create and return the delegate
    return (_memberUpdaterByRef)method.CreateDelegate(typeof(_memberUpdaterByRef));
}
Related