Wei
Posted on December 8, 2020
Github Link : Trace-Dapper.NET-Source-Code
8. Strongly Typed Mapping Part5: Emit IL convert to C# code
With the concept of the previous Expression version, we can then enter the core technology of Dapper: Emit.
First of all, there must be a concept, MSIL (CIL) is intended for JIT compiler, so the readability will be poor and difficult to debug, but more detailed logical operations can be done compared to Expression.
In the actual environment development and use Emit, usually c# code > Decompilation to IL > use Emit to build dynamic methods
, for example:
- First create a simple printing example:
void SyaHello()
{
Console.WriteLine("Hello World");
}
- Decompile and view IL
SyaHello:
IL_0000: nop
IL_0001: ldstr "Hello World"
IL_0006: call System.Console.WriteLine
IL_000B: nop
IL_000C: ret
- Use DynamicMethod + Emit to create a dynamic method
void Main()
{
// 1. create void method()
DynamicMethod methodbuilder = new DynamicMethod("Deserialize" + Guid.NewGuid().ToString(),typeof(void),null);
// 2. Create the content of the method body by Emit
var il = methodbuilder.GetILGenerator();
il.Emit(OpCodes.Ldstr, "Hello World");
Type[] types = new Type[1]
{
typeof(string)
};
MethodInfo method = typeof(Console).GetMethod("WriteLine", types);
il.Emit(OpCodes.Call,method);
il.Emit(OpCodes.Ret);
// 3. Convert the specified type of Func or Action
var action = (Action)methodbuilder.CreateDelegate(typeof(Action));
action();
}
But this is not the process for a project that has been written. Developers may not kindly tell you the logic of the original design.
How to check like Dapper, only Emit IL doesn’t have C# Source Code Project
My solution is: 「Since only Runtime can know IL, save IL as a static file and decompile and view」
You can use the MethodBuild + Save
method here Save IL as static exe file > Decompile view
, but you need to pay special attention
- Please correspond to the parameters and return type, otherwise it will compile error.
- netstandard does not support this method, Dapper needs to be used
region if yourversion
to distinguish, otherwise it cannot be used, such as picture
code show as below :
//Use MethodBuilder to view Emit IL that others have written
//1. Create MethodBuilder
AppDomain ad = AppDomain.CurrentDomain;
AssemblyName am = new AssemblyName();
am.Name = "TestAsm";
AssemblyBuilder ab = ad.DefineDynamicAssembly(am, AssemblyBuilderAccess.Save);
ModuleBuilder mb = ab.DefineDynamicModule("Testmod", "TestAsm.exe");
TypeBuilder tb = mb.DefineType("TestType", TypeAttributes.Public);
MethodBuilder dm = tb.DefineMethod("TestMeThod", MethodAttributes.Public |
MethodAttributes.Static, type, new[] { typeof(IDataReader) });
ab.SetEntryPoint(dm);
// 2. the IL code
//..
// 3. Generate static files
tb.CreateType();
ab.Save("TestAsm.exe");
Then use this method to decompile Dapper Query Mapping IL in the GetTypeDeserializerImpl method, and you can get the C# code:
public static User TestMeThod(IDataReader P_0)
{
int index = 0;
User user = new User();
object value = default(object);
try
{
User user2 = user;
index = 0;
object obj = value = P_0[0];
if (!(obj is DBNull))
{
user2.Name = (string)obj;
}
index = 1;
object obj2 = value = P_0[1];
if (!(obj2 is DBNull))
{
user2.Age = (int)obj2;
}
user = user2;
return user;
}
catch (Exception ex)
{
SqlMapper.ThrowDataException(ex, index, P_0, value);
return user;
}
}
After having the C# code, it will be much faster to understand the Emit logic.
9. Strongly Typed Mapping Principle Part6: Emit Version
The following code is the Emit version, I wrote the corresponding IL part of C# code
public static class DemoExtension
{
public static IEnumerable<T> Query<T>(this IDbConnection cnn, string sql) where T : new()
{
using (var command = cnn.CreateCommand())
{
command.CommandText = sql;
using (var reader = command.ExecuteReader())
{
var func = GetTypeDeserializerImpl(typeof(T), reader);
while (reader.Read())
{
var result = func(reader as DbDataReader);
yield return result is T ? (T)result : default(T);
}
}
}
}
private static Func<DbDataReader, object> GetTypeDeserializerImpl(Type type, IDataReader reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false)
{
var returnType = type.IsValueType ? typeof(object) : type;
var dm = new DynamicMethod("Deserialize" + Guid.NewGuid().ToString(), returnType, new[] { typeof(IDataReader) }, type, true);
var il = dm.GetILGenerator();
//C# : User user = new User();
//IL :
//IL_0001: newobj
//IL_0006: stloc.0
var constructor = returnType.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)[0];
il.Emit(OpCodes.Newobj, constructor);
var returnValueLocal = il.DeclareLocal(type);
il.Emit(OpCodes.Stloc, returnValueLocal); //User user = new User();
// C# :
//object value = default(object);
// IL :
//IL_0007: ldnull
//IL_0008: stloc.1 // value
var valueLoacl = il.DeclareLocal(typeof(object));
il.Emit(OpCodes.Ldnull);
il.Emit(OpCodes.Stloc, valueLoacl);
int index = startBound;
var getItem = typeof(IDataRecord).GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(p => p.GetIndexParameters().Length > 0 && p.GetIndexParameters()[0].ParameterType == typeof(int))
.Select(p => p.GetGetMethod()).First();
foreach (var p in type.GetProperties())
{
//C# : value = P_0[0];
//IL:
//IL_0009: ldarg.0
//IL_000A: ldc.i4.0
//IL_000B: callvirt System.Data.IDataRecord.get_Item
//IL_0010: stloc.1 // value
il.Emit(OpCodes.Ldarg_0);
EmitInt32(il, index);
il.Emit(OpCodes.Callvirt, getItem);
il.Emit(OpCodes.Stloc, valueLoacl);
//C#: if (!(value is DBNull)) user.Name = (string)value;
//IL:
// IL_0011: ldloc.1 // value
// IL_0012: isinst System.DBNull
// IL_0017: ldnull
// IL_0018: cgt.un
// IL_001A: ldc.i4.0
// IL_001B: ceq
// IL_001D: stloc.2
// IL_001E: ldloc.2
// IL_001F: brfalse.s IL_002E
// IL_0021: ldloc.0 // user
// IL_0022: ldloc.1 // value
// IL_0023: castclass System.String
// IL_0028: callvirt UserQuery+User.set_Name
il.Emit(OpCodes.Ldloc, valueLoacl);
il.Emit(OpCodes.Isinst, typeof(System.DBNull));
il.Emit(OpCodes.Ldnull);
var tmpLoacl = il.DeclareLocal(typeof(int));
il.Emit(OpCodes.Cgt_Un);
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Ceq);
il.Emit(OpCodes.Stloc,tmpLoacl);
il.Emit(OpCodes.Ldloc,tmpLoacl);
var labelFalse = il.DefineLabel();
il.Emit(OpCodes.Brfalse_S,labelFalse);
il.Emit(OpCodes.Ldloc, returnValueLocal);
il.Emit(OpCodes.Ldloc, valueLoacl);
if (p.PropertyType.IsValueType)
il.Emit(OpCodes.Unbox_Any, p.PropertyType);
else
il.Emit(OpCodes.Castclass, p.PropertyType);
il.Emit(OpCodes.Callvirt, p.SetMethod);
il.MarkLabel(labelFalse);
index++;
}
// IL_0053: ldloc.0 // user
// IL_0054: stloc.s 04
// IL_0056: br.s IL_0058
// IL_0058: ldloc.s 04
// IL_005A: ret
il.Emit(OpCodes.Ldloc, returnValueLocal);
il.Emit(OpCodes.Ret);
var funcType = System.Linq.Expressions.Expression.GetFuncType(typeof(IDataReader), returnType);
return (Func<IDataReader, object>)dm.CreateDelegate(funcType);
}
private static void EmitInt32(ILGenerator il, int value)
{
switch (value)
{
case -1: il.Emit(OpCodes.Ldc_I4_M1); break;
case 0: il.Emit(OpCodes.Ldc_I4_0); break;
case 1: il.Emit(OpCodes.Ldc_I4_1); break;
case 2: il.Emit(OpCodes.Ldc_I4_2); break;
case 3: il.Emit(OpCodes.Ldc_I4_3); break;
case 4: il.Emit(OpCodes.Ldc_I4_4); break;
case 5: il.Emit(OpCodes.Ldc_I4_5); break;
case 6: il.Emit(OpCodes.Ldc_I4_6); break;
case 7: il.Emit(OpCodes.Ldc_I4_7); break;
case 8: il.Emit(OpCodes.Ldc_I4_8); break;
default:
if (value >= -128 && value <= 127)
{
il.Emit(OpCodes.Ldc_I4_S, (sbyte)value);
}
else
{
il.Emit(OpCodes.Ldc_I4, value);
}
break;
}
}
}
There are many detailed of Emit here. First pick out the important concepts to explain.
Emit Label
In Emit if/else, you need to use Label positioning, tell the compiler which position to jump to when the condition is true/false, for example: boolean to integer
, assuming that you want to simply convert Boolean to Int, C# code can use If it is True Return 1 otherwise return 0
logic to write:
public static int BoolToInt(bool input) => input ? 1 : 0;
When converting to Emit, the following logic is required:
- Consider the label dynamic positioning problem
- The label must be established first to let Brtrue_S know which label position to set when the conditions are true
(Note:at this time the label position has not been determined yet)
- Continue to build IL from top to bottom in order
- Wait until
match condition
you want to run the blockprevious line
, use itMarkLabel to position Label
.
The final c # Emit Code:
public class Program
{
public static void Main(string[] args)
{
var func = CreateFunc();
Console.WriteLine(func(true)); //1
Console.WriteLine(func(false)); //0
}
static Func<bool, int> CreateFunc()
{
var dm = new DynamicMethod("Test" + Guid.NewGuid().ToString(), typeof(int), new[] { typeof(bool) });
var il = dm.GetILGenerator();
var labelTrue = il.DefineLabel();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Brtrue_S, labelTrue);
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Ret);
il.MarkLabel(labelTrue);
il.Emit(OpCodes.Ldc_I4_1);
il.Emit(OpCodes.Ret);
var funcType = System.Linq.Expressions.Expression.GetFuncType(typeof(bool), typeof(int));
return (Func<bool, int>)dm.CreateDelegate(funcType);
}
}
Here you can find the Emit version, which has the advantage of:
Can do more detailed operations
Because the detail granularity is small, the efficiency that can be optimized is better
Disadvantages:
- Difficult to debug
- Poor readability
- The amount of code becomes larger and the complexity increases
Then look at the suggestions of the author of Dapper. Now there is no need to use Emit in general projects. Using Expression + Func/Action can solve most of the needs of dynamic methods, especially when Expression supports Block and other methods. Link c#-What's faster: expression trees or manually emitting IL
Having said that, there are some powerful open source projects that use Emit to manage details If you want to understand them, you need the basic Emit IL concept
.
Posted on December 8, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.