65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Reflection.Emit 创建动态类型

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (80投票s)

2010年10月25日

CPOL

23分钟阅读

viewsIcon

318699

downloadIcon

2536

在本文中,我提供了示例代码,以确保您可以使用 Reflection.Emit 类轻松构建自己的动态类型。

目录

大家好,很久没给你们写文章了。虽然我一直在为你们写技术博客,但真的很想分享一个有助于数据库的教程。最终决定开始写一个。

在努力学习 Reflection 类之后,我认为我可以对代码生成做一些研究。我选择了 CodeDom 作为生成代码的最佳替代方案。然而,我很快发现 CodeDom 实际上允许你构建你的程序集,但它不允许你在运行时动态编译程序集的一部分,而是调用编译器来完成。因此,我认为除了 CodeDom 之外,一定还有其他东西可以满足我的需求。

接下来我找到了一个,使用 Expression Trees。如果你已经关注我了,我想你知道,几天前我已经写过关于表达式树和 Lambda 分解的文章。所以现在不是回顾同一主题的好时机。后来,我研究了 MSIL,发现它值得学习。如果你想在 .NET 中成长,了解 MSIL 将是一个额外的优势。因此,我开始研究 MSIL。最后,我发现了一些可以帮助你动态构建 Type 的类。让我与你分享所有这些。

引言 

Reflection.Emit 类似于 CodeDom,它允许你构建自定义程序集,并提供许多 Builder 类,这些类可以在运行时编译,从而调用 C# 的 DLR 功能。该库还公开了一个 ILGenerator,稍后可以通过发出操作码来生成实际的 MSIL。因此,当你正确编写操作码后,就可以在运行时动态编译类型。在这篇文章中,我将使用 ILDASM 来查看我们自己定义的类的生成 IL,然后我将尝试动态构建相同的类。 

什么是 Reflection? 

如果你对 Reflection 感到困惑,那么你需要真正振作起来才能进一步。让我简要解释一下 Reflection。Reflection 实际上是一种读取应用程序引用或未引用的托管 dll 并调用其类型的技术。换句话说,它是一种在运行时发现类型并调用其属性的机制。例如,你有一个外部 dll,它写入日志信息并发送到服务器。在这种情况下,你有两种选择。

  • 你直接引用程序集并调用其方法。
  • 你使用 Reflection 加载程序集并使用接口调用它。

如果您想为您的应用程序构建一个真正解耦的架构,例如可以稍后插入应用程序的架构,那么始终最好选择第二种选项。让我更清楚地说明一下,假设您希望客户从您的服务器下载日志 dll 并在需要时将其插入应用程序。相信我,除了使用 Reflection 别无选择。Reflection 类允许您将外部程序集加载到应用程序并在运行时调用其类型。

要了解更多信息,请尝试反射概述。 

什么是 Reflection.Emit? 

作为 Reflection 的一部分,Reflection.Emit 命名空间列出了许多可用于构建类型的类。正如我之前告诉你的,`Reflection.Emit` 实际上为你提供了一些 Builder 类,如 `AssemblyBuilder`、`ModuleBuilder`、`ConstructorBuilder`、`MethodBuilder`、`EventBuilder`、`PropertyBuilder` 等,它们允许你在运行时动态构建 IL。`ILGenerator` 提供了生成 IL 并将其放置在方法中的能力。通常,开发人员很少需要这些能力来在运行时生成程序集,但很高兴发现框架中存在这些能力。 

现在让我们看看构建程序集需要什么。

生成程序集的步骤

现在让我们回顾一下创建程序集的步骤

  1. 在应用程序域中创建程序集。AssemblyBuilder 将帮助您完成此操作。
  2. Assembly 内部创建模块
  3. 在模块内部创建多个类型
  4. 在类型内部添加属性、方法、事件等。
  5. 使用 ILGenerator 写入属性、方法等。

基本上,这些是创建自己的动态生成程序集的常见步骤。

structure.JPG

从上图中,您可以清楚地看到 CLR 程序集的整个结构。AppDomain 是层次结构的根,它创建 Assembly,然后是 Module,然后是 Type。如果您更仔细地查看 IL,您会理解 Delegate 也是一个继承自 System.MultiCastDelegate 的类,而 Struct 派生自 System.ValueTypes。每个类型可能包含其成员,每个成员方法或属性都可以有其 OPCodes、Locals 和 Parameters。Locals 定义您在方法体内部定义的局部变量,而 OpCodes 是 IL 的指令代码。

创建动态程序集的步骤

现在让我们一步一步地使用 IL 自己构建一个动态程序集。

步骤 1. 动态创建程序集

public AssemblyBuilder GetAssemblyBuilder(string assemblyName)
{
    AssemblyName aname = new AssemblyName(assemblyName);
    AppDomain currentDomain = AppDomain.CurrentDomain; // Thread.GetDomain();
    AssemblyBuilder builder = currentDomain.DefineDynamicAssembly(aname, 
                               AssemblyBuilderAccess.Run);
    return builder;
} 

要创建程序集,您需要程序集的名称才能唯一标识程序集。我使用了 AssemblyName 类来命名程序集。AppDomain 是程序集将被创建的地方。这非常重要,因为应用程序在调用跨域对象时可能会出现问题。为了使其更简单,我没有创建新的 AppDomain,而是使用程序正在运行的 CurrentDomain。最后,我创建了一个 AssemblyBuilder 对象,它最终会使用唯一的名称 aname 构建程序集。AssemblyBuilderAccess 指定了程序集对我们的可访问性。正如我所做的那样,如果您使用 Run,这意味着程序集只能使用 Reflection 动态运行,而不能保存以备将来使用。浏览每个值以查看输出。

注意:如果您为程序集定义了自定义属性,您可以轻松使用 SetCustomAttriburte 将您的自定义属性添加到程序集。

AssemblyBuilder 的一些功能(供进一步阅读)

AssemblyBuilder 允许您定义许多功能,例如

  • AddResourceFile :允许您指定要作为资源添加到程序集的文件。
  • DefineUnmanagedResource / DefineResource:为程序集添加一个非托管资源
  • EntryPoint/SetEntryPoint:要定义的一个特殊子例程/方法,当程序集被调用时将自动调用。
  • SetCustomAttribute:允许您为程序集指定属性。
  • DefineDynamicModule :为程序集定义模块,实际代码将包含在此模块中。

在构建程序集时有更多的灵活性。.NET 提供了我们通过库可以获得的所有功能,并公开了方法以确保我们可以通过 Reflection.Emit 来实现。您可以尝试 MSDN 阅读更多关于它公开的方法。

步骤 2:创建模块

模块是对象的一部分,实际的类将保留在那里。模块是我们放置所有类的容器。让我们为自己创建一个模块。

public ModuleBuilder GetModule(AssemblyBuilder asmBuilder)
{
    ModuleBuilder builder = asmBuilder.DefineDynamicModule("EmitMethods", 
                                           "EmitMethods.dll");
    return builder;
}

因此,该方法实际上需要一个 ModuleName、一个唯一的模块名称以及程序集将导出到的文件名。

一些有用的方法

模块公开了一些方法,例如

  • DefineEnum :允许您定义一个枚举,它返回一个 EnumBuilder。
  • DefineType :允许您定义一个类型/类。
  • DefineManifestResource :一个 dll 包含一个二进制清单。此方法允许您定义清单。 
  • DefinePInvokeMethod :允许您为程序集定义一个 PInvoke 方法 (COM)。

步骤 3:创建类型

这才是关键。要创建一个类、结构、委托等,你需要定义一个 TypeBuilder。从现在开始,我将查看使用 ILDASM 生成的实际 IL,然后为你生成相同的输出。

public TypeBuilder GetType(ModuleBuilder modBuilder, string className)
{
    TypeBuilder builder = modBuilder.DefineType(className, TypeAttributes.Public);
    return builder;
}

public TypeBuilder GetType(ModuleBuilder modBuilder, string className, 
                                  params string[] genericparameters)
{
    TypeBuilder builder = modBuilder.DefineType(className, TypeAttributes.Public);
    GenericTypeParameterBuilder[] genBuilders = builder.DefineGenericParameters(
                                                    genericparameters);

    foreach (GenericTypeParameterBuilder genBuilder in genBuilders) 
       // We take each generic type T : class, new()
    {
        genBuilder.SetGenericParameterAttributes(
                 GenericParameterAttributes.ReferenceTypeConstraint | 
                              GenericParameterAttributes.DefaultConstructorConstraint);
        //genBuilder.SetInterfaceConstraints(interfaces);
    }

    return builder;
}

上述 GetType 方法有两个重载。正如您所见,第一个很简单,我只指定了类的名称和 ModuleBuilder,该方法返回 TypeBuilder

在第二个重载中,我放置了一个额外的字符串参数数组,它定义了类的每个泛型类型。GenericTypeParameterBuilder 允许您定义 GenericTypeParameter。一旦您定义了 GenericTypeParameters 并设置了其约束属性,您就可以使用构建器了。

一些有用的方法

与类相比,TypeBuilder 允许您定义具有所有选项的完整结构。其中一些是

  • DefineField / DefineMethod / DefineProperties / DefineEvent:您可以使用这些方法生成类成员。
  • DefineMethodOverride :允许您在类型继承自另一个基类型时覆盖现有方法
  • DefineConstructor / DefineDefaultConstructor:指定当前类型的构造函数。 
  • AddInterfaceImplementation :允许您从另一个接口实现当前类型。

步骤 4:创建方法

方法是任何程序的构建块。我们将定义许多方法来阐明如何轻松地从 IL 构建方法。暂时,让我们使用 MethodBuilder 创建一个动态方法。

public MethodBuilder GetMethod(TypeBuilder typBuilder, string methodName)
{
    MethodBuilder builder = typBuilder.DefineMethod(methodName, 
                        MethodAttributes.Public | MethodAttributes.HideBySig);
    return builder;
}
public MethodBuilder GetMethod(TypeBuilder typBuilder, string methodName, 
                      Type returnType, params Type[] parameterTypes)
{
    MethodBuilder builder = typBuilder.DefineMethod(methodName, 
                      MethodAttributes.Public | MethodAttributes.HideBySig, 
                             CallingConventions.HasThis, returnType, parameterTypes);
    return builder;
}

public MethodBuilder GetMethod(TypeBuilder typBuilder, string methodName, 
                   Type returnType, string[] genericParameters, params Type[] 
                                                                  parameterTypes)
{
    MethodBuilder builder = typBuilder.DefineMethod(methodName, 
         MethodAttributes.Public | MethodAttributes.HideBySig, 
                     CallingConventions.HasThis, returnType, parameterTypes);

    GenericTypeParameterBuilder[] genBuilders = 
                           builder.DefineGenericParameters(genericParameters);

    foreach (GenericTypeParameterBuilder genBuilder in genBuilders) 
                  // We take each generic type T : class, new()
    {
        genBuilder.SetGenericParameterAttributes(
                   GenericParameterAttributes.ReferenceTypeConstraint | 
                       GenericParameterAttributes.DefaultConstructorConstraint);
        //genBuilder.SetInterfaceConstraints(interfaces);
    }
    return builder;
}

因此,上述方法将返回 MethodBuilder,它允许您定义 IL 代码。您可以看到我为此指定了 3 个重载。这些重载允许您为方法放置参数和泛型类型参数。

现在,构建类型后,您需要创建 Locals(局部变量)并使用 OpCode 指令。

使用 ILGenerator 发出操作码

为了定义您的 OpCodes,您将需要 ILGenerator。ILGenerator 允许您为方法体发出 IL,从而允许您为您即将构建的方法创建指令集。让我首先介绍一些指令集,它们可以帮助您将传入方法的两个整数变量相加,并返回一个浮点值作为结果。

public void CreateMethod()
{
    AppDomain currentDomain = AppDomain.CurrentDomain;
    AssemblyBuilder asmbuilder = this.GetAssemblyBuilder("MyAssembly");
    ModuleBuilder mbuilder = this.GetModule(asmbuilder);
    TypeBuilder tbuilder = this.GetTypeBuilder(mbuilder, "MyClass");

    Type[] tparams = { typeof(System.Int32), typeof(System.Int32) };
    MethodBuilder methodSum = this.GetMethod(tbuilder, "Sum", typeof(System.Single), 
                                                                 tparams);
    
    ILGenerator generator = methodSum.GetILGenerator();

    generator.DeclareLocal(typeof(System.Single));  
    generator.Emit(OpCodes.Ldarg_1);    
    generator.Emit(OpCodes.Ldarg_2);    
    generator.Emit(OpCodes.Add_Ovf);    
    generator.Emit(OpCodes.Conv_R4);    
    generator.Emit(OpCodes.Stloc_0);    
          
    generator.Emit(OpCodes.Ldloc_0);    
    generator.Emit(OpCodes.Ret);        
}

如果你仔细看上面的代码,代码实际上在 CurrentDomain 中创建了一个程序集,并在其中包含一个动态创建的类型 MyClass。因此,创建的类将包含 Sum 方法。

带有 ILGenerator.Emit 的代码行实际上将 IL 发送到 Sum 方法体。每个方法都必须声明局部堆栈元素来运行其数据。在 IL 中,我们在调用任何指令代码之前声明局部变量。就像这样,我使用了 DeclareLocal 在方法中声明了一个 float32 局部变量。DeclareLocal 方法实际上返回一个 LocalBuilder,您也可以使用它来操作此变量的索引。在声明所有局部变量之后,我们首先加载参数列表 Ldarg_1 Ldarg_2 (因为第一个参数是隐式对象 this)。Add_Ovf 实际上将两个加载的参数相加并将其传递给局部变量 Stloc_0 (它表示堆栈的顶部元素或我们在索引 0 处创建的局部变量)。接下来,Ldloc_0 弹出值并将其返回给外部世界。

现在,这是生成您自己的类型的最简单的示例。现在让我进一步构建一个更具体的类型

一个更具体的示例

既然我们已经构建了自己的类型,是时候为您提供一个构建更具体类型的示例了。在我们演示之前,让我向您展示我们将如何在运行时动态创建代码,并调用其方法以获取输出。请注意,我已尝试在一定程度上简化代码,以便帮助您更好地理解代码。

public interface IBuilder
{
    float Sum(int firstnum, int secondnum);
    float Substract(int firstnum, int secondnum);
    float Multiply(int firstnum, int secondnum);
    float Divide(int firstnum, int secondnum);
}

public class Builder : IBuilder
{

    # region Event
    public delegate void BuilderDelegate(string message);

    public event BuilderDelegate InvokeMessage;

    public virtual void OnInvokeMessage(string message)
    {
        if (this.InvokeMessage != null)
            this.InvokeMessage(message);
    }

    # endregion

    # region Fields
    private int firstNum, secondNum;
    public int FirstNum
    {
        [DebuggerStepThrough()]
        get { return this.firstNum; }
        set { this.firstNum = value; }
    }

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public int SecondNum
    {
        get { return this.secondNum; }
        set { this.secondNum = value; }
    }

    # endregion

    # region Constructors
    public Builder(int firstnum, int secondnum)
    {
        this.FirstNum = firstnum;
        this.SecondNum = secondnum;
    }

    # endregion

    #region IBuilder Members

    public float Sum(int firstnum, int secondnum)
    {
        return firstnum + secondnum;
    }

    public float Substract(int firstnum, int secondnum)
    {
        return firstnum - secondnum;
    }

    public float Multiply(int firstnum, int secondnum)
    {
        return firstnum * secondnum;
    }

    public float Divide(int firstnum, int secondnum)
    {
        try
        {
            return firstnum / secondnum;
        }
        catch (DivideByZeroException ex)
        {
            Console.WriteLine("ZeroDivide exception : {0}", ex.Message);
            return 0;
        }
    }

    #endregion

    # region Methods

    public float GetProduct()
    {
        return this.Multiply(this.FirstNum, this.secondNum);
    }
    public override string ToString()
    {
        return string.Format("FirstNum : {0}, SecondNum : {1}", 
                                           this.FirstNum, this.SecondNum);
    }

    # endregion

}

在上面的类中,我实际上声明了一个接口 IBuilder,后来我实现了它来生成一个类 Builder。该类包含一些方法、事件、属性等,以便您了解它所具有的每个灵活性。

ILDASM1.JPG

看完代码后,我们打开 ILDASM,看看 IL 看起来像上面的图片一样。它基本上包含两种类型 .class

  1. IBuilder.class 接口
  2. Builder 实现 IBuilder.class

除此之外,您还会看到控制台应用程序 Main 方法的另一种类型,您可以暂时忽略它,因为我们将专注于 Type。

要构建动态类型,我们已经讨论过的最重要的事情是 Builder 类。BCL 公开了一些 Builder 类,使我们能够在运行时动态生成 MSIL 代码,因此您可以编译相同的代码以生成输出。

从上图中,我用红色标记了一些最重要的 Builder 类。但最终,您的应用程序需要运行指令。为了编写 IL 表达式,Reflection.Emit 提供了一个名为 ILGenerator 的类。ILGenerator(用蓝色标记)使您能够为方法或属性编写 IL。OpCodes 是确定计算机指令的操作码。因此,在编写指令时,您需要传递操作码并为方法生成指令集。

现在,继续我们的示例,让我逐一演示代码,以便您轻松构建自己的代码生成器。

为您的程序集实现 IBuilder 

让我们转到 IBuilder 接口的实际实现。根据我们的讨论,IBuilder 包含 4 个成员:Sum、Divide、Multiply & Subtract

public interface IBuilder
{
    float Sum(int firstnum, int secondnum);
    float Substract(int firstnum, int secondnum);
    float Multiply(int firstnum, int secondnum);
    float Divide(int firstnum, int secondnum);
}

所以,因为我对 IL 还不熟悉,让我们用 ILDASM 看看 IL 到底长什么样,然后我们尝试为我们的程序集实现它。

嗯...在我构建并打开 ILDASM 反汇编我的程序集后,它生成的 IL 看起来像

.class interface public abstract auto ansi EmitMethodSample.IBuilder
{
    .method public hidebysig newslot abstract virtual 
                    instance float32  Divide(int32 firstnum,
                        int32 secondnum) cil managed
    {
    }
    .method public hidebysig newslot abstract virtual 
                    instance float32  Sum(int32 firstnum,
                    int32 secondnum) cil managed
    {
    }
    .method public hidebysig newslot abstract virtual 
                    instance float32  Multiply(int32 firstnum,
                        int32 secondnum) cil managed
    {
    }
    .method public hidebysig newslot abstract virtual 
                    instance float32  Substract(int32 firstnum,
                        int32 secondnum) cil managed
    {
    }
                
} 

让我稍微解释一下 IL。

  • 在 MSIL 中,任何类型都用 .class 定义,因此我们的 Type IBuilder 会得到一个 .class。
  • interface, abstract 关键字将类型标识为抽象的,因此您不能创建该类型的对象。 
  • Auto 指定 LPSTR(字符串长指针)自动解释。
  • Ansi 指定 LPSTR(字符串长指针)解释为 ANSI。
  • IBuilder 中的方法使用 .method 关键字标识。
  • 由于是接口成员,这些方法显示为抽象虚方法。
  • instance 关键字指定对象为非静态
  • hidebysig 指定该方法可以通过名称和签名隐藏。您在 .NET 中定义的任何普通方法都具有这种灵活性。
  • NewSlot 使成员在 vtable 中获得一个槽位。(vtable 是整个对象的内存区域。所以每当创建对象时,每个对象都会创建一个 vtable,并且在该对象内部创建的任何对象都会获得一个 vtable 条目。第一个成员作为隐藏指针可以用于查找 vtable 的成员)
  • cil managed 用于确定该方法在托管环境中实现。

现在您已经了解了接口生成的 IL,是时候开始创建类型 IBuilder 了。让我们为它编写代码

private Type CreateIBuilder(ModuleBuilder mbuilder)
{

    TypeBuilder tbuilder = mbuilder.DefineType("IBuilder", TypeAttributes.Interface | 
        TypeAttributes.Public | 
        TypeAttributes.Abstract | 
        TypeAttributes.AutoClass | 
        TypeAttributes.AnsiClass);

    //Define Divide
    Type[] tparams = { typeof(System.Int32), typeof(System.Int32) };
    MethodBuilder metDivide = tbuilder.DefineMethod("Divide", MethodAttributes.Public | 
        MethodAttributes.Abstract | 
        MethodAttributes.Virtual |
        MethodAttributes.HideBySig | 
        MethodAttributes.NewSlot, 
        CallingConventions.HasThis, 
        typeof(System.Single), tparams);
    metDivide.SetImplementationFlags(MethodImplAttributes.Managed);

    MethodBuilder metSum = tbuilder.DefineMethod("Sum", MethodAttributes.Public |
        MethodAttributes.Abstract |
        MethodAttributes.Virtual |
        MethodAttributes.HideBySig |
        MethodAttributes.NewSlot,
        CallingConventions.HasThis,
        typeof(System.Single), tparams);
    metSum.SetImplementationFlags(MethodImplAttributes.Managed);

    MethodBuilder metMultiply = tbuilder.DefineMethod("Multiply", 
        MethodAttributes.Public |
        MethodAttributes.Abstract |
        MethodAttributes.Virtual |
        MethodAttributes.HideBySig |
        MethodAttributes.NewSlot,
        CallingConventions.HasThis,
        typeof(System.Single), tparams);
    metMultiply.SetImplementationFlags(MethodImplAttributes.Managed);

    MethodBuilder metSubstract = tbuilder.DefineMethod("Substract", 
        MethodAttributes.Public |
        MethodAttributes.Abstract |
        MethodAttributes.Virtual |
        MethodAttributes.HideBySig |
        MethodAttributes.NewSlot,
        CallingConventions.HasThis,
        typeof(System.Single), tparams);
    metSubstract.SetImplementationFlags(MethodImplAttributes.Managed);

    Type tIBuilder = tbuilder.CreateType();


    return tIBuilder;
}

在上面的代码中,我们首先从 ModuleBuilder.DefineType 创建 TypeBuilder。您应该注意,我以与 MSIL 相同的方式添加了 TypeAttributes。创建 TypeBuilder 后,我们接下来可以添加方法。DefineMethod 方法通过正确定义 MethodAttributes 来帮助构建方法。CallingConvensions.HasThis 将使该方法成为实例方法。我们还需要具体说明 ReturnType 和参数类型。在本例中,我将 ReturnType 指定为 System.Single(float),并将参数指定为整数。应该注意的是,我们需要使用 SetImplementationFlags 来指定方法为 cil managed。

既然我们的 IBuilder 接口已经准备就绪,是时候构建我们的实际类型了。

实现 Builder 类

嗯,现在是时候进行最后的修改了。首先,我将创建一个仅包含实现 IBuilder 接口所需方法的基本类,稍后我们将添加委托、事件、新方法、静态方法等。

要创建基本的 Builder 类,我们首先需要一个构造函数。但是,由于我们的构造函数还添加了一些行来初始化属性 FirstNum 和 SecondNum,所以我先定义它们。

1. 实现类型

在 IL 中,大多数类型都是 .class,因为整个主体都依赖于类型头,所以最好从构建类型签名开始。就 IL 而言,在 ILDASM 中它看起来像

.class public auto ansi beforefieldinit EmitMethodSample.Builder
    extends [mscorlib]System.Object
    implements EmitMethodSample.IBuilder
{
}

所以基本上这个类扩展了 System.Object (对于任何不继承自其他类的类),在我们的例子中它实现了 IBuilder

我想,使用 TypeBuilder 构建类型对您来说应该很容易,让我再为您构建一次

Type[] interfaces = { parentBuilder };
TypeBuilder tbuilder = mbuilder.DefineType("Builder", TypeAttributes.Public |
    TypeAttributes.AutoClass |
    TypeAttributes.AnsiClass |
    TypeAttributes.BeforeFieldInit,
    typeof(System.Object),
    interfaces);

所以在这里的代码中,我从 parentBuilder 实现了 Builder,它是 IBuilder 接口的 Type 对象。如果你关注代码,你会看到我为类型指定了 BeforeFieldInit,这意味着你可以在不初始化对象的情况下调用静态成员。我还根据 IL 从 System.Object 实现了 Type。

2. 实现字段和属性

现在我们的类型已经准备好了,让我们为类型添加一些字段和属性。如 Builder 类型中所示,我们有两个字段来存储数值,每个字段都使用属性包装器进行包装。让我们看看我到底在说什么

private int firstNum, secondNum;
public int FirstNum
{
    get { return this.firstNum; }
    set { this.firstNum = value; }
}

public int SecondNum
{
    get { return this.secondNum; }
    set { this.secondNum = value; }
}

所以 FirstNum 和 SecondNum 是我们需要为我们的类型声明的两个属性。现在,如果你回顾 IL 的实现,它看起来像

.field private int32 firstNum
.property instance int32 FirstNum()
{
    .set instance void EmitMethodSample.Builder::set_FirstNum(int32)
    .get instance int32 EmitMethodSample.Builder::get_FirstNum()
}
.method public hidebysig specialname instance int32 
        get_FirstNum() cil managed
{
    .custom instance void 
         [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = 
                                                             ( 01 00 00 00 ) 
    // Code size       12 (0xc)
    .maxstack  1
    .locals init (int32 V_0)
    IL_0000:  nop
    IL_0001:  ldarg.0
    IL_0002:  ldfld      int32 EmitMethodSample.Builder::firstNum
    IL_0007:  stloc.0
    IL_0008:  br.s       IL_000a
    IL_000a:  ldloc.0
    IL_000b:  ret
}
.method public hidebysig specialname instance void 
        set_FirstNum(int32 'value') cil managed
{
    // Code size       9 (0x9)
    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldarg.0
    IL_0002:  ldarg.1
    IL_0003:  stfld      int32 EmitMethodSample.Builder::firstNum
    IL_0008:  ret
}

因此,考虑到 IL,属性似乎是两个方法的包装器,一个带 get_PropertyName,另一个带 set_PropertyName,其中 get_PropertyName 返回值,set_PropertyName 设置值。所以根据 IL 定义,如果你要实现代码,它看起来像

FieldBuilder fFirst = tbuilder.DefineField("firstNum", typeof(System.Int32), 
   FieldAttributes.Private);
PropertyBuilder pFirst = tbuilder.DefineProperty("FirstNum", 
        PropertyAttributes.HasDefault, typeof(System.Int32), null);
//Getter
MethodBuilder mFirstGet = tbuilder.DefineMethod("get_FirstNum", MethodAttributes.Public | 
    MethodAttributes.SpecialName | 
    MethodAttributes.HideBySig, typeof(System.Int32), Type.EmptyTypes);
ILGenerator firstGetIL = mFirstGet.GetILGenerator();

firstGetIL.Emit(OpCodes.Ldarg_0);
firstGetIL.Emit(OpCodes.Ldfld, fFirst);
firstGetIL.Emit(OpCodes.Ret);

//Setter
MethodBuilder mFirstSet = tbuilder.DefineMethod("set_FirstNum", MethodAttributes.Public |
    MethodAttributes.SpecialName |
    MethodAttributes.HideBySig, null, new Type[] { typeof(System.Int32) });

ILGenerator firstSetIL = mFirstSet.GetILGenerator();

firstSetIL.Emit(OpCodes.Ldarg_0);
firstSetIL.Emit(OpCodes.Ldarg_1);
firstSetIL.Emit(OpCodes.Stfld, fFirst);
firstSetIL.Emit(OpCodes.Ret);

pFirst.SetGetMethod(mFirstGet);
pFirst.SetSetMethod(mFirstSet);

哦,太大了……是的,让我解释一下。首先,我添加了一个字段 firstNum,它是一个数字私有变量。FieldBuilder 可以帮助您向 IL 中添加字段。要定义一个属性,您需要首先定义属性本身,然后您必须定义两个方法,一个用于 Getter,一个用于 Setter,以便 Getter 返回 System.Int32,Setter 接受 System.Int32 作为参数。

操作码提供完整的表达式集。ldarg 加载参数,Ldfld 和 Stfld 将字段加载并设置到字段 fFirst 中。

一个值得一提的好事

所以你现在必须明白,属性实际上保留了带有 get_Property 和 set_Property 的方法,并且它们具有相同的签名。例如,你定义一个如下所示的类

private string first;
public string First { get { return this.first; } set { this.first = value; } }

public string get_First()
{
    return this.first;
}
public void set_First(string value)
{
    this.first = value;
}

该类将无法编译,因为 get_First 和 set_First 已被保留,编译器会因此抛出警告。

weird.JPG

是不是很有趣?

3. 实现构造函数

如果您的类中没有定义构造函数,C# 编译器会自动为您添加一个默认构造函数。它是如何做到的?实际上,对于任何类,System.Object 的默认构造函数都会自动继承到对象中,因此在这种情况下您无需定义默认构造函数。只有当您明确定义默认构造函数时,它才会被写入 IL 中。

在我们的例子中,我明确声明了一个带参数的构造函数,因为向你展示它的代码是很好的。

public Builder(int firstnum, int secondnum)
{
    this.FirstNum = firstnum;
    this.SecondNum = secondnum;
}

为此,让我快速向您展示构造函数在 IL 中的样子。

.method public hidebysig specialname rtspecialname 
        instance void  .ctor(int32 firstnum,
                int32 secondnum) cil managed
{
    // Code size       26 (0x1a)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  nop
    IL_0007:  nop
    IL_0008:  ldarg.0
    IL_0009:  ldarg.1
    IL_000a:  call       instance void EmitMethodSample.Builder::set_FirstNum(int32)
    IL_000f:  nop
    IL_0010:  ldarg.0
    IL_0011:  ldarg.2
    IL_0012:  call       instance void EmitMethodSample.Builder::set_SecondNum(int32)
    IL_0017:  nop
    IL_0018:  nop
    IL_0019:  ret
}

要构建一个构造函数,我们需要创建 ConstructorBuilder 对象,你可以从 TypeBuilder 的 DefineConstructor 方法中获取。如果你查看 IL,你会发现 IL 实际上首先调用了 System.Object 的构造函数。这是必需的,因为任何对象都内部继承自基 System.Object。

setFirstNum 和 set_SecondNum 从 IL 调用,用于设置类的 FirstNum 和 SecondNum 的值。

Type[] parameters = { typeof(System.Int32), typeof(System.Int32) };
ConstructorBuilder cBuilder = tbuilder.DefineConstructor(MethodAttributes.Public | 
    MethodAttributes.HideBySig | 
    MethodAttributes.SpecialName | 
    MethodAttributes.RTSpecialName, 
    CallingConventions.Standard, 
    parameters);

ConstructorInfo conObj = typeof(object).GetConstructor(new Type[0]);

ILGenerator cil = cBuilder.GetILGenerator();
cil.Emit(OpCodes.Ldarg_0);
cil.Emit(OpCodes.Call, conObj);
cil.Emit(OpCodes.Nop);
cil.Emit(OpCodes.Nop);
cil.Emit(OpCodes.Ldarg_0);
cil.Emit(OpCodes.Ldarg_1);
cil.Emit(OpCodes.Call, mFirstSet);
cil.Emit(OpCodes.Nop);
cil.Emit(OpCodes.Ldarg_0);
cil.Emit(OpCodes.Ldarg_1);
cil.Emit(OpCodes.Call, mSecondSet);
cil.Emit(OpCodes.Nop);
cil.Emit(OpCodes.Nop);
cil.Emit(OpCodes.Ret);

Constructor 的 MethodAttribute 中的 SpecialName 使该方法对 CLR 特殊。因此,方法名称 ctor 使其成为类的构造函数。

要调用 System.Object 的构造函数,我们需要获取该对象的构造函数。我使用 Reflection 从 Type 获取 ConstructorInfo 并将其传递给 Call OpCode。我们按照 IL 中指定的方式发出代码,然后将生成构造函数。

一件值得记住的有趣事情

关于 Reflection.Emit,有一件有趣的事情需要记住,它在内部向它调用的每个方法发送一个隐藏对象。这就是我们在 C# 中标识为“this”或在 Vb 中标识为“Me”的隐式对象调用。因此,当我们为 OpCodes 调用 Ldarg_0 时,我们实际上指的是作为第一个参数传递给构造函数的隐式对象。因此,我们指定的任何参数都从索引 1 开始。

构造函数与普通方法唯一的区别在于,构造函数不返回值。在 CLR 中,如果收到 OpCodes.Ret,方法会立即返回堆栈中的顶部元素。因此,如果您的堆栈在调用 Ret 之前加载了一个值到堆栈中,当您创建该类型的对象时,您将收到“Invalid Program”异常。因此,在这种情况下,在调用 Ret 之前应调用 Nop 以消耗一个处理周期。

现在我们已经定义了构造函数,让我继续定义方法。

4. 实现 IBuilder 中的方法

现在我们的构造函数已经准备好,是时候实现 IBuilder 对象并为我们定义方法了。随着我们深入代码,我认为您应该清楚如何构建自己的自定义对象。让我们尝试 IBuilder 的 Divide 方法并为我们实现它。

我们之前声明的 Divide 方法看起来像

public float Divide(int firstnum, int secondnum)
{
    try
    {
        return firstnum / secondnum;
    }
    catch (DivideByZeroException ex)
    {
        Console.WriteLine("ZeroDivide exception : {0}", ex.Message);
        return 0;
    }
}

因此,从这个方法中,您可以理解如何从对象中调用外部成员函数,就像我在这里使用 Console.WriteLine 所做的那样,并且还可以了解如何在代码生成过程中使用 try/Catch 块。所以,废话不多说,让我们再次打开 ILDASM,看看代码有什么不同

.method public hidebysig newslot virtual final 
            instance float32  Divide(int32 firstnum,
                        int32 secondnum) cil managed
    {
        // Code size       39 (0x27)
        .maxstack  2
        .locals init (class [mscorlib]System.DivideByZeroException V_0,
                float32 V_1)
        IL_0000:  nop
        .try
        {
        IL_0001:  nop
        IL_0002:  ldarg.1
        IL_0003:  ldarg.2
        IL_0004:  div
        IL_0005:  conv.r4
        IL_0006:  stloc.1
        IL_0007:  leave.s    IL_0024
        }  // end .try
        catch [mscorlib]System.DivideByZeroException 
        {
        IL_0009:  stloc.0
        IL_000a:  nop
        IL_000b:  ldstr      "ZeroDivide exception : {0}"
        IL_0010:  ldloc.0
        IL_0011:  callvirt   instance string [mscorlib]System.Exception::get_Message()
        IL_0016:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                        object)
        IL_001b:  nop
        IL_001c:  ldc.r4     0.0
        IL_0021:  stloc.1
        IL_0022:  leave.s    IL_0024
        }  // end handler
        IL_0024:  nop
        IL_0025:  ldloc.1
        IL_0026:  ret
    }

现在,实现表明该方法加载第一个和第二个参数,并使用 Div 操作来除法这些值,Conv.r4 实际上将结果转换为 float32 值。这里使用了声明为 float32 的局部堆栈元素,并且转换后的结果使用 stloc.1 再次推回堆栈。如果一切正常,应用程序将其控制权传递给 IL_0024,导致该方法返回第一个位置的局部堆栈值。

所以让我们使用 Builder 对象来实现同样的功能

MethodBuilder mDivide = tbuilder.DefineMethod("Divide", MethodAttributes.Public |
    MethodAttributes.HideBySig |
    MethodAttributes.NewSlot |
    MethodAttributes.Virtual |
    MethodAttributes.Final,
    CallingConventions.Standard,
    typeof(System.Single),
    new Type[] { typeof(System.Int32), typeof(System.Int32) });
mDivide.SetImplementationFlags(MethodImplAttributes.Managed);
ILGenerator dil = mDivide.GetILGenerator();

dil.Emit(OpCodes.Nop);
Label lblTry = dil.BeginExceptionBlock();

dil.Emit(OpCodes.Nop);
dil.Emit(OpCodes.Ldarg_1);
dil.Emit(OpCodes.Ldarg_2);
dil.Emit(OpCodes.Div);
dil.Emit(OpCodes.Conv_R4); // Converts to Float32
dil.Emit(OpCodes.Stloc_1);
dil.Emit(OpCodes.Leave, lblTry);

dil.BeginCatchBlock(typeof(DivideByZeroException));
dil.Emit(OpCodes.Stloc_0);
dil.Emit(OpCodes.Nop);
dil.Emit(OpCodes.Ldstr, "ZeroDivide exception : {0}");
dil.Emit(OpCodes.Ldloc_0);
MethodInfo minfo = typeof(DivideByZeroException).GetMethod("get_Message");
dil.Emit(OpCodes.Callvirt, minfo);
MethodInfo wl = typeof(System.Console).GetMethod("WriteLine", new Type[] 
                                      { typeof(string), typeof(object) });
dil.Emit(OpCodes.Call, wl);
dil.Emit(OpCodes.Nop);
dil.Emit(OpCodes.Ldc_R4, 0.0);
dil.Emit(OpCodes.Stloc_1);
dil.Emit(OpCodes.Leave_S, lblTry);

dil.EndExceptionBlock();
dil.Emit(OpCodes.Nop);
dil.Emit(OpCodes.Ldloc_1);
dil.Emit(OpCodes.Ret);

要在 IL 中打开一个 Try 块,您需要使用 BeginExceptionBlock。务必存储标签,以便在需要时可以跳转到特定的 IL 指令代码。现在,在开始使用 BeginCatchBlock 通知 Catch 块之前,我们需要使用 OpCodes.Leave 和 LabelName 离开 Try 块。这将确保应用程序保持作用域。

您可以看到,我们可以有多个 catch 块,并且每个 catch 块都将通过传递给 BeginCatchBlock 的类型来标识。因此,我们只需将字符串加载到 Catch 块中,并调用 Console 的 WriteLine 方法来显示字符串。最后,在调用 EndExceptionBlock 之前,我们再次离开 Try/catch 块。

您应该注意,每当您要调用方法时,都需要使用 MethodInfo 对象。

构建委托

由于为你的类型创建其他方法很简单,所以让我们进一步为你创建一个委托。向你展示如何为类构建委托是个好主意。构建委托与构建其他成员不同。假设我们需要为我们的类型声明一个委托,如下所示

public delegate void BuilderDelegate(string message);

现在看完这一行代码,不要以为 IL 的声明会像这样简单。IL 看起来像

.class auto ansi sealed nested public BuilderDelegate
    extends [mscorlib]System.MulticastDelegate
{
    .method public hidebysig specialname rtspecialname 
        instance void  .ctor(object 'object',
                        native int 'method') runtime managed
    {
    }
    .method public hidebysig newslot virtual 
        instance class [mscorlib]System.IAsyncResult 
        BeginInvoke(string message,
            class [mscorlib]System.AsyncCallback callback,
            object 'object') runtime managed
    {
    }
    .method public hidebysig newslot virtual 
        instance void  EndInvoke(class [mscorlib]System.IAsyncResult result) 
                                                               runtime managed
    {
    }
    .method public hidebysig newslot virtual 
        instance void  Invoke(string message) runtime managed
    {
    }
}

天哪,委托实际上被定义为实际类型 Builder 内部的一个嵌套类型 (.class),它继承自 System.MulticastDelegate。它还在新的类型声明内部声明了 Invoke、BeginInvoke 和 EndInvoke 等方法。所以让我们构建这个类型来帮助你,尽管我认为你现在可以自己创建它了。

TypeBuilder tdelegate = tbuilder.DefineNestedType("", TypeAttributes.AutoClass | 
    TypeAttributes.AnsiClass | 
    TypeAttributes.Sealed | 
    TypeAttributes.Public, typeof(System.MulticastDelegate));

MethodBuilder methodBeginInvoke = tdelegate.DefineMethod("BeginInvoke",
    MethodAttributes.Public |
    MethodAttributes.HideBySig |
    MethodAttributes.NewSlot |
    MethodAttributes.Virtual,
    typeof(IAsyncResult), new Type[] { typeof(string), typeof(AsyncCallback), 
      typeof(object) });
methodBeginInvoke.SetImplementationFlags(MethodImplAttributes.Runtime | 
MethodImplAttributes.Managed);

MethodBuilder methodEndInvoke = tdelegate.DefineMethod("EndInvoke", 
 MethodAttributes.Public | 
    MethodAttributes.HideBySig | 
    MethodAttributes.NewSlot | 
    MethodAttributes.Virtual,null, new Type[] { typeof(IAsyncResult)});
methodEndInvoke.SetImplementationFlags(MethodImplAttributes.Runtime | 
 MethodImplAttributes.Managed);

MethodBuilder methodInvoke = tdelegate.DefineMethod("Invoke", MethodAttributes.Public |
    MethodAttributes.HideBySig |
    MethodAttributes.NewSlot | MethodAttributes.Virtual, CallingConventions.Standard, 
                   null, new Type[] { typeof(string) });

接下来,我建议您尝试其他方法,您可以使用 EventBuilder 类来构建事件,使用 CustomAttributeBuilder 来构建您自己的用户定义自定义属性等。

总结

现在我们已经实现了所有方法,让我检查一下我是否正确生成了 IL。

//Step 1 : Create the Assembly
AssemblyBuilder asmBuilder = this.GetAssemblyBuilder("MyBuilder");

//Step 2: Add A Module to the Assembly
ModuleBuilder mbuilder = this.GetModule(asmBuilder);

//Step 3: Add the Type IBuilder
Type iBuilder = this.CreateIBuilder(mbuilder);

//Step 4 : Implement IBuilder to create Builder 
Type Builder = this.CreateBuilderImpl(mbuilder, iBuilder);

dynamic variable = Activator.CreateInstance(Builder, new object[] { 20, 10 });
float result = variable.Sum(30, 40);
Console.WriteLine("Result  for Sum(30, 40) : {0}", result);
result = variable.Substract(50, 25);
Console.WriteLine("Result  for Substract(50, 25) : {0}", result);
result = variable.Multiply(3, 5);
Console.WriteLine("Result  for Multiply(3, 5) : {0}", result);
result = variable.Divide(30, 5);
Console.WriteLine("Result  for Divide(30, 5) : {0}", result);

您应该注意,我使用 dynamic 来避免再次不必要地使用 Reflection 类。

所以它看起来不错,编译后我得到了这样的输出

ilcode.JPG

因此,生成的 IL 对我们来说效果很好。您还可以使用以下命令将 IL 保存为程序集

asmBuilder.Save("MyBuilder.dll");

处理无效 IL

在构建 IL 时,您可能会经常遇到 IL 无法编译的情况。

invalidprogram.JPG

别担心,.NET 附带了一个免费工具,随 Visual Studio 一起安装,可以帮助您摆脱这些困境。常见的异常类型是“通用语言运行时检测到无效程序”。该工具名为 PeVerify.exe。

安装 Visual Studio 后,您可以打开控制台并尝试调用以下语句

peverify.exe <assemblypath>\yourassembly.dll 再次编译程序集后,它将给出在构建 Type 时发生的实际异常。您可以从以下位置阅读有关 PEVerify 的更多信息:MSDN PEVerify 参考 [^]

参考文献

互联网上有很多关于这个主题的参考资料,可能会对您有所帮助。让我们为您列举一些 

历史

初始发布 - 2010年10月26日

结论

为您创作这篇文章很有趣。我也很兴奋能为您写一篇这样的文章。对我来说这一切都是新的,但我尽力写得尽可能多。我希望您会喜欢我的帖子。

如果你认为我犯了任何错误,请告诉我,因为我在这方面并非大师,这样我们就可以使这篇文章更好。感谢您阅读这篇文章。

© . All rights reserved.