Fast Late-bound Calls with Generics in .NET
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:
//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:
//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:
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:
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:
//Helper.createNewObj<plugin.p4sdfm2mlwd5>();
Because type p4sdfm2mlwd5
is unknown.
Let's use two rare functions: MethodInfo.MakeGenericMethod
and Delegate.CreateDelegate
:
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:
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:
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:
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:
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:
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:
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.
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.
/// <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
:
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…
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.
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:
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:
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