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 調用函數方法的研究基本成功!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM