Understanding Dynamic Assemblies
By definition, static assemblies are .NET binaries loaded directly from disk storage, meaning they are located somewhere in a physical file at the time CLR requests them.
A dynamic assembly is created in memory, on the fly, using the types provided by the System.Reflection.Emit namespace. This namespace makes it possible to create an assembly and its modules, type definitions, and CIL implementation logic at runtime. After you have done so, you are then free to save your in-memory binary to disk.
Exploring the System.Reflection.Emity Namespace
Creating a dynamic assembly required you have to some familiarity with CIL opcodes, but the types of the System.Reflection.Emit namespace hide the complexity of CIL as much as possible. For ex, rather than specifying the necessary CIL directives and attributes to define a class type, you can simply use the TypeBuilderClass.
Select Members of the System.Reflection.Emit Namespace
- AssemblyBuilder : Used to create an assembly (*.dll or *.exe) at runtime. *.exes must call the ModuleBuilder.SetEntryPoint() method to set the method that is the entry point to the module. If no entry point is specified, a *.dll will be generated.
- ModuleBuilder : Used to define the set of modules within the current assembly.
- EnumBuilder : Used to create a .NET enumeration type.
- TypeBuilder : May be used to create classes,interfaces,structures, and delegates within a module at runtime.
- MethodBuilder LocalBuilder PropertyBuilder FieldBuilder ConstructorBuilder CustomAttributeBuilder ParameterBuilder EventBuilder : Used to create type members (such as methods, local variables,properties, constructors, and attributes) at runtime.
- ILGenerator : Emits CIL opcodes into a given type member.
- OpCodes : Provides numerous fields that map to CIL opcodes. This type is used in conjunction with the various members of System.Reflection.Emit.ILGenerator
The Role of the System.Reflection.Emit.ILGenerator
The ILGenerator type's role is to inject CIL opcodes into a given type member.
// Obtain an ILGenerator from a ConstructorBuilder
// object named "myCtorBuilder".
ConstructorBuilder myCtorBuilder = helloWorldClass.DefineConstructor(
MethodAttributes.Public,
CallingConventions.Standard,
constructorArgs);
ILGenerator myCILGen = myCtorBuilder.GetILGenerator();
Various Methods of ILGenerator
- BeginCatchBlock() : Begins a catch block
- BeginExceptionBlock() : Begins an exception scope for an exception
- BeginFinallyBlock() : Begins a finally block
- BeginScope() : Begins a lexical scope
- DeclareLocal() : Declares a local variable
- DefineLabel() : Declares a new label
- Emit() : Is overloaded numerous times to allow you to emit CIL opcodes
- EmitCall() : Pushes a call or callvirt opcode into the CIL stream
- EmitWriteLine() : Emits a call to Console.WriteLine() with different types of values
- EndExceptionBlock() : Ends an exception block
- EndScope() : Ends a lexical scope
- ThrowException() : Emits an instruction to throw an exception
- UsingNamespace() : Specifies the namespace to be used in evaluating locals and watches for the
current active lexical scope
The key method of ILGenerator is Emit(), wihch works in conjunction with the System.Reflection.Emin.OpCodess class type.
Emitting a Dynamic Assembly
// This class will be created at runtime
// using System.Reflection.Emit.
public class HelloWorld
{
private string theMessage;
HelloWorld() { }
HelloWorld(string s) { theMessage = s; }
public string GetMsg() { return theMessage; }
public void SayHello()
{
System.Console.WriteLine("Hello from the HelloWorld class!");
}
}
static AssemblyBuilder CreateMyAsm()
{
// Establish general assembly characteristics.
AssemblyName assemblyName = new AssemblyName
{
Name = "MyAssembly",
Version = new Version("1.0.0.0")
};
// Create new assembly.
var builder = AssemblyBuilder.DefineDynamicAssembly(
assemblyName, AssemblyBuilderAccess.Run);
// Define the name of the module.
ModuleBuilder module = builder.DefineDynamicModule("MyAssembly");
// Define a public class named "HelloWorld".
TypeBuilder helloWorldClass =
module.DefineType("MyAssembly.HelloWorld",
TypeAttributes.Public);
// Define a private String variable named "theMessage".
FieldBuilder msgField = helloWorldClass.DefineField(
"theMessage",
Type.GetType("System.String"),
attributes: FieldAttributes.Private);
// Create the custom ctor.
Type[] constructorArgs = new Type[1];
constructorArgs[0] = typeof(string);
ConstructorBuilder constructor =
helloWorldClass.DefineConstructor(
MethodAttributes.Public,
CallingConventions.Standard,
constructorArgs);
ILGenerator constructorIl = constructor.GetILGenerator();
constructorIl.Emit(OpCodes.Ldarg_0);
Type objectClass = typeof(object);
ConstructorInfo superConstructor =
objectClass.GetConstructor(new Type[0]);
constructorIl.Emit(OpCodes.Call, superConstructor);
constructorIl.Emit(OpCodes.Ldarg_0);
constructorIl.Emit(OpCodes.Ldarg_1);
constructorIl.Emit(OpCodes.Stfld, msgField);
constructorIl.Emit(OpCodes.Ret);
// Create the default constructor.
helloWorldClass.DefineDefaultConstructor(
MethodAttributes.Public);
// Now create the GetMsg() method.
MethodBuilder getMsgMethod = helloWorldClass.DefineMethod(
"GetMsg",
MethodAttributes.Public,
typeof(string),
null);
ILGenerator methodIl = getMsgMethod.GetILGenerator();
methodIl.Emit(OpCodes.Ldarg_0);
methodIl.Emit(OpCodes.Ldfld, msgField);
methodIl.Emit(OpCodes.Ret);
// Create the SayHello method.
MethodBuilder sayHiMethod = helloWorldClass.DefineMethod(
"SayHello", MethodAttributes.Public, null, null);
methodIl = sayHiMethod.GetILGenerator();
methodIl.EmitWriteLine("Hello from the HelloWorld class!");
methodIl.Emit(OpCodes.Ret);
// "Bake" the class HelloWorld.
// (Baking is the formal term for emitting the type.)
helloWorldClass.CreateType();
return builder;
}
Emitting the Assembly and Module Set
The method body begins by establishing the minimal set of characteristics about your assembly, using the AssemblyName and Version types. Next, you obtain an AssemblyBuilder type via the static AssemblyBuilder.DefineDynamicAssembly() method.
When calling DefineDynamicAssembly(), you must specify the access mode of the assembly you want to define:
| RunAndCollect | The assembly will be immediately unloaded, and its memory is reclaimed once it is no longer accessible. |
| Run | This represents that a dynamic assembly can be executed in memory but not saved to disk. |
// Create new Assembly
var builder = AssemblyBuilder.DefineDynamicAssembly(aName, AssemblyBuilderAccess.Run);
The Role of the ModelBuilder TypeC
ModelBuilder supports several members that allow you to define the set of types contained within a given module (classes,interfaces,structures,etc) as well as the set of embedded resources (string tables, images,etc.) contained within.
| DefineEnum() | Used to emit a .NET enum definition. |
| DefineType() | Constructs a TypeBuilder, which allows you to define value types,interfaces, and class types (including delegates) |
In DefineType: You can use System.Reflection.TypeAttributes Enum;
- Abstract : Specifies that the type is abstract.
- Class : Specifies that the type is abstract.
- Interface : Specfies that the type is an interface.
- NestedAssembly : Specifies that the class is nested with assembly visibility and is thus accessible only by methods within its assembly.
- NestedFamANDAssem :Specifies that the class is nested with assembly visibility and is thus accessible only by methods lying in intersection of its family and assembly.
- NestedFamORAssem : Specifies that the class is nested with assembly visibility and is thus accessible only by methods lying in union of its family and assembly.
- NestedFamily : Specifies that the class is nested with family visibility and is thus accessible only by
methods within its own type and any subtypes.
- NestedPrivate : Specifies that the class is nested with private visibility.
- NestedPublic : Specifies that the class is nested with public visibility.
- NotPublic : Specifies that the class is not public.
- Public : Specifies that the class is public.
- Sealed : Specifies that the class is concrete and cannot be extended.
- Serializable : Specifies that the class can be serializable.
Emitting the HelloClass Type and the String Member Variable
// Define a public class named "HelloWorld".
TypeBuilder helloWorldClass = module.DefineType("MyAssembly.HelloWorld",TypeAttributes.Public);
// Define a private String variable named "theMessage".
FieldBuilder msgField = helloWorldClass.DefineField("theMessage",Type.GetType("System.String"),
attributes: FieldAttributes.Private);
Emitting the Constructors
The TypeBuilder.DefineConstructor() method can be used to define a constructor for the current type.
// Create the custom ctor taking single string arg.
Type[] constructorArgs = new Type[1];
constructorArgs[0] = typeof(string);
ConstructorBuilder constructor =helloWorldClass.DefineConstructor(MethodAttributes.Public,CallingConventions.Standard,
constructorArgs);
//Emit the necessary CIL into the ctor
ILGenerator constructorIl = constructor.GetILGenerator();
constructorIl.Emit(OpCodes.Ldarg_0);
Type objectClass = typeof(object);
ConstructorInfo superConstructor =
objectClass.GetConstructor(new Type[0]);
constructorIl.Emit(OpCodes.Call, superConstructor);
//Load this pointer onto the stack
constructorIl.Emit(OpCodes.Ldarg_0);
constructorIl.Emit(OpCodes.Ldarg_1);
//Load argument on virtual stack and store in msdField
constructorIl.Emit(OpCodes.Stfld, msgField);
constructorIl.Emit(OpCodes.Ret);
Emitting the SayHello() Method
The first task is to obtain a MethodBuilder type from the helloWorlClass variable. After you do this, you define the method and obtain the underlying ILGenerateor to inject CIL instructions.
Using the Dynamically Generated Assembly
// Create the assembly builder using our helper f(x).
AssemblyBuilder builder = CreateMyAsm();
// Get the HelloWorld type.
Type hello = builder.GetType("MyAssembly.HelloWorld");
// Create HelloWorld instance and call the correct ctor.
Console.Write("-> Enter message to pass HelloWorld class: ");
string msg = Console.ReadLine();
object[] ctorArgs = new object[1];
ctorArgs[0] = msg;
object obj = Activator.CreateInstance(hello, ctorArgs);
// Call SayHello and show returned string.
Console.WriteLine("-> Calling SayHello() via late binding.");
MethodInfo mi = hello.GetMethod("SayHello");
mi.Invoke(obj, null);
// Invoke method.
mi = hello.GetMethod("GetMsg");
Console.WriteLine(mi.Invoke(obj, null));