C# Reflection and Attributes (Part 10): Reflection to Build Code

2020年6月11日 86点热度 2人点赞 0条评论
内容目录

Previously, this series has written a total of nine articles related to reflection and attributes, explaining how to parse information from assemblies through reflection and instantiate types.

In the previous nine articles, the focus was on reading data using already constructed data structures (such as metadata). Next, we will learn about dynamic code construction in .NET Core.

The expression trees have already been covered in another series, so this series mainly discusses reflection, Emit, AOP, and related content.

If we summarize, what data structures are related to reflection?

We can glimpse from the AttributeTargets enumeration:

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
}

These include assemblies, modules, classes, structs, enums, constructors, methods, properties, fields, events, interfaces, parameters, delegates, and return values.

In previous articles, these have been explained in detail, and we can obtain various information from reflection. Of course, we can also generate the above data structures using dynamic code.

One way of dynamic code is through expression trees. We can also use Emit technology, Roslyn technology to write; related frameworks include Natasha, CS-Script, etc.

Building Code

First, we introduce a namespace:

using System.Reflection.Emit;

The Emit namespace contains many types for building dynamic code, such as AssemblyBuilder, which is used to construct assemblies. Similarly, to build other data structures such as method attributes, there are MethodBuilder and PropertyBuilder.

1. Assembly

The AssemblyBuilder type defines and represents a dynamic assembly. It is a sealed class defined as follows:

public sealed class AssemblyBuilder : Assembly

AssemblyBuilderAccess defines the access mode for dynamic assemblies. In .NET Core, there are only two enumerations:

Enumeration Value Description
Run 1 The dynamic assembly can be executed but cannot be saved.
RunAndCollect 9 The assembly will be automatically unloaded and its memory reclaimed when no longer accessible.

In .NET Framework, there are enumerations like RunAndSave, Save, which can be used to save the constructed assembly. However, in .NET Core, these enumerations do not exist, meaning that assemblies built using Emit can only reside in memory and cannot be saved as .dll files.

Additionally, the ways to construct assemblies (APIs) have changed. If you find articles mentioning AppDomain.CurrentDomain.DefineDynamicAssembly, you can disregard it as much of the code within cannot run under .NET Core.

Now, without further ado, let’s look at the code to create an assembly:

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

Create an assembly consists of two parts:

  • AssemblyName fully describes the unique identifier of the assembly.
  • AssemblyBuilder constructs the assembly.

A complete assembly contains a lot of information such as version, author, build time, token, etc., which can be set using

AssemblyName.

Typically, an assembly should include the following contents:

  • Simple name.
  • Version number.
  • Cryptographic key pair.
  • Supported cultures.

You can refer to the following example:

            AssemblyName assemblyName = new AssemblyName("MyTest");
            assemblyName.Name = "MyTest";   // Already set in the constructor; can be ignored here
        // Version indicates the version number of the assembly, operating system or common language runtime.
        // There are several constructors available to choose from like Major version, Minor version, Build number and Revision number.
        // Please refer to 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());</code></pre>

The final display name of the assembly's AssemblyName is a string in the following format:

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

For example:

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

Additionally, the assembly builder is created using AssemblyBuilder.DefineDynamicAssembly() instead of new AssemblyBuilder().

2. Module

The difference between an assembly and a module can refer to the following:

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

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

A module is a logical collection of code within an assembly. Each module can be written in different languages, and in most cases, an assembly contains one module. An assembly includes code, version information, metadata, etc.

MSDN states: “A module is a Microsoft Intermediate Language (MSIL) file without an assembly manifest.”

Let’s move on to creating a module after creating the assembly.

            AssemblyName assemblyName = new AssemblyName("MyTest");
            AssemblyBuilder assBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
        ModuleBuilder moduleBuilder = assBuilder.DefineDynamicModule(&quot;MyTest&quot;);             // ⬅</code></pre>

3. Type

Current steps:

Assembly -> Module -> Type or Enum

The ModuleBuilder has a DefineType method that is used to create a class and struct; the DefineEnum method is used to create an enum.

Let’s explain these separately.

Creating a class or struct:

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

When defining, note that the name is the full path name, i.e., namespace + type name.

We can first use reflection to obtain the information of the constructed code:

            Console.WriteLine($"Assembly Info: {type.Assembly.FullName}");
            Console.WriteLine($"Namespace: {type.Namespace} , Type: {type.Name}");

Result:

Assembly Info: MyTest, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
Namespace: MyTest , Type: MyClass

Next, we will create an enum type and generate it.

We want to create an enum like this:

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

The process of creating it using Emit is as follows:

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

TypeAttributes has many enumerations, here we only need to know it declares this enum type as public; typeof(int) sets the base type for the enum value.

Then, the EnumBuilder uses the DefineLiteral method to create the enum.

Method Description
DefineLiteral(String, Object) Defines a named static field using the specified constant value in the enum type.

The code is as follows:

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

We can use reflection to print out the created enum:

        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("}");
        }

Call in the Main method:

 WriteEnum(enumBuilder.CreateTypeInfo());

Next, creating members of the type becomes much more complex.

4. DynamicMethod Defining Methods and Adding IL

Next, we will create a method for the type and dynamically add IL to it using Emit. Here, we will not use MethodBuilder, but instead use DynamicMethod.

Before starting, please install a decompilation tool like dnSpy or others, as we will be dealing with IL code.

Let’s disregard the prior code and clear the Main method.

We create a type:

    public class MyClass{}

This type has nothing in it.

Then we dynamically create a method and attach it to the MyClass type using Emit:

            // Dynamically create a method and attach it to MyClass type
            DynamicMethod dyn = new DynamicMethod("Foo",null,null,typeof(MyClass));
            ILGenerator iLGenerator = dyn.GetILGenerator();
        iLGenerator.EmitWriteLine(&quot;HelloWorld&quot;);
        iLGenerator.Emit(OpCodes.Ret);

        dyn.Invoke(null,null);</code></pre>

Running this will print the string.

The DynamicMethod type is used to construct methods and defines and represents a dynamically compiled, executable, and disposable method. Discarded methods can be collected by garbage collection.

ILGenerator is the IL code generator.

EmitWriteLine writes the string,

OpCodes.Ret marks the end of the method's execution,

and Invoke converts the method to execute as a delegate.

The above example is quite simple, please remember it well.

Next, we want to use Emit to generate a method like this:

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

What seems to be simple code becomes complex when written in IL.

The ILGenerator is used to write IL through C# code form, but all processes must follow IL steps.

The most important is the OpCodes enumeration, which represents all operational functionalities of IL with dozens of enumerations.

Please refer to: https://docs.microsoft.com/zh-cn/dotnet/api/system.reflection.emit.opcodes?view=netcore-3.1

If you click the above link to view the OpCodes enumeration, you'll see many opcodes. It’s impossible to remember all these opcodes, and as we just began learning Emit, it will make things more difficult.

Therefore, we should download a tool that can view IL code to help us explore and adjust our writing.

Now, let’s look at the IL code generated for this method:


  .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

It doesn’t matter if you don’t understand, as the author also doesn’t fully understand.

Currently, we have obtained information on these two major parts, and next we will use DynamicMethod to dynamically write the method.

Define the Add method and obtain the IL generator:

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

DynamicMethod is used to define a method; ILGenerator is the IL generator. This method can also be attached to a type. The complete code example is as follows:

            // typeof(Program), indicating that this dynamically written method is attached to 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, load the argument at index 0 onto the evaluation stack.
        ilCode.Emit(OpCodes.Ldarg_1); // b, load the argument at index 1 onto the evaluation stack.
        ilCode.Emit(OpCodes.Add);     // add the two values and push the result onto the evaluation stack.

        // The following instructions are not needed as the result is popped from the stack by default
        //ilCode.Emit(OpCodes.Stloc_0); // load the local variable at index 0 onto the evaluation stack.
        //ilCode.Emit(OpCodes.Br_S);    // unconditionally transfer control to the target instruction (short format).
        //ilCode.Emit(OpCodes.Ldloc_0); // load the local variable at index 0 onto the evaluation stack.

        ilCode.Emit(OpCodes.Ret);     // return from the current method and push the return value (if any) from the callee's evaluation stack to the caller's evaluation stack.

        // Method 1
        Func&lt;int, int, int&gt; test = (Func&lt;int, int, int&gt;)dynamicMethod.CreateDelegate(typeof(Func&lt;int, int, int&gt;));
        Console.WriteLine(test(1, 2));

        // Method 2
        int sum = (int)dynamicMethod.Invoke(null, BindingFlags.Public, null, new object[] { 1, 2 }, CultureInfo.CurrentCulture);
        Console.WriteLine(sum);</code></pre>

In reality, the above code differs from the IL code we decompiled; I’m not entirely sure why. I asked in the group and debugged it; only by commenting out those few lines of code was I able to make it work.

痴者工良

高级程序员劝退师

文章评论