Skip to content

C# Library for building dynamic types and methods at runtime using the Microsoft Intermediate Language (MSIL or IL).

Notifications You must be signed in to change notification settings

artyredd/ILHelper

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ILHelper

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.

For Package Users

ILHelper in addition to providing a website and user interface to learn also provides a robust library for developing dynamic types at runtime.

  1. Creating a Type
    a. Create the assembly
    b. Create the Type
  2. Add Members to a Type
    a. Adding a Field
    b. Adding an Auto Property
  3. Adding Methods to a Type
    a. Creating The Signature
    b. Emitting the Method Body
    c. Building the Method
  4. Inherit Base Classes
  5. Implement Interfaces
  6. Referencing Dynamic Types in Dynamic Methods
  7. Creating a Complex Type at Runtime

Creating a Type

Create the assembly

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

Create the Type

To create a type use the .CreateType() method implemented by the IDynamicAssemblyBuilder that you previously created.

var sampleClass = assembly.CreateType("SampleClass");

Add Members to a Type

Let's start by creating a type

var sampleClass = new DynamicAssemblyBuilder("TestAssembly")
    .CreateType("SampleClass");

Adding a Field

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

Adding an Auto Property

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

Adding Methods to a Type

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.

Creating the Signature

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>()

Emitting the Method Body

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)

Building the Method

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

Inherit Base Classes

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

Implement Interfaces

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

Referencing Dynamic Types in Dynamic Methods

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

Creating a Complex Type at Runtime

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

About

C# Library for building dynamic types and methods at runtime using the Microsoft Intermediate Language (MSIL or IL).

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published