Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Value Type Conversion in Dynamically Generated IL

Update
Over a year later, and I finally realized the cause of this behavior. Essentially, an object can't be unboxed to a different type than it was boxed as (even if that type casts or converts to the destination type), and if you don't know the correct type you have to discover it somehow. The assignment may be perfectly valid, but it is not feasible for this to happen automatically.

For example, even though a byte fits into an Int64, you can't unbox a byte as a long. You must unbox a byte as a byte, and then cast it.

If you don't have enough information to do that, you must use another means (as demonstrated below).

Representation and Identity

Original Problem

I'm working with IL to increase the performance of many tasks which are commonly handled with reflection. To accomplish this, I'm heavily using the DynamicMethod class.

I've written dynamic methods for setting properties on an object. This allows a developer to set properties on the fly based only on name. This works great for tasks such as loading records from a database into a business object.

However, I am stuck on one (probably simple) thing: converting value types, even larger to smaller types (such as putting the value of a byte into an Int32).

Here is the method I am using to create a dynamic property setter. Note that I have removed everything but the IL generation portion.

 // An "Entity" is simply a base class for objects which use these dynamic methods.
 // Thus, this dynamic method takes an Entity as an argument and an object value
 DynamicMethod method = new DynamicMethod( string.Empty, null, new Type[] { typeof( Entity ), typeof( object ) } );

ILGenerator il = method.GetILGenerator();    
PropertyInfo pi = entityType.GetProperty( propertyName );
MethodInfo mi = pi.GetSetMethod();

il.Emit( OpCodes.Ldarg_0 ); // push entity
il.Emit( OpCodes.Castclass, entityType ); // cast entity
il.Emit( OpCodes.Ldarg_1 ); // push value

if( propertyType.IsValueType )
{
    il.Emit( OpCodes.Unbox_Any, propertyType );
    // type conversion should go here?
}
else
{
    il.Emit( OpCodes.Castclass, propertyType ); // cast value
}

//
// The following Callvirt works only if the source and destination types are exactly the same
il.Emit( OpCodes.Callvirt, mi ); // call the appropriate setter method
il.Emit( OpCodes.Ret );

I have tried checking the property type at IL-generation time and using conversion OpCodes. In spite of this, the code still throws an InvalidCastException. This example shows a check that (I think) should ensure that whatever value is on the stack is converted to match the type of property to which it is being assigned.

if( pi.PropertyType == typeof( long ) )
{
    il.Emit( OpCodes.Conv_I8 );
}
else if( pi.PropertyType == typeof( int ) )
{
    il.Emit( OpCodes.Conv_I4 );
}
else if( pi.PropertyType == typeof( short ) )
{
    il.Emit( OpCodes.Conv_I2 );
}
else if( pi.PropertyType == typeof( byte ) )
{
    il.Emit( OpCodes.Conv_I1 );
}

I've also tried casting before or after unboxing the value type, such as:

if( propertyType.IsValueType )
{
    // cast here?
    il.Emit( OpCodes.Unbox_Any, propertyType );
    // or here?
}

I guess I could create IL to dynamically create the Convert object and call ChangeType() but that seems wasteful when most of the time this isn't even an issue (when types match, there is no problem).

To sum up the problem: When I pass a value type to a dynamically generated method, if it does not exactly match the type of property it is being assigned to, an InvalidCastException will be thrown, even if the size of the destination type is bigger than the source type. The type conversion I have tried does not work.

If you need more information to answer the question, please let me know.

EDIT: @JeffN825 was on the right track with looking at conversion. I had considered the System.Convert class, but ruled it out as being too expensive. However, with the destination type in hand, you can create a routine that only calls the method appropriate for the type. This (based on testing) seems relatively cheap. The resulting code looks something like this:

il.Emit( OpCodes.Call, GetConvertMethod( propertyType );

internal static MethodInfo GetConvertMethod( Type targetType )
{
    string name;

    if( targetType == typeof( bool ) )
    {
        name = "ToBoolean";
    }
    else if( targetType == typeof( byte ) )
    {
        name = "ToByte";
    }
    else if( targetType == typeof( short ) )
    {
        name = "ToInt16";
    }
    else if( targetType == typeof( int ) )
    {
        name = "ToInt32";
    }
    else if( targetType == typeof( long ) )
    {
        name = "ToInt64";
    }
    else
    {
        throw new NotImplementedException( string.Format( "Conversion to {0} is not implemented.", targetType.Name ) );
    }

    return typeof( Convert ).GetMethod( name, BindingFlags.Static | BindingFlags.Public, null, new Type[] { typeof( object ) }, null );
}

Granted, this results in a giant if/else statement (when all types are implemented) but its not dissimilar to what the BCL does, and this check is only performed when the IL is generated, and not with every call. Thus, it picks the correct Convert method and compiles a Call to it.

Note that OpCodes.Call is required, not OpCodes.Callvirt, as the Convert object's methods are static.

Performance is respectable; casual testing shows 1,000,000 calls to the dynamically generated set method taking about 40ms. Beats the heck out of reflection.

like image 337
Tim M. Avatar asked Apr 21 '11 22:04

Tim M.


2 Answers

I know this doesn't directly answer your question, but after having to maintain many different IL generation implementations, I've found better success in using Expression Trees.

They're available as part of the DLR for .NET 2.0/3.5, or integrated directly in .NET 4.0.

You can compile your expression tree to a lambda or event emit directly to a DynamicMethod.

Ultimately, the underlying Expression Tree API generates IL using the same ILGenerator mechanism.

P.S. When I'm debugging IL generation like this, I like to create a simple Console test application and Reflector the compiled code.
For your problem, I tried the following:

static class Program
{
    static void Main(string[] args)
    {
        DoIt((byte) 0);
    }

    static void DoIt(object value)
    {
        Entity e = new Entity();
        e.Value = (int)value;
    }
}

public class Entity
{
    public int Value { get; set; }
}

And the IL generated is:

L_0000: nop 
L_0001: newobj instance void ConsoleApplication2.Entity::.ctor()
L_0006: stloc.0 
L_0007: ldloc.0 
L_0008: ldarg.0 
L_0009: unbox.any int32
L_000e: callvirt instance void ConsoleApplication2.Entity::set_Value(int32)
L_0013: nop 
L_0014: ret 

It's unboxing to the value type just like you do. Guess what? I get an invalid cast exception! So the problem isn't the IL you're generating. I'd recommend you try using it as an IConvertable:

static void DoIt(object value)
{
    Entity e = new Entity();
    e.Value = ((IConvertible) value).ToInt32(null);
}

L_0000: nop 
L_0001: newobj instance void ConsoleApplication2.Entity::.ctor()
L_0006: stloc.0 
L_0007: ldloc.0 
L_0008: ldarg.0 
L_0009: castclass [mscorlib]System.IConvertible
L_000e: ldnull 
L_000f: callvirt instance int32 [mscorlib]System.IConvertible::ToInt32(class [mscorlib]System.IFormatProvider)
L_0014: callvirt instance void ConsoleApplication2.Entity::set_Value(int32)
L_0019: nop 
L_001a: ret 
like image 56
Jeff Avatar answered Oct 20 '22 01:10

Jeff


In order to unbox the value, you must first have boxed it, and for the unbox to not throw you must have converted the value to the type you unbox it to before you boxed it.

However, as the type of the property setter is known, and you're dealing with value types, you shouldn't have to box/unbox at all:

E.g. if you wanted to call a property setter of type Int32 with an Int64 argument, it would go something like this:

// Int 64 argument value assumed on top of stack now
conv.i4  // convert it to int32
callvirt   ...
like image 2
500 - Internal Server Error Avatar answered Oct 20 '22 02:10

500 - Internal Server Error