一. 介紹
最近充能看書,在書上看到函數調用可以 " 通過 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 調用函數方法的研究基本成功!