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("MyTest"); // ⬅</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("HelloWorld");
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<int, int, int> test = (Func<int, int, int>)dynamicMethod.CreateDelegate(typeof(Func<int, int, int>));
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.
文章评论