Click here to Skip to main content
15,882,113 members
Articles / Programming Languages / C#

Fast Late-bound Calls with Generics in .NET

Rate me:
Please Sign up or sign in to vote.
4.85/5 (15 votes)
27 Feb 2009Ms-PL4 min read 38.6K   124   28   6
Creation of unknown types and invocation of unknown methods without IL generation and Invoke calls

Preface

Imagine that you are using a dynamically created library. Imagine you are constructing a plug-in engine for your application. You will attach assemblies dynamically in both these cases. Then it will be necessary to create instances of some unknown classes from attached assemblies. Maybe you will even need to call some unknown functions of these unknown classes. You can solve a part of such problems with usage of base classes and interfaces. However, several situations require reflection anyway. Unfortunately, it is well known that the reflection is very slow in .NET.

I can give an example of such a situation:

C#
//autogenerated code
namespace plugin {
    public class l5fks9rffn33md : knownParamType
    {
    }

    public class p4sdfm2mlwd5 : knownType
    {
        public void testAction(l5fks9rffn33md p)
        { 
        }
    }
}

We need to do it within our code:

C#
//new plugin.p4sdfm2mlwd5().testAction(new plugin.l5fks9rffn33md());

We have knownType and knownParamType in our code, but we do not know auto generated types in compile-time. We also cannot define an interface containing testAction because it uses unknown types in signature.

It is possible to do the necessary actions with:

C#
Type unknownType = Type.GetType("plugin.p4sdfm2mlwd5");
Type unknownParamType = Type.GetType("plugin.l5fks9rffn33md");

knownType instance = (knownType)Activator.CreateInstance(unknownType);
knownParamType param = (knownParamType)Activator.CreateInstance(unknownParamType);
unknownType.GetMethod("testAction").Invoke(instance, new object[] {param});

However, this code is very slow (up to 100 times slower than direct call). In addition, it is not type-safe.

There is also other way: dynamic method, which uses dynamic IL generation with Reflection.Emit. This way is very complex (and very powerful also). C# 4.0 offers native calling of unknown methods but it is not released yet. I want to present one more method using power of generics only and doing the same things with the same speed as IL generation. It is not as easy as MethodInfo.Invoke, but not so complex as Reflection.Emit.

Instance Creation

We want to create an instance of a type having a constructor without parameters. Let's create a generic function doing it:

C#
public static object createNewObj<T>() where T : new()
{
      return new T();
}

It is very simple, is it not?

But how to call this function? We cannot write:

C#
//Helper.createNewObj<plugin.p4sdfm2mlwd5>();

Because type p4sdfm2mlwd5 is unknown.

Let's use two rare functions: MethodInfo.MakeGenericMethod and Delegate.CreateDelegate:

C#
public static Func<object> MakeNewDlg(Type t)
{
	return Delegate.CreateDelegate(typeof(Func<object>), 
		typeof(Helper).GetMethod("createNewObj").MakeGenericMethod(new Type[]
		{ t })) as Func<object>;
}

Note that useful generic delegates Func and Action are defined in System namespace. Now we can call:

C#
Func<object> knownTypeCreator = 
	Helper.MakeNewDlg(Type.GetType("plugin.p4sdfm2mlwd5"));
knownType instance = (knownType)knownTypeCreator();

It is also good to store knownTypeCreator somewhere and use it each time you need a new p4sdfm2mlwd5 instance. It will work fast because it is just a delegate and it uses no reflection.

However, this code is still not type-safe. The next one is better:

C#
public static Func<K> MakeNewDlg<K>(Type t)
{
	return Delegate.CreateDelegate(typeof(Func<K>),    
	    typeof(Helper).GetMethod("createNewObj2").MakeGenericMethod(new Type[]
	    { t, typeof(K) })) as Func<K>;
}

public static K createNewObj2<T, K>() where T : K, new()
{
	return new T();
}

It returns a typed delegate and we can omit type cast:

C#
Func<knownType> knownTypeCreator = Helper.MakeNewDlg <knownType>
				(Type.GetType("plugin.p4sdfm2mlwd5"));
knownType instance = knownTypeCreator();

Summary: we have any generators and can create as many instances as we need. The next step is to call methods. MakeGenericMethod will help us again.

Method Calling

Delegate.CreateDelegate function allows creating static delegates for non-static methods. It is the greatest ability to reuse the same delegate with different object instances like this:

C#
class X
{
    void test() { }
}
…
Action<X> caller = (Action<X>)Delegate.CreateDelegate(typeof(Action<X>), 
	typeof(X).GetMethod("test"));
X a = new X();
X b = new X();
caller(a);
caller(b);

Seems, we can use the same way for our purposes:

C#
MethodInfo method = Type.GetType("p4sdfm2mlwd5").GetMethod("testAction");
Action<knownType, knownParamType> d = 
	(Action<knownType, knownParamType>)Delegate.CreateDelegate
	(typeof(Action<knownType, knownParamType>), method);

Ow! This code generates a runtime error! It occurs because a newly created delegate should have the same signature as the underlying method (actually, it can return base type as result type but it is not allowed to use base type as parameter, see MSDN for details). In addition, the target type should have this method.

We can solve this problem with an anonymous method:

C#
Action<p4sdfm2mlwd5, l5fks9rffn33md> tmp = 
	(Action<p4sdfm2mlwd5, l5fks9rffn33md>)Delegate.CreateDelegate
	(typeof(Action<p4sdfm2mlwd5, l5fks9rffn33md>), method);
Action<knownType, knownParamType> d = delegate
	(knownType target, knownParamType p) { tmp((p4sdfm2mlwd5)target, 
	(l5fks9rffn33md)p); };

There is no problem with CreateDelegate now, there is another problem. This code cannot be compiled because it uses unknown types.

Let's surround it with a generic function eliminating bad types.

C#
public static Action<knownT, knownP1> A1<T, P1, knownT, knownP1>(MethodInfo method)
        where T : knownT
        where P1 : knownP1
{
        // create first delegate. It is not fine because 
        // its signature contains unknown types T and P1
        Action<T, P1> d = (Action<T, P1>)Delegate.CreateDelegate
				(typeof(Action<T, P1>), method);
        // create another delegate having necessary signature. 
        // It encapsulates first delegate with a closure
        return delegate(knownT target, knownP1 p) { d((T)target, (P1)p); };
}

Note, I've named the function "A1". It will have a special meaning when we will search for it by name.

One more thing remains: we should call this nice function with the same way as createNewObj (do you remember it?). Calling code is more complex because it should support different parameters count.

C#
/// <summary>
/// Creates static caller delegate for specified method
/// </summary>
/// <typeparam name="T">signature of the delegate</typeparam>
/// <param name="method">method to surround</param>
/// <returns>caller delegate with specified signature</returns>
public static T MakeCallDlg<T>(MethodInfo method) 
{            
	// we're going to select necessary generic function and 
         // parameterize it with specified signature

	// 1. select function name accordingly to parameters count
	string creatorName = (method.ReturnParameter.ParameterType == 
	typeof(void) ? "A" : "F") + method.GetParameters().Length.ToString();

	// 2. create parameterization signature
	List<Type> signature = new List<Type>();
	// first type parameter is type of target object
	signature.Add(method.DeclaringType);

	//next parameters are real types of method arguments
	foreach (ParameterInfo pi in method.GetParameters())
	{
	    signature.Add(pi.ParameterType);
	}

	// last parameters are known types of method arguments
	signature.AddRange(typeof(T).GetGenericArguments());

	// 3. call generator function with Delegate.Invoke. 
	// We can do it because the generator will be called only once. 
	// Result will be cached somewhere then.
	return (T)typeof(Helper).GetMethod(creatorName).MakeGenericMethod
		(signature.ToArray()).Invoke(null, new object[] { method });
}

You should prepare several functions like A0, A1, A2, etc, F0, F1, F2, etc for any parameters count you need. MakeCallDlg will select the necessary generic function and fill it with generic parameters.

Now we can call testAction:

C#
Type unknownType = Type.GetType("plugin.p4sdfm2mlwd5");
Type unknownParamType = Type.GetType("plugin.l5fks9rffn33md");
MethodInfo test = unknownType.GetMethod("testAction");

Func<knownType> unknownTypeCreator = 
	Helper.MakeNewDlg<knownType>(unknownType);
Func<knownParamType> unknownParamTypeCreator = 
	Helper.MakeNewDlg<knownParamType>(unknownParamType);            
Action<knownType, knownParamType> methodCaller = 
	Helper.MakeCallDlg<Action<knownType, knownParamType>>(test);

knownType instance = unknownTypeCreator();
knownParamType param = unknownParamTypeCreator();
methodCaller(instance, param);

And again, and again…

C#
methodCaller(unknownTypeCreator(),unknownParamTypeCreator());
methodCaller(unknownTypeCreator(),unknownParamTypeCreator());
methodCaller(unknownTypeCreator(),unknownParamTypeCreator());

I also created F0 and F1. You can add any other functions you need in the same way.

C#
public static Func<knownT, knownR> F0<T, knownT, knownR>(MethodInfo method)
            where T : knownT
{
	Func<T, knownR> d = (Func<T, knownR>)Delegate.CreateDelegate
					(typeof(Func<T, knownR>), method);
	return delegate(knownT target) { return d((T)target); };
}

public static Func<knownT, knownP1, knownR> F1<T, P1, knownT, knownP1, knownR>
							(MethodInfo method)
            where T : knownT
            where P1 : knownP1
{
	Func<T, P1, knownR> d = (Func<T, P1, knownR>)Delegate.CreateDelegate
					(typeof(Func<T, P1, knownR>), method);
	return delegate(knownT target, knownP1 p) { return d((T)target, (P1)p); };
}

You also can access unknown properties with Get and Set methods. These methods are available with:

C#
unknownType.GetProperty("testProperty").GetGetMethod();
unknownType.GetProperty("testProperty").GetSetMethod();

Conclusion

I've compared performance of different calling types (see benchmark code below):  

  • Direct call - 2200
  • My way - 7400
  • MethodInfo.Invoke - 192000

Here is the full source code of the example:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

   #region autogenerated code
    
    //autogenerated code
    namespace plugin {
        public class l5fks9rffn33md : knownParamType
        {
        }

        public class p4sdfm2mlwd5 : knownType
        {
            private l5fks9rffn33md field;

            public void testAction(l5fks9rffn33md p)
            {
                field = p;
            }
            public l5fks9rffn33md testFunction(l5fks9rffn33md p)
            {
                return p;
            }
        }
    } 
    #endregion

    #region known types
    public class knownParamType
    {
    }

    public class knownType
    {
    } 
    #endregion       

    class Helper
    {
        private static Action<knownT, knownP1> A1<T, P1, knownT, 
					knownP1>(MethodInfo method)
            where T : knownT
            where P1 : knownP1
        {
            // create first delegate. It is not fine because its 
	   // signature contains unknown types T and P1
            Action<T, P1> d = (Action<T, P1>)Delegate.CreateDelegate
					(typeof(Action<T, P1>), method);
            // create another delegate having necessary signature. 
	   // It encapsulates first delegate with a closure
            return delegate(knownT target, knownP1 p) { d((T)target, (P1)p); };
        }

        private static Func<knownT, knownR> F0<T, knownT, knownR>(MethodInfo method)
            where T : knownT
        {
            Func<T, knownR> d = (Func<T, knownR>)Delegate.CreateDelegate
					(typeof(Func<T, knownR>), method);
            return delegate(knownT target) { return d((T)target); };
        }

        private static Func<knownT, knownP1, knownR> 
		F1<T, P1, knownT, knownP1, knownR>(MethodInfo method)
            where T : knownT
            where P1 : knownP1
        {
            Func<T, P1, knownR> d = (Func<T, P1, knownR>)
		Delegate.CreateDelegate(typeof(Func<T, P1, knownR>), method);
            return delegate(knownT target, knownP1 p) { return d((T)target, (P1)p); };
        }

        /// <summary>
        /// Creates static caller delegate for specified method
        /// </summary>
        /// <typeparam name="T">signature of the delegate</typeparam>
        /// <param name="method">method to surround</param>
        /// <returns>caller delegate with specified signature</returns>
        public static T MakeCallDlg<T>(MethodInfo method) 
        {            
            // we're going to select necessary generic function 
	   // and parameterize it with specified signature

            // 1. select function name accordingly to parameters count
            string creatorName = (method.ReturnParameter.ParameterType == 
		typeof(void) ? "A" : "F") + 
		method.GetParameters().Length.ToString();

            // 2. create parametrization signature
            List<Type> signature = new List<Type>();
            // first type parameter is type of target object
            signature.Add(method.DeclaringType);

            //next parameters are real types of method arguments
            foreach (ParameterInfo pi in method.GetParameters())
            {
                signature.Add(pi.ParameterType);
            }

            // last parameters are known types of method arguments
            signature.AddRange(typeof(T).GetGenericArguments());

            // 3. call generator function with Delegate.Invoke. 
	   // We can do it because the generator will be called only once. 
	   // Result will be cached somewhere then.
            return (T)typeof(Helper).GetMethod(creatorName, 
		BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod
		(signature.ToArray()).Invoke(null, new object[] { method });
        }


        /// <summary>
        /// Creates static creator delegate for specified type
        /// </summary>
        /// <typeparam name="K">known parent type of specified type</typeparam>
        /// <param name="t">specified type to create</param>
        /// <returns>delegate</returns>
        public static Func<K> MakeNewDlg<K>(Type t)
        {
            return Delegate.CreateDelegate(typeof(Func<K>), 
		typeof(Helper).GetMethod("createNewObj2", 
		BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod
		(new Type[] { t, typeof(K) })) as Func<K>;
        }

        private static K createNewObj2<T, K>() where T : K, new()
        {
            return new T();
        }

        /// <summary>
        /// Creates static creator delegate for specified type
        /// </summary>        
        /// <param name="t">specified type to create</param>
        /// <returns>delegate</returns>
        public static Func<object> MakeNewDlg(Type t)
        {
            return Delegate.CreateDelegate(typeof(Func<object>), 
		typeof(Helper).GetMethod("createNewObj").MakeGenericMethod
		(new Type[] { t })) as Func<object>;
        }

        private static object createNewObj<T>() where T : new()
        {
            return new T();
        }
    }    
    
    class Program
    {
        static void Main(string[] args)
        {
            //find unknown types and method by names
            Type unknownType = Type.GetType("plugin.p4sdfm2mlwd5");
            Type unknownParamType = Type.GetType("plugin.l5fks9rffn33md");
            MethodInfo test = unknownType.GetMethod("testFunction");

            //create and store delegates
            Func<knownType> unknownTypeCreator = 
		Helper.MakeNewDlg<knownType>(unknownType);
            Func<knownParamType> unknownParamTypeCreator = 
		Helper.MakeNewDlg<knownParamType>(unknownParamType);
            Func<knownType, knownParamType, knownParamType> methodCaller = 
		Helper.MakeCallDlg<Func<knownType, knownParamType, 
		knownParamType>>(test);

            //call delegates
            knownType instance = unknownTypeCreator();
            knownParamType param = unknownParamTypeCreator();
            knownParamType result = methodCaller(instance, param);

            //benchmark
            int times = 50000000;

            #region direct call 
            int i = times;
            plugin.p4sdfm2mlwd5 unknownInstance = new plugin.p4sdfm2mlwd5();
            plugin.l5fks9rffn33md unknownParam = new plugin.l5fks9rffn33md();
            DateTime start = DateTime.Now;

            while (i-- > 0)
                result = unknownInstance.testFunction(unknownParam);

            Console.WriteLine("Direct call - {0}", 
		(DateTime.Now - start).TotalMilliseconds);  
            #endregion

            #region my way
            i = times;
            start = DateTime.Now;

            while (i-- > 0)
                result = methodCaller(instance, param);

            Console.WriteLine("My way - {0}", 
		(DateTime.Now - start).TotalMilliseconds); 
            #endregion

            #region Invoke
            i = times;
            object[] paramArray = new object[] { param };
            start = DateTime.Now;

            while (i-- > 0)
                 result = (knownParamType)test.Invoke(instance, paramArray);

            Console.WriteLine("Invoke - {0}", (DateTime.Now - start).TotalMilliseconds); 
            #endregion

            Console.ReadKey();
        }
    }

History

  • 27th February, 2009: Initial post

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


Written By
Software Developer (Senior) HRsoft
Russian Federation Russian Federation
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionGreat article Pin
sergey.rush16-Oct-14 23:59
sergey.rush16-Oct-14 23:59 
Thanks for helping
GeneralMy vote of 5 Pin
Glepp15-Feb-11 23:56
Glepp15-Feb-11 23:56 
QuestionWhat about MEF or AddIns Pin
Sergey Morenko28-Feb-09 14:23
professionalSergey Morenko28-Feb-09 14:23 
AnswerRe: What about MEF or AddIns [modified] Pin
Alexey Z12-Mar-09 4:42
Alexey Z12-Mar-09 4:42 
GeneralMore info needed Pin
PIEBALDconsult27-Feb-09 15:24
mvePIEBALDconsult27-Feb-09 15:24 
GeneralRe: More info needed Pin
Alexey Z127-Feb-09 22:25
Alexey Z127-Feb-09 22:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.