C#如何用IL和Emit类通过Calli来实现实例函数与静态函数的调用
一. 介绍
最近充能看书,在书上看到函数调用可以 " 通过 ldftn 获得函数指针,然后使用 calli 指令 " 来进行调用,并说这种行为 " 类似 C 的函数指针,但是 C# 不支持这种行为 ",那么这是一种什么样的调用呢?我翻阅了一些资料,才知道 ldftn 和 calli 分别是 IL 语言中的两个指令 ,也就是说这是一种基于 IL 语言的调用。
事实上,C#确实不直接支持这种方式调用函数,但是却可以通过 Emit 类的相关方法来构造 IL 来间接的实现调用。那么,我们开始看看是怎么实现的吧。
二. 实现准备和实现原理
1. 首先我们要实现的方法调用,首先我们有如下类:
public class A { public void Test(int num) { Console.WriteLine(num); } public static void Test2(int num) { Console.WriteLine(num); } }
2. IL语言相关:
(1)Ldftn 指令:
- 语法:ldftn <token>
- 功能:把函数指针加载到由 MethodDef 或 MemberRef 类型的 <token> 所指定的方法上
(2)Calli 指令:
- 语法:calli <token>
- 功能:先从栈上弹出函数指针,再从栈上弹出所有的参数,然后根据 <toke> 指定的方法签名进行间接方法调用。<token> 必须是有效的 StandAloneSig 标记。函数指针必须位于栈顶。如过方法返回数值,那么该数值在调用完成后被压入栈上。
★ 3. IL调用函数方法的一般流程:
一般在IL中使用方式分为两大类,一种是调用托管函数,另外一种是调用非托管的函数,这里我们暂不考虑对非托管函数的调用。对于托管函数,按照其调用方法是不是要引用一个对象实列,可以分为静态方法和实例方法。
接下来就对这两种类型的方法,分别描述一下 IL 代码的一般流程。
(1)实例方法:
过程如下:
- 通过 " newobj instance 构造函数签名 " 先创建一个实例对象,并将对象从栈顶弹出保存到局部变量
- 通过 " ldftn instance 实例方法签名 " 来获得函数方法的指针,调用完成后这个指针会被放置于栈顶,同理也将栈顶的函数指针弹出并保存到局部变量
- 把实例对象放置到栈顶,由于实例方法要 this 指向的对象,所以这个对象会作为第一个参数 arg.0 作为 this
- 按照函数的调用的参数的顺序,依次将变量放置到栈顶
- 把函数的指针放置到栈顶
- 通过 " calli instance 实例方法签名 " 来进行函数的调用,调用完成之后会把返回值放置与栈顶,最后把栈顶的返回值弹出并保存使用
(2)静态方法:
静态方法不需要 this 指向的对象,所以这边也不需要先创建一个对象,过程如下:
- 通过 " ldftn 静态方法签名 " 来获得函数方法的指针,调用完成后这个指针会被放置于栈顶,将栈顶的函数指针弹出并保存到局部变量
- 按照函数的调用的参数的顺序,依次将变量放置到栈顶
- 把函数的指针放置到栈顶
- 通过 " calli 静态方法签名 " 来进行函数的调用,调用完成之后会把返回值放置与栈顶,最后把栈顶的返回值弹出并保存使用
(3)一个简单的实例调用的例子:
.locals init (native int fnptr) ... ldfrn void [mscorlib]System.Console::WriteLine(int32) stloc.0 //本地变量中存储函数指针 ... ldc.i4 12345 //加载参数 ldloc.0 callo void(int32) ...
下面我们看一下怎么实现对 Test 和 Test2 的调用的,直接上菜...
三. IL 代码实现
IL 代码如下:
.assembly extern mscorlib { auto } .assembly MyTest {} .module MyTest.exe .class public A { .method public specialname void .ctor() { ldarg.0 call instance void [mscorlib]System.Object::.ctor() ret } .method public void Test(int32 param_0) { ldarg.1 call void [mscorlib]System.Console::WriteLine(int32) ret } .method public static void Test2(int32 param_0) { ldarg.0 call void [mscorlib]System.Console::WriteLine(int32) ret } } .method public static void Main() { .entrypoint .locals (class A a,int32 v_1) //实例方法调用 newobj instance void A::.ctor() stloc.0 ldftn instance void A::Test(int32) stloc.1 ldloc.0 ldc.i4 120 ldloc.1 calli instance void(int32) //静态方法调用 ldftn void A::Test2(int32) stloc.1 ldc.i4 233 ldloc.1 calli void(int32)
ret }
四. C# 用 Emit 类来实现
C# 代码:
public class CreateTypeHelper { public static Type CreateMethodCallingType() { //获得当前的程序域 AppDomain currentDomain = Thread.GetDomain(); //创建这个类的程序集 AssemblyName assemblyName = new AssemblyName(); assemblyName.Name = "DynamicAssembly"; AssemblyBuilder assemblyBuilder = currentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave); //创建模块 ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MethodCallingModule", "MethodCalling.dll"); //创建类型 TypeBuilder typeBuilder = moduleBuilder.DefineType("MethodCalling", TypeAttributes.Public); //创建一个方法 MethodBuilder methodBuilder = typeBuilder.DefineMethod("CalliMethodCall", MethodAttributes.Public | MethodAttributes.Static, typeof(void), new Type[0]); //IL ILGenerator il = methodBuilder.GetILGenerator(); //.locals (class A a,int32 v_1) il.DeclareLocal(typeof(A)); il.DeclareLocal(typeof(Int32)); //newobj instance void A::.ctor() ConstructorInfo constructorInfo = typeof(A).GetConstructor(new Type[0]); il.Emit(OpCodes.Newobj, constructorInfo); //stloc.0 il.Emit(OpCodes.Stloc_0); //获得A.Test方法 MethodInfo methodInfo = typeof(A).GetMethod("Test", BindingFlags.Public | BindingFlags.Instance); //ldftn instance void A::Test(int32) il.Emit(OpCodes.Ldftn, methodInfo); //stloc.1 il.Emit(OpCodes.Stloc_1); //ldloc.0 il.Emit(OpCodes.Ldloc_0); //ldc.i4 120 il.Emit(OpCodes.Ldc_I4, 120); ////ldloc.1 il.Emit(OpCodes.Ldloc_1); //calli instance void(int32) il.EmitCalli(OpCodes.Calli, CallingConventions.HasThis, typeof(void), new Type[] { typeof(Int32) }, null); ////获得A.Test方法 MethodInfo methodInfo1 = typeof(A).GetMethod("Test2", BindingFlags.Public | BindingFlags.Static); //ldftn void A::Test2(int32) il.Emit(OpCodes.Ldftn, methodInfo1); //stloc.1 il.Emit(OpCodes.Stloc_1); //ldc.i4 233 il.Emit(OpCodes.Ldc_I4, 233); //ldloc.1 il.Emit(OpCodes.Ldloc_1); //calli void(int32) il.EmitCalli(OpCodes.Calli,CallingConventions.Standard, typeof(void), new Type[] { typeof(Int32) }, null); //ret il.Emit(OpCodes.Ret); Type retType = typeBuilder.CreateType(); assemblyBuilder.Save("MethodCalling.dll"); return retType; } }
上端调用方法:
Type type = CreateTypeHelper.CreateMethodCallingType(); //获得方法 MethodInfo methodInfo = type.GetMethod("CalliMethodCall"); if (methodInfo != null) { methodInfo.Invoke(null, null); }
执行结果:
五. 验证结果
最后我们验证一下动态生成的类。在代码中,我们创建了一个 " MethodCalling.dll " 用来保存动态生成的类,里面承载了我们的 Emit 代码生成的 IL代码,如下图:
用 ILDasm.exe 查看后如下图:
看了一下与我们写的 IL 代码基本一致,今天对 Calli 调用函数方法的研究基本成功!