Note: phiên bản Tiếng Việt của bài này ở link dưới.
https://duongnt.com/fastandfaster-vie
Last year, I wrote an article about dynamic code generation in C# .NET. Earlier this year, my former employer Medidata Solutions generously allowed me to spend two weeks developing the idea in that article into a FOSS package as an innovation project. Today, I’m pleased to introduce you to the FastAndFaster library. It can be used to dynamically create objects and call their methods at runtime.
You can find the complete source code at the link below.
https://github.com/duongntbk/FastAndFaster
Or you can install it as a Nuget package.
dotnet add package FastAndFaster
The general idea
For the guide on how to use the FastAndFaster package, please see the document here and the overview here. In today’s article, I would like to talk about some interesting details.
Roughly speaking FastAndFaster builds on the techniques introduced in my previous article. Below are some of the noticeable improvements.
- The code to create delegates to initialize object instances is wrapped into the Initializer class.
- The code to create delegates to invoke method calls is wrapped into the Invocator class. Note that it is possible to invoke instance, static, as well as generic methods.
- All delegates are cached so that we don’t waste time recreating them in successive calls. By default, each cache item will expire after 12 hours. But this is customizable.
Handle generic methods
It is easy to invoke a non-generic method. As we can see here, we simply search for its MethodInfo
by calling an overload of the GetMethod. The array of parameter types helps us locate the correct MethodInfo
. Then we can use it to create the necessary delegate.
methodInfo = type.GetMethod(methodName, parameterTypes);
However, the approach above does not work with a generic method. At the time we retrieve the MethodInfo
, the type arguments are not provided to the method yet. And the Type[]
argument of GetMethod
cannot contain an unbounded type. We cannot use it to retrieve the MethodInfo
. Thus, we need to use the following solution.
- Store the list of concrete types we want to use with the generic method in a helper class called GenericInfo. This class also stores the index of each generic type in the array of method parameters.
- Use GetMethods to retrieve all MethodInfos associated with the target class. Then filter for all generic methods with the provided name.
var candidates = type.GetMethods().Where(n => n.Name == methodName && n.IsGenericMethod);
- For each candidate, check if its parameters array has generic types at the correct indexes. Return the matching generic
MethodInfo
(if found). - Then use the list of concrete types from
GenericInfo
to call MakeGenericMethod to create a constructed method.
After all of that, we finally have the MethodInfo
we need to create the delegate.
Handling static methods
The IL of a static method and a non-static method generally follows the same structure. The biggest difference is that for a non-static method, we need to load the class instance onto the stack. While for a static method, there is no class instance. This step corresponds to the following IL.
IL_0000: ldarg.0
IL_0001: castclass <target class>
In FastAndFaster, to handle static methods we first need to check the property methodInfo.IsStatic
. Only if the target is an instance method, do we load the object onto the stack with the helper method LoadTarget.
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Castclass, type);
For static methods, we can skip the code above entirely.
Handle value type
As mentioned in the Value type and boxing section, we need to deal with boxing and unboxing. The library uses object
type to store all method arguments and the return value. Because of that, we need to unbox all value type method arguments and box value type return value.
- The helper method LoadArguments checks if each parameter expects a value type of reference type argument, and acts accordingly.
if (parameterTypes[i].IsValueType) { il.Emit(OpCodes.Unbox_Any, parameterTypes[i]); } else { il.Emit(OpCodes.Castclass, parameterTypes[i]); }
- Likewise, ExecuteMethod can check if it is necessary to box the return value before emitting
OpCodes.Ret
.if (methodInfo.ReturnType != typeof(void) && methodInfo.ReturnType.IsValueType) { il.Emit(OpCodes.Box, methodInfo.ReturnType); }
Conclusion
Writing FastAndFaster was a very interesting experience for me. I hope you can also find it useful. The library is completely free, so please go ahead and try it out. And if you have any ideas or suggestions, please don’t hesitate to open a pull request.
One Thought on “FastAndFaster and dynamic code generation”