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

posted @ 2019-11-23 19:49  霁雪湖上三映月  阅读(2290)  评论(0编辑  收藏  举报