ILHelper is a Blazor WASM website that is intended to serve as a tool to assist CSharp programmers in learning what Microsoft Intermediate Language(MSIL or IL for short) is.
ILHelper in addition to providing a website and user interface to learn also provides a robust library for developing dynamic types at runtime.
- Creating a Type
a. Create the assembly
b. Create the Type - Add Members to a Type
a. Adding a Field
b. Adding an Auto Property - Adding Methods to a Type
a. Creating The Signature
b. Emitting the Method Body
c. Building the Method - Inherit Base Classes
- Implement Interfaces
- Referencing Dynamic Types in Dynamic Methods
- Creating a Complex Type at Runtime
Start off by creating an assembly that the type should be contained within. Make sure you are referencing ILHelper
in your project.
using ILHelper;
using ILHelper.Extensions;
var assembly = new DynamicAssemblyBuilder("TestAssembly");
To create a type use the .CreateType()
method implemented by the IDynamicAssemblyBuilder
that you previously created.
var sampleClass = assembly.CreateType("SampleClass");
Let's start by creating a type
var sampleClass = new DynamicAssemblyBuilder("TestAssembly")
.CreateType("SampleClass");
You can create fields either from a type
.CreateField<T>(string FieldName, FieldAttributes Attributes, T DefaultValue)
Alternatively you could create it from a type variable, which may be necessary if you are using a dynamic type generated by this library
.CreateField(Type FieldType, string FieldName, FieldAttributes Attributes, object DefaultValue)
Let's create a field and read it's values. This field will be of type int
and have a default value of 44
.
sampleClass.CreateField<int>("Value", 44);
We can create an instance of our new type by using .CreateInstance()
. This will compile our type and we will no longer be able to change its members.
// cast to dynamic for convenience
dynamic instance = (dynamic)sampleClass.CreateInstance();
Console.WriteLine(instance.Value);
// outputs 44
We can add properties to our class using a type variable or a type just like we could with fields.
.CreateProperty(Type PropertyType, string PropertyName, object DefaultValue)
.CreateProperty<T>(string PropertyName, T DefaultValue)
By default properties are public
and contain a Get
;Set
; methods that access a private backing field.
Let's create a class with a property
var sampleClass = new DynamicAssemblyBuilder("TestAssembly")
.CreateType("SampleClass")
.CreateProperty<int>("MyProperty",99);
Now we can create an instance and see if the property properly works as intended.
dynamic instance = (dynamic)sampleClass.CreateInstance();
Console.WriteLine(instance.MyProperty);
// outputs 99
instance.MyProperty = 23;
Console.WriteLine(instance.MyProperty);
// outputs 23
Methods are build dynamically using IL. You can find all fo the OpCodes(IL) on MSDN here.
Let's create a Type
that has a single method that adds two integers.
Start by creating a type to hold the method.
var sampleClass = new DynamicAssemblyBuilder("TestAssembly")
.CreateType("SampleClass");
Then we will create a method
sampleClass.CreateMethod("Adder")
We can define what the signature of the method should look like or define the method body(the IL) in any order.
I prefer defining the signature first so let's define what the method should accept as arguments and what it should return. Since this should add two numbers, it should accept two integers and return another integer.
sampleClass.CreateMethod("Adder")
.Accepts<int,int>()
.Returns<int>()
It should be noted that using generic types is not required you could alternatively use type variables as well
sampleClass.CreateMethod("Adder")
.Accepts(typeof(int), typeof(int)))
.Returns<int>()
Now that we have defined a signature we can implement the method body. In IL to add to numbers we should push both values to the stack, add them, and return the value.
In order to implement the method body we have to emit the IL we want to run. For this we just use the .Emit()
methods and extensions.
sampleClass.CreateMethod("Adder")
.Accepts(typeof(int), typeof(int)))
.Returns<int>()
// push the first argument to the stack
.Emit(OpCodes.Ldarg_0)
// push the second argument to the stack
.Emit(OpCodes.Ldarg_1)
// add the two numbers
.Emit(OpCodes.Add)
// return the added number
.Emit(OpCodes.Ret)
Finally to finish the method we should compile it for that use .Build()
.
Let's put it together and check to see if it works.
sampleClass.CreateMethod("Adder")
.Accepts(typeof(int), typeof(int)))
.Returns<int>()
// push the first argument to the stack
.Emit(OpCodes.Ldarg_1)
// push the second argument to the stack
.Emit(OpCodes.Ldarg_2)
// add the two numbers
.Emit(OpCodes.Add)
// return the added number
.Emit(OpCodes.Ret)
// compile the method
.Build();
dynamic instance = (dynamic)sampleClass.CreateInstance();
int result = instance.Adder(12,12);
Console.WriteLine(result);
// outputs 24
You can have dynamically created types inherit from a base class using either the type or a type variable.
.Inherits(Type BaseClass)
.Inherits<T>()
Let's create a base class and a new type that inherits from it
abstract class SampleBase
{
}
var sampleClass = new DynamicAssemblyBuilder("TestAssembly")
.CreateType("SampleClass")
.Inherits<SampleBase>();
Now when we create a new instance(compile the type) we can check to see if the base class was inherited.
object instance = sampleClass.CreateInstance();
Console.WriteLine(instance is SampleBase);
// outputs true
You can have dynamically created types implement interfaces using either the type or a type variable.
.Implements(Type InterfaceType)
.Implements<T>()
Let's create an interface and a new type that implements it
public interface ISample
{
}
var sampleClass = new DynamicAssemblyBuilder("TestAssembly")
.CreateType("SampleClass")
.Implements<ISample>();
Now when we create a new instance(compile the type) we can check to see if the interface was implemented.
object instance = sampleClass.CreateInstance();
Console.WriteLine(instance is ISample);
// outputs true
Often times you may want to reference other dynamic types, fields, properties and methods within other dynamic objects. This is by-default supported for all members.
This is often needed when implementing method bodies so let's create an example.
We are going to create a SampleClass
with one field Value
. We should then create another class called Factory
who should have a single method called GetValue
and returns the value of the Value
field on a SampleClass
object.
Let's start by creating the SampleClass
var sampleClass = assembly.CreateType("SampleClass")
.CreateField<int>("Value",99);
sampleClass.Build();
Let's then create the Factory
and the GetValue
.
var factoryClass = assembly.CreateType("Factory")
.CreateMethod("GetValue")
.Returns<int>()
.Accepts(assembly.Type("SampleClass"));
// push the pointer to the class to the stack
.Emit(OpCodes.Ldarg_1)
// get the value from the field and push it to the stack
.Emit(OpCodes.Ldfld, sampleClass.Field("Value"))
// return the value
// this emits OpCode.Ret AND builds the method
.Return();
Let's create the instances and test to make sure everything works
dynamic sample = (dynamic)sampleClass.CreateInstance();
sample.Value = 32;
dynamic factory = (dynamic)factoryClass.CreateInstance();
int result = factory.GetValue(sample);
Console.WriteLine(result);
// outputs 32
Using all of the above tools we can create a complex dynamic type at runtime.
For this example we're going to be replicating the following class in IL.
public class SampleClass : IValue
{
public int Value
{
get => _Value;
set
{
_Value = Set;
}
}
private int _Value;
public string Name = "";
private bool flag = false;
public void SetFlag(bool flag)
{
flag = true;
}
}
public interface IValue
{
Value {get; set;}
}
// create the assembly and type
var sampleClass = new DynamicAssemblyBuilder("TestAssembly")
.CreateType("SampleClass")
.Implements(typeof(IValue))
.CreateField<bool>("flag", FieldAttributes.Private, false)
.CreateProperty<int>("Value")
.CreateField<string>("Name", string.Empty)
.CreateMethod("SetFlag")
.Returns(typeof(void))
.Accepts<bool>()
.Emit(OpCodes.Ldarg_0)
.Emit(OpCodes.Ldarg_1)
.Emit(OpCodes.Stfld, "flag")
.Return();
Lets create the instance and check to see if everything works, however, becuase the field flag
is private
we will have to use reflection to check it's value.
dynamic instance = (dynamic)sampleClass.CreateInstance();
instance.SetFlag(true);
bool result = (bool)((object)instance).GetType().GetField("flag", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(instance);
Console.WriteLine(result);
// outputs true