C#反射与特性(十):EMIT构建代码

简介: C#反射与特性(十):EMIT构建代码

前面,本系列一共写了 九 篇关于反射和特性相关的文章,讲解了如何从程序集中通过反射将信息解析出来,以及实例化类型。

前面的九篇文章中,重点在于读数据,使用已经构建好的数据结构(元数据等),接下来,我们将学习 .NET Core 中,关于动态构建代码的知识。


其中表达式树已经在另一个系列写了,所以本系列主要是讲述 反射,Emit ,AOP 等内容。

如果现在总结一下,反射,与哪些数据结构相关?


我们可以从 AttributeTargets 枚举中窥见:

public enum AttributeTargets
{
   All=16383,
   Assembly=1,
   Module=2,
   Class=4,
   Struct=8,
   Enum=16,
   Constructor=32,
   Method=64,
   Property=128,
   Field=256,
   Event=512,
   Interface=1024,
   Parameter=2048,
   Delegate=4096,
   ReturnValue=8192
}


分别是程序集、模块、类、结构体、枚举、构造函数、方法、属性、字段、事件、接口、参数、委托、返回值。

以往的文章中,已经对这些进行了很详细的讲解,我们可以中反射中获得各种各样的信息。当然,我们也可以通过动态代码,生成以上数据结构。


动态代码的其中一种方式是表达式树,我们还可以使用 Emit 技术、Roslyn 技术来编写;相关的框架有 Natasha、CS-Script 等。


构建代码


首先我们引入一个命名空间:

using System.Reflection.Emit;

Emit 命名空间中里面有很多用于构建动态代码的类型,例如 AssemblyBuilder,这个类型用于构建程序集。类推,构建其它数据结构例如方法属性,则有

MethodBuilderPropertyBuilder


1,程序集(Assembly)


AssemblyBuilder 类型定义并表示动态程序集,它是一个密封类,其定义如下:

public sealed class AssemblyBuilder : Assembly


AssemblyBuilderAccess 定义动态程序集的访问模式,在 .NET Core 中,只有两个枚举:

枚举 说明
Run 1 可以执行但无法保存该动态程序集。
RunAndCollect 9 当动态程序集不再可供访问时,将自动卸载该程序集,并回收其内存。


.NET Framework 中,有 RunAndSave 、Save 等枚举,可用于保存构建的程序集,但是在 .NET Core 中,是没有这些枚举的,也就是说,Emit 构建的程序集只能在内存中,是无法保存成 .dll 文件的。


另外,程序集的构建方式(API)也做了变更,如果你百度看到文章 AppDomain.CurrentDomain.DefineDynamicAssembly,那么你可以关闭创建了,说明里面的很多代码根本无法在 .NET Core 下跑。


好了,不再赘述,我们来看看创建一个程序集的代码:

AssemblyName assemblyName = new AssemblyName("MyTest");
            AssemblyBuilder assBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);


构建程序集,分为两部分:

  • AssemblyName 完整描述程序集的唯一标识。
  • AssemblyBuilder 构建程序集


一个完整的程序集,有很多信息的,版本、作者、构建时间、Token 等,这些可以使用

AssemblyName 来设置。


一般一个程序集需要包含以下内容:

  • 简单名称。
  • 版本号。
  • 加密密钥对。
  • 支持的区域性。


你可以参考以下示例:

AssemblyName assemblyName = new AssemblyName("MyTest");
            assemblyName.Name = "MyTest";   // 构造函数中已经设置,此处可以忽略
            // Version 表示程序集、操作系统或公共语言运行时的版本号.
            // 构造函数比较多,可以选用 主版本号、次版本号、内部版本号和修订号
            // 请参考 https://docs.microsoft.com/zh-cn/dotnet/api/system.version?view=netcore-3.1
            assemblyName.Version = new Version("1.0.0");
            assemblyName.CultureName = CultureInfo.CurrentCulture.Name; // = "zh-CN" 
            assemblyName.SetPublicKeyToken(new Guid().ToByteArray());


最终程序集的 AssemblyName 显示名称是以下格式的字符串:

Name <,Culture = CultureInfo> <,Version = Major.Minor.Build.Revision> <, StrongName> <,PublicKeyToken> '\0'


例如:

ExampleAssembly, Version=1.0.0.0, Culture=en, PublicKeyToken=a5d015c7d5a0b012


另外,创建程序集构建器使用 AssemblyBuilder.DefineDynamicAssembly() 而不是 new AssemblyBuilder()


2,模块(Module)


程序集和模块之间的区别可以参考

https://stackoverflow.com/questions/9271805/net-module-vs-assembly

https://stackoverflow.com/questions/645728/what-is-a-module-in-net


模块是程序集内代码的逻辑集合,每个模块可以使用不同的语言编写,大多数情况下,一个程序集包含一个模块。程序集包括了代码、版本信息、元数据等。


MSDN指出:“模块是没有 Assembly 清单的 Microsoft 中间语言(MSIL)文件。”。

这些就不再扯淡了。

创建完程序集后,我们继续来创建模块。


AssemblyName assemblyName = new AssemblyName("MyTest");
            AssemblyBuilder assBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
            ModuleBuilder moduleBuilder = assBuilder.DefineDynamicModule("MyTest");             // ⬅


3,类型(Type)


目前步骤:

Assembly -> Module -> Type 或 Enum


ModuleBuilder 中有个 DefineType 方法用于创建 classstructDefineEnum方法用于创建 enum

这里我们分别说明。


创建类或结构体:

TypeBuilder typeBuilder = moduleBuilder.DefineType("MyTest.MyClass",TypeAttributes.Public);


定义的时候,注意名称是完整的路径名称,即命名空间+类型名称。


我们可以先通过反射,获取已经构建的代码信息:

Console.WriteLine($"程序集信息:{type.Assembly.FullName}");
            Console.WriteLine($"命名空间:{type.Namespace} , 类型:{type.Name}");


结果:

程序集信息:MyTest, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
命名空间:MyTest , 类型:MyClass


接下来将创建一个枚举类型,并且生成枚举。

我们要创建一个这样的枚举:

namespace MyTest
{
    public enum MyEnum
    {
        Top = 1,
        Bottom = 2,
        Left = 4,
        Right = 8,
        All = 16
    }
}


使用 Emit 的创建过程如下:

EnumBuilder enumBuilder = moduleBuilder.DefineEnum("MyTest.MyEnum", TypeAttributes.Public, typeof(int));


TypeAttributes 有很多枚举,这里只需要知道声明这个枚举类型为 公开的(Public);typeof(int) 是设置枚举数值基础类型。

然后 EnumBuilder 使用 DefineLiteral 方法来创建枚举。


方法 说明
DefineLiteral(String, Object) 在枚举类型中使用指定的常量值定义命名的静态字段。


代码如下:

enumBuilder.DefineLiteral("Top", 0);
            enumBuilder.DefineLiteral("Bottom", 1);
            enumBuilder.DefineLiteral("Left", 2);
            enumBuilder.DefineLiteral("Right", 4);
            enumBuilder.DefineLiteral("All", 8);


我们可以使用反射将创建的枚举打印出来:

public static void WriteEnum(TypeInfo info)
        {
            var myEnum = Activator.CreateInstance(info);
            Console.WriteLine($"{(info.IsPublic ? "public" : "private")} {(info.IsEnum ? "enum" : "class")} {info.Name}");
            Console.WriteLine("{");
            var names = Enum.GetNames(info);
            int[] values = (int[])Enum.GetValues(info);
            int i = 0;
            foreach (var item in names)
            {
                Console.WriteLine($" {item} = {values[i]}");
                i++;
            }
            Console.WriteLine("}");
        }


Main 方法中调用:

WriteEnum(enumBuilder.CreateTypeInfo());

接下来,类型创建成员,就复杂得多了。


4,DynamicMethod 定义方法与添加 IL


下面我们来为 类型创建一个方法,并通过 Emit 向程序集中动态添加 IL。这里并不是使用 MethodBuider,而是使用 DynamicMethod。


在开始之前,请自行安装反编译工具 dnSpy 或者其它工具,因为这里涉及到 IL 代码。

这里我们先忽略前面编写的代码,清空 Main 方法。


我们创建一个类型:

public class MyClass{}


这个类型什么都没有。

然后使用 Emit 动态创建一个 方法,并且附加到 MyClass 类型中:

// 动态创建一个方法并且附加到 MyClass 类型中
            DynamicMethod dyn = new DynamicMethod("Foo",null,null,typeof(MyClass));
            ILGenerator iLGenerator = dyn.GetILGenerator();
            iLGenerator.EmitWriteLine("HelloWorld");
            iLGenerator.Emit(OpCodes.Ret);
            dyn.Invoke(null,null);


运行后会打印字符串。

DynamicMethod 类型用于构建方法,定义并表示可以编译、执行和丢弃的一种动态方法。 丢弃的方法可用于垃圾回收。。


ILGenerator 是 IL 代码生成器。

EmitWriteLine 作用是打印字符串,

OpCodes.Ret 标记 结束方法的执行,

Invoke 将方法转为委托执行。

上面的示例比较简单,请认真记一下。


下面,我们要使用 Emit 生成一个这样的方法:

public int Add(int a,int b)
        {
            return a + b;
        }


看起来很简单的代码,要用 IL 来写,就变得复杂了。

ILGenerator 正是使用 C# 代码的形式去写 IL,但是所有过程都必须按照 IL 的步骤去写。


其中最重要的,便是 OpCodes 枚举了,OpCodes 有几十个枚举,代表了 IL 的所有操作功能。


请参考:https://docs.microsoft.com/zh-cn/dotnet/api/system.reflection.emit.opcodes?view=netcore-3.1


如果你点击上面的链接查看 OpCodes 的枚举,你可以看到,很多 功能码,这么多功能码是记不住的。我们现在刚开始学习 Emit,这样就会难上加难。

所以,我们要先下载能够查看 IL 代码的工具,方便我们探索和调整写法。


我们看看此方法生成的 IL 代码:

.method public hidebysig instance int32
    Add(
      int32 a,
      int32 b
    ) cil managed
  {
    .maxstack 2
    .locals init (
      [0] int32 V_0
    )
    // [14 9 - 14 10]
    IL_0000: nop
    // [15 13 - 15 26]
    IL_0001: ldarg.1      // a
    IL_0002: ldarg.2      // b
    IL_0003: add
    IL_0004: stloc.0      // V_0
    IL_0005: br.s         IL_0007
    // [16 9 - 16 10]
    IL_0007: ldloc.0      // V_0
    IL_0008: ret
  } // end of method MyClass::Add


看不懂完全没关系,因为笔者也看不懂。

目前我们已经获得了上面两大部分的信息,接下来我们使用 DynamicMethod 来动态编写方法。


定义 Add 方法并获取 IL 生成工具:

DynamicMethod dynamicMethod = new DynamicMethod("Add",typeof(int),new Type[] { typeof(int),typeof(int)});
            ILGenerator ilCode = dynamicMethod.GetILGenerator();


DynamicMethod 用于定义一个方法;ILGenerator是 IL 生成器。当然也可以将此方法附加到一个类型中,完整代码示例如下:

// typeof(Program),表示将此动态编写的方法附加到 MyClass 中
            DynamicMethod dynamicMethod = new DynamicMethod("Add", typeof(int), new Type[] { typeof(int), typeof(int) },typeof(MyClass));
            ILGenerator ilCode = dynamicMethod.GetILGenerator();
            ilCode.Emit(OpCodes.Ldarg_0); // a,将索引为 0 的自变量加载到计算堆栈上。
            ilCode.Emit(OpCodes.Ldarg_1); // b,将索引为 1 的自变量加载到计算堆栈上。
            ilCode.Emit(OpCodes.Add);     // 将两个值相加并将结果推送到计算堆栈上。
            // 下面指令不需要,默认就是弹出计算堆栈的结果
            //ilCode.Emit(OpCodes.Stloc_0); // 将索引 0 处的局部变量加载到计算堆栈上。
            //ilCode.Emit(OpCodes.Br_S);    // 无条件地将控制转移到目标指令(短格式)。
            //ilCode.Emit(OpCodes.Ldloc_0); // 将索引 0 处的局部变量加载到计算堆栈上。
            ilCode.Emit(OpCodes.Ret);     // 即 return,从当前方法返回,并将返回值(如果存在)从被调用方的计算堆栈推送到调用方的计算堆栈上。
            // 方法1
            Func<int, int, int> test = (Func<int, int, int>)dynamicMethod.CreateDelegate(typeof(Func<int, int, int>));
            Console.WriteLine(test(1, 2));
            // 方法2
            int sum = (int)dynamicMethod.Invoke(null, BindingFlags.Public, null, new object[] { 1, 2 }, CultureInfo.CurrentCulture);
            Console.WriteLine(sum);


实际以上代码与我们反编译出来的 IL 编写有所差异,具体俺也不知道为啥,在群里问了调试了,注释掉那么几行代码,才通过的。

相关文章
|
15天前
|
编译器 C# Android开发
震惊!Uno Platform 与 C# 最新特性的完美融合,你不可不知的跨平台开发秘籍!
Uno Platform 是一个强大的跨平台应用开发框架,支持 Windows、macOS、iOS、Android 和 WebAssembly,采用 C# 和 XAML 进行编程。C# 作为其核心语言,持续推出新特性,如可空引用类型、异步流、记录类型和顶级语句等,极大地提升了开发效率。要在 Uno Platform 中使用最新 C# 特性,需确保开发环境支持相应版本,并正确配置编译器选项。通过示例展示了如何在 Uno Platform 中应用可空引用类型、异步流、记录类型及顶级语句等功能,帮助开发者更好地构建高效、优质的跨平台应用。
140 59
|
15天前
|
编译器 C# Android开发
Uno Platform 是一个用于构建跨平台应用程序的强大框架,它允许开发者使用 C# 和 XAML 来创建适用于多个平台的应用
Uno Platform 是一个用于构建跨平台应用程序的强大框架,它允许开发者使用 C# 和 XAML 来创建适用于多个平台的应用
50 8
|
2月前
|
开发框架 .NET 编译器
总结一下 C# 如何自定义特性 Attribute 并进行应用
总结一下 C# 如何自定义特性 Attribute 并进行应用
|
2月前
|
Android开发 iOS开发 C#
Xamarin:用C#打造跨平台移动应用的终极利器——从零开始构建你的第一个iOS与Android通用App,体验前所未有的高效与便捷开发之旅
【8月更文挑战第31天】Xamarin 是一个强大的框架,允许开发者使用单一的 C# 代码库构建高性能的原生移动应用,支持 iOS、Android 和 Windows 平台。作为微软的一部分,Xamarin 充分利用了 .NET 框架的强大功能,提供了丰富的 API 和工具集,简化了跨平台移动应用开发。本文通过一个简单的示例应用介绍了如何使用 Xamarin.Forms 快速创建跨平台应用,包括设置开发环境、定义用户界面和实现按钮点击事件处理逻辑。这个示例展示了 Xamarin.Forms 的基本功能,帮助开发者提高开发效率并实现一致的用户体验。
78 0
|
2月前
|
Android开发 iOS开发 C#
Xamarin.Forms:从零开始的快速入门指南——打造你的首个跨平台移动应用,轻松学会用C#和XAML构建iOS与Android通用界面的每一个步骤
【8月更文挑战第31天】Xamarin.Forms 是一个强大的框架,让开发者通过单一共享代码库构建跨平台移动应用,支持 iOS、Android 和 Windows。使用 C# 和 XAML,它简化了多平台开发流程并保持一致的用户体验。本指南通过创建一个简单的 “HelloXamarin” 应用演示了 Xamarin.Forms 的基本功能和工作原理。
44 0
|
2月前
|
数据安全/隐私保护 C# UED
利用 Xamarin 开展企业级移动应用开发:从用户登录到客户管理,全面演示C#与Xamarin.Forms构建跨平台CRM应用的实战技巧与代码示例
【8月更文挑战第31天】利用 Xamarin 进行企业级移动应用开发能显著提升效率并确保高质量和高性能。Xamarin 的跨平台特性使得开发者可以通过单一的 C# 代码库构建 iOS、Android 和 Windows 应用,帮助企业快速推出产品并保持一致的用户体验。本文通过一个简单的 CRM 示例应用演示 Xamarin 的使用方法,并提供了具体的代码示例。该应用包括用户登录、客户列表显示和添加新客户等功能。此外,还介绍了如何增强应用的安全性、数据持久化、性能优化及可扩展性,从而构建出功能全面且体验良好的移动应用。
35 0
|
2月前
|
前端开发 开发者 C#
深度解析 Uno Platform 中的 MVVM 模式:从理论到实践的全方位指南,助你轻松掌握通过 C# 与 XAML 构建高效可维护的跨平台应用秘籍
【8月更文挑战第31天】本文详细介绍如何在优秀的跨平台 UI 框架 Uno Platform 中实施 MVVM(Model-View-ViewModel)模式,通过一个简单的待办事项列表应用演示其实现过程。MVVM 模式有助于分离视图层与业务逻辑层,提升代码组织性、易测性和可维护性。Uno Platform 的数据绑定机制使视图与模型间的同步变得高效简便。文章通过构造 `TodoListViewModel` 类及其相关视图,展示了如何解耦视图与模型,实现动态数据绑定及命令处理,从而提高代码质量和开发效率。通过这一模式,开发者能更轻松地构建复杂的跨平台应用。
28 0
|
2月前
|
前端开发 开发者 Apache
揭秘Apache Wicket项目结构:如何打造Web应用的钢铁长城,告别混乱代码!
【8月更文挑战第31天】Apache Wicket凭借其组件化设计深受Java Web开发者青睐。本文详细解析了Wicket项目结构,帮助你构建可维护的大型Web应用。通过示例展示了如何使用Maven管理依赖,并组织页面、组件及业务逻辑,确保代码清晰易懂。Wicket提供的页面继承、组件重用等功能进一步增强了项目的可维护性和扩展性。掌握这些技巧,能够显著提升开发效率,构建更稳定的Web应用。
76 0
|
2月前
|
C# 索引
C#各大版本特性
C#各大版本特性
60 0
|
2月前
|
物联网 C# Windows
看看如何使用 C# 代码让 MQTT 进行完美通信
看看如何使用 C# 代码让 MQTT 进行完美通信
335 0