Search code examples
c#reflection.emit.net-6.0

Getter method generated by System.Reflection.Emit fails to return primitive types, but works as expected for non-primitive objects


I'm trying to write a program that creates a dynamic "Wrapper" class around an existing class with properties, and redirects all the virtual properties getters and setters to a dedicated GetValue and SetValue methods in BaseClass. It's a bit difficult to explain, so here's the code so far:

public abstract class BaseClass
{
    protected void SetValue(string propertyName, object value) { ... }
    protected object? GetValue(string propertyName) { ... }
}

public class DataClass : BaseClass
{
    public virtual string? StrValue { get; set; }
    public virtual int IntValue { get; set; }
    public virtual bool BoolValue { get; set; }
    public virtual List<float>? FloatListValue { get; set; }
}

In this example, my code generates a new class called DataClassWrapper that inherits from DataClass, and overrides all the properties' getter and setter methods with IL code that redirects the call to re respective GetValue and SetValue methods in the base class, passing the property's name as the first argument. The actual method that does all this is a bit too long for this post, but it can be viewed here.

Here's the code that generates the setter for each property: (works correctly in all cases)

setMethodGenerator.Emit(OpCodes.Ldarg_0); // 'this'
setMethodGenerator.Emit(OpCodes.Ldstr, propertyInfo.Name); // 1st argument of SetValue
setMethodGenerator.Emit(OpCodes.Ldarg_1); // value passed into this setter, aka. `value`
setMethodGenerator.Emit(OpCodes.Box, propertyInfo.PropertyType); // cast it to `object`
setMethodGenerator.EmitCall(OpCodes.Call, baseSetterMethod, Type.EmptyTypes); // call SetValue
setMethodGenerator.Emit(OpCodes.Ret); // return

where propertyInfo describes a property from DataClass, and baseSetterMethod references the SetValue() method from the base class.

Getter:

getMethodGenerator.Emit(OpCodes.Ldarg_0); // 'this'
getMethodGenerator.Emit(OpCodes.Ldstr, propertyInfo.Name); // 1st (and only) argument of GetValue
getMethodGenerator.EmitCall(OpCodes.Call, baseGetterMethod, Type.EmptyTypes); // call GetValue
getMethodGenerator.Emit(OpCodes.Castclass, propertyInfo.PropertyType); // cast result to expected type
getMethodGenerator.Emit(OpCodes.Ret); // return it

And some example usage:

var type = CreateDynamicType(typeof(DataClass), baseGetterMethod, baseSetterMethod);
var inst = (DataClass) Activator.CreateInstance(type)!;
inst.IntValue = 1123;
Console.Out.WriteLine(inst.IntValue);

I can read and write the properties StrValue and FloatListValue without any issues, my custom GetValue and SetValue methods are being called as they should be.

However if I try to read a primitive property like BoolValue or IntValue, it returns a seemingly random garbage number (for example -1151033224 the last time I ran this code). If I convert my IntValue into a nullable IntValue? value, it still returns gibberish, but this time the random numbers are much smaller (in the 0-700 range).

The setter method does receive the original int value correctly, and the getter also retrieves it intact, so the problem must be around the generated IL code that calls the getter. But it only seems to happen with primitive types. Does anyone have an idea?


Solution

  • I think your problem is the castclass instruction. The documentation says:

    typeTok [the argument] is a metadata token (a typeref, typedef or typespec), indicating the desired class. If typeTok is a non-nullable value type or a generic parameter type it is interpreted as “boxed” typeTok. If typeTok is a nullable type, Nullable, it is interpreted as “boxed” T.

    Unlike coercions (§III.1.6) and conversions (§III.3.27), a cast never changes the actual type of an object and preserves object identity (see Partition I)

    That means that if the object is a boxed instance of a value type, castclass does not unbox it but retains the object reference. So this will actually return the managed(!) address of the object, which is useless. In case of a value type, you need to use an unbox.any instruction.