The primary case for runtime code generation in C# is expression trees, which allow you to create high-level ASTs and compile them to delegate types (such as Func
or Action
) at runtime. Since delegate types are common in C#, it's easy to mix compiled expression trees with normal C# code. Today, I'd like to discuss another kind of runtime code generation in C#: creating new types!
In C#, we can compile types into a new assembly at runtime, and then use those types in our running program. This comes with two key limitations:
First, we can’t use the new types in statically-typed code. We can’t do var myObject = new MyRuntimeType()
or write myObject.myRuntimeField
in C#— because that would require the type and field to have existed at compile-time. However, we can have the types inherit from existing interfaces or existing classes and handle instances of the new types via polymorphism, such as computing (myObject as IExistingInterface).InterfaceProperty
. We can also use the new types within expression trees. For example, we could create an expression tree of type Func<object, int>
that computes (myObject as MyRuntimeType).myRuntimeField
.
Second, we have to use MSIL if we want to define methods on our new types. MSIL is an intermediate language that is very close to assembly, and is much more inconvenient to use than expression trees, which are much closer to native C#. This makes it impractical to define complex logic on runtime type methods.
In this article, I’ll show some basic cases of how to use runtime type construction and how to integrate it with expression trees. I’ll be using the FluentIL library, which simplifies a lot of the boilerplate around MSIL generation. Note that this package isn’t on NuGet (the one on NuGet is another one of several packages named FluentIL), so you’ll have to build it yourself or use a substitute.
Creating Structs
Let’s start by creating an empty struct type. First, we have to create a new assembly and module in which to store the type. In FluentIL, this is abstracted as TypeFactory.
TypeFactory factory = new("DynamicAssemblyTest", "DynamicAssemblyTest");
To create a struct type, we’ll want to create a new public type that inherits from ValueType, which is the parent type for all structs in C#. TypeFactory.NewType
returns an ITypeBuilder, which allows us to configure features of the type before actually realizing it.
var typCreator = factory.NewType("MyType").Public()
.InheritsFrom<ValueType>();
This is enough to define a struct type — no constructor is required. We can now realize the type and create an instance of it.
var typ = typCreator.CreateType();
Console.WriteLine($"Is value type: {typ.IsValueType}"); //true
dynamic s1 = Activator.CreateInstance(typ)!;
Since we haven’t defined any fields or properties on this struct type, we can’t really do much with our new instance. Let’s define two fields on the type builder (before calling CreateType
):
var intField = typCreator.NewField<int>("m_myInt"); //private
var stringField = typCreator.NewField<string>("myString").Public(); //public
Let’s also create a constructor that takes an int and string argument and populates these two fields. To create a constructor, or any method, we must define its signature and (if it’s not abstract) its body.
//Signature
var constr = typCreator.NewConstructor().Public()
.Param<int>("intArgument")
.Param<string>("stringArgument");
//Body
constr.Body()
.LdArg0().LdArg1().StFld(intField)
.LdArg0().LdArg2().StFld(stringField)
.Ret();
The method body is defined using MSIL. The code works as follows:
- Load
this
(arg0) and then "intArgument" (arg1) onto the stack. (For instance methods, the first argument is always the implicitthis
.) - Use StFld to store “intArgument” into the field defined by
intField
onto "this". - Load “this” (arg0) and then “stringArgument” (arg2) onto the stack.
- Use StFld to store “stringArgument” into the field defined by
stringField
onto "this". - Return.
We can now call this constructor:
var typ = typCreator.CreateType();
dynamic s1 = Activator.CreateInstance(typ, [214, "hello world"])!;
Console.WriteLine(s1.myString); //hello world
Note: dynamic
is a special C# construct that allows accessing fields for unknown types using reflection. Since it is slow and error-prone, it is generally not suitable for actual production code. We can avoid this by using interfaces, which will be demonstrated later in this article.
Earlier, we defined m_myInt
as a private field and myString
as a public field. We can create a property to access m_myInt
. Defining properties is much more complex than defining fields, because properties are actually abstractions over two separate hidden methods (a getter and setter).
var virtualMethod =
MethodAttributes.Public | MethodAttributes.HideBySig |
MethodAttributes.Virtual;
var virtualProperty = MethodAttributes.SpecialName | virtualMethod;
var intField = typCreator.NewField<int>("m_myInt");
var intProp = typCreator.NewProperty<int>("MyInt");
intProp.Getter().MethodAttributes(virtualProperty).Body()
.LdArg0().LdFld(intField).Ret();
intProp.Setter().MethodAttributes(virtualProperty).Body()
.LdArg0().LdArg1().StFld(intField).Ret();
The getter method has one implicit argument, which is this
(arg0). LdFld will pull the value of the field from the item at the top of the stack. The setter method has two implicit arguments, which are are this
(arg0) and value
(arg1). As before, StFld will store the value on the top of the stack into the field on the second item in the stack.
The default method attributes we should use for a method are Public and HideBySig. It’s convenient to additionally mark methods as virtual since that allows them to implement interfaces, but if we’re not overriding/implementing anything, then Virtual can be omitted. For property getters/setters, we additionally need the SpecialName attribute.
We can now access MyInt as a property on the struct instance:
var typ = typCreator.CreateType();
dynamic s1 = Activator.CreateInstance(typ, [214, "hello world"])!;
Console.WriteLine(s1.myString); //hello world
s1.MyInt += 5;
Console.WriteLine(s1.MyInt); //219
Creating Classes
Creating a class instead of a struct is fairly straightforward. First, we change the inheritance to use object
instead of ValueType
as a base class:
var typCreator = factory.NewType("MyType").Public()
.InheritsFrom<object>();
Second, we add a line to the constructor to call the base object constructor. (It’s not clear that this is necessary, but official documentation includes this step.)
constr.Body()
.LdArg0().Call(typeof(object).GetConstructor(Type.EmptyTypes)) //New
.LdArg0().LdArg1().StFld(intField)
.LdArg0().LdArg2().StFld(stringField)
.Ret();
We can now create the type as normal, and IsValueType will return false.
var typ = typCreator.CreateType();
Console.WriteLine($"Is value type: {typ.IsValueType}");
dynamic s1 = Activator.CreateInstance(typ, [214, "hello world"])!;
Console.WriteLine(s1.myString); //hello world
s1.MyInt += 5;
Console.WriteLine(s1.MyInt); //219
But is it REALLY a struct?
You might not be convinced that IsValueType
accurately represents whether the constructed object is "really" a struct or not. Luckily, there's a fairly straightforward test we can use to verify this. Consider the following example code, where there is a type MyType
with a field intField
.
var x = new MyType();
var y = x;
x.intField += 1;
Console.WriteLine(x.intField == y.intField);
If MyType
is a value type, such that the "value" of an instance is a chunk of data containing its fields, then y = x
will create a copy of this data, such that changes to x.intField
will not be reflected in y.intField
, so the final check will be false.
If MyType
is a reference type, such that the "value" of an instance is a pointer to another location in memory containing the fields, then y = x
will only copy this pointer, and point to the same location in memory. Thus, changes to x.intField
will be reflected in y.intField
, and the final check will be true.
Running this test is a bit more complicated with our runtime types. If we’re running this code using dynamic objects or object/interface polymorphism, then value-typed x
is boxed into a reference-type container, and y = x
will copy the pointer to the boxing of x
. Instead, we can use an expression tree to perform this copy operation.
Consider the function MyType Return(MyType arg) => arg
. If we write var y = Return(x)
, then we will get the desired distinction between value and reference types. We can dynamically create this function for runtime types as follows:
public static Delegate Return(Type t) {
var prm = Expression.Parameter(t);
return Expression.Lambda(prm, prm).Compile();
}
Even if boxing occurs around the usage of this expression function, boxing will not occur within it, since the types are known when the expression is compiled. Thus, if t
is a value type, then the return value of this expression function will be different from the argument.
We can now check for value typing as follows:
Console.WriteLine($"(Formally) is value type: {typ.IsValueType}");
dynamic s1 = Activator.CreateInstance(typ, [214, "hello world"])!;
Console.WriteLine(s1.myString); //hello world
var s2 = Return(typ).DynamicInvoke(s1);
s1.MyInt += 5;
Console.WriteLine(s1.MyInt); //219
Console.WriteLine(s2.MyInt); //214 for value type; 219 for reference type
Console.WriteLine($"(Experimentally) is value type: {s1.MyInt != s2.MyInt}");
If we run this code on our current implementation that inherits from object
, then s2.MyInt
will be 219 and the final check will return false. If we instead have our type inherit from ValueType
(and remove the base constructor call), then s2.MyInt
will be 214 and the final check will return true.
Implementing Existing Interfaces
Implementing an interface doesn’t require anything more than marking the relevant methods/properties as Virtual and marking the type builder to implement the interface. To see this in action, let’s first create an interface that is known at compile-time with one property and two methods, one of which has a default implementation.
public interface IMyInterface {
public int MyInt { set; }
public int GetDifference(int from);
public int DefaultImpl() => GetDifference(100);
}
Let’s update our type builder to mark that it implements the interface. Note that it’s not a good idea to make struct types that implement interfaces, since you’ll have to manually handle boxing and unboxing them.
var typCreator = factory.NewType("MyType").Public()
.InheritsFrom<object>()
.Implements<IMyInterface>();
We’ve already implemented MyInt
and labelled it as virtual, but we haven't implemented GetDifference
. If we try to construct the type as-is, we'll get an exception:
Unhandled exception. System.TypeLoadException: Method 'GetDifference' in type 'MyType' from assembly 'DynamicAssemblyTest, Version=0.0.0.
0, Culture=neutral, PublicKeyToken=null' does not have an implementation.
Let’s add an implementation for GetDifference
to typCreator
. In addition to defining the attributes and body as we did for properties, we also need to define the signature, including the return type.
typCreator.NewMethod("GetDifference").MethodAttributes(virtualMethod)
.Param<int>("from").Returns<int>()
.Body()
.LdArg1()
.LdArg0()
.LdFld(intField) //.Call(prop.Getter()) //either works
.Sub()
.Ret();
The IL for the method body computes return from - this.intField
. To understand how this works, think of the instructions as written in postfix:
((arg1:from (arg0:this load_intField) subtract) return)
With this, the type will compile, and we can cast its instances to IMyInterface
.
IMyInterface s1 = (IMyInterface)Activator.CreateInstance(typ, [214, "hello world"])!;
Console.WriteLine(s1.GetDifference(300)); //86
s1.MyInt = 5;
Console.WriteLine(s1.GetDifference(200)); //195
Console.WriteLine(s1.DefaultImpl()); //95
Since DefaultImpl
has a default implementation, we don't need to provide an implementation it in the type builder. However, if we do provide an implementation, we can even do something that isn't possible in standard C#: we can call the default interface implementation from its override.
typCreator.NewMethod("DefaultImpl").MethodAttributes(virtualMethod)
.Returns<int>()
.Body()
.LdArg0().Call(typeof(IMyInterface).GetMethod("DefaultImpl"))
.LdcI4(10000)
.Add()
.Ret();
This implementation calls the default interface implementation for DefaultImpl
and adds 10000 to it. After adding this method, s1.DefaultImpl()
now returns 10095.
Note that the Call
opcode executes the provided method without any virtual lookup. This is not generally possible in C# proper, which always performs virtual lookup (CallVirt
opcode) for any overridable method except when using the base
keyword. If we used CallVirt
in this definition, then the function would call itself recursively and cause a stack overflow. Since we use Call
, the default interface implementation will be invoked even if a virtual override exists.
If IMyInterface
was a class and not an interface, then this would be equivalent to int DefaultImpl() => base.DefaultImpl() + 10000
, but you can't call base
with regards to an interface as of C#12.
Observe that since we can immediately cast instances of our runtime types to the existing IMyInterface
, the runtime types can play nice with the compile-time type system and avoid reflection hijinks. In previous examples, we used dynamic
to access fields on our runtime types, but that's no longer necessary. If our production code uses IMyInterface
, then it's practical for us to use these runtime types in production code.
You might object that Activator.CreateInstance
still requires reflection every time a new instance of our runtime type is created. In fact, we can avoid this as well by putting the constructor into an expression tree that outputs IMyInterface
:
var intPrm = Expression.Parameter(typeof(int));
var stringPrm = Expression.Parameter(typeof(string));
var constructor = Expression.Lambda<Func<int, string, IMyInterface>>(
Expression.New(typ.GetConstructor([typeof(int), typeof(string)])!, intPrm, stringPrm)
, intPrm, stringPrm).Compile();
IMyInterface s1 = constructor(214, "hello world");
Console.WriteLine(s1.GetDifference(300)); //86
s1.MyInt = 5;
Console.WriteLine(s1.GetDifference(100)); //95
In this setup, we no longer have any reflection after the one-time type creation and expression tree creation. We can create as many instances of the type as we like, and operate on it in statically-typed code using the preexisting interface, all without involving any reflection calls.
Creating New Interfaces
Instead of implementing existing interfaces, we can use runtime type construction to create new interfaces. Admittedly, it’s difficult to think of a use case for this. Implementing existing interfaces allows runtime types to be integrated into the existing type system of a compiled program, but new runtime interfaces can’t really be used except in expression trees. However, it’s fairly straightforward to do.
Let’s create an interface that looks the same as IMyInterface
defined above.
var intfCreator = factory.NewType("IRuntimeInterface")
.Attributes(TypeAttributes.Abstract | TypeAttributes.Interface | TypeAttributes.Public);
intfCreator.NewProperty<int>("MyInt").Setter().MethodAttributes(MethodAttributes.Abstract | virtualProperty);
intfCreator.NewMethod("GetDifference").Param<int>("from").Returns<int>()
.MethodAttributes(MethodAttributes.Abstract | virtualMethod);
intfCreator.NewMethod("DefaultImpl").Returns<int>()
.MethodAttributes(virtualMethod)
.Body().LdArg0().LdcI4(100).CallVirt(getDiff).Ret();
var intf = intfCreator.CreateType();
The code for declaring the interface is fairly straightforward. The key differences from declaring a standard type are as follows:
- We have to mark the type attributes Abstract and Interface.
- Properties/methods without default implementations must be marked as abstract and virtual, and not have bodies.
- Properties/methods with default implementations must be marked as virtual and have bodies.
Now, let’s make our runtime type implement this runtime interface. Let’s declare it as follows:
var typCreator = factory.NewType("MyType").Public()
.InheritsFrom<object>()
.Implements(intf);
To show that the default implementation works, also remove the definition for typCreator.NewMethod("DefaultImpl")...
.
We can’t use IRuntimeInterface as a type in our code since it doesn’t exist at compile-time. Instead, let’s use expression trees to cast our instance to IRuntimeInterface and access its methods.
object s1 = Activator.CreateInstance(typ, [214, "hello world"])!;
var prm = Expression.Parameter(typeof(object));
//(s1 as IRuntimeInterface).GetDifference(300)
Console.WriteLine(Expression.Lambda<Func<object, int>>(
Expression.Call(Expression.TypeAs(prm, intf), intf.GetMethod("GetDifference")!, Expression.Constant(300))
, prm).Compile()(s1)); //=86
//(s1 as IRuntimeInterface).MyInt = 5
Expression.Lambda<Func<object, int>>(
Expression.Assign(
Expression.Property(Expression.TypeAs(prm, intf), intf.GetProperty("MyInt")!),
Expression.Constant(5))
, prm).Compile()(s1);
//(s1 as IRuntimeInterface).GetDifference(200)
Console.WriteLine(Expression.Lambda<Func<object, int>>(
Expression.Call(Expression.TypeAs(prm, intf), intf.GetMethod("GetDifference")!, Expression.Constant(200))
, prm).Compile()(s1)); //=195
//(s1 as IRuntimeInterface).DefaultImpl()
Console.WriteLine(Expression.Lambda<Func<object, int>>(
Expression.Call(Expression.TypeAs(prm, intf), intf.GetMethod("DefaultImpl")!)
, prm).Compile()(s1)); //=95
We can see that all the methods print the expected values. Now, let’s add back the override for DefaultImpl. This time, we’ll call the default implementation on IRuntimeInterface and subtract 1000 from it.
typCreator.NewMethod("DefaultImpl").MethodAttributes(virtualMethod)
.Returns<int>()
.Body()
.LdArg0().Call(intf.GetMethod("DefaultImpl"))
.LdcI4(1000)
.Sub()
.Ret();
Now, running the expression tree for (s1 as IRuntimeInterface).DefaultImpl()
returns -905.
Static Methods
Creating static methods is almost the same as creating instance methods. We just have to mark the Static method attribute, and keep in mind that there is no longer an implicit this
in arg0.
var staticMeth = typCreator.NewMethod("ThisMethodIsStatic")
.MethodAttributes(MethodAttributes.Static | MethodAttributes.Public | MethodAttributes.HideBySig)
.Param<int>("x").Returns<int>();
staticMeth.Body()
.LdArg0() //this points to `x`, not `this`
.LdcI4(42)
.Add()
.Ret();
This is equivalent to public static int ThisMethodIsStatic(int x) => x + 42
.
After creating the type, we can call this static method from an expression tree as follows:
Console.WriteLine(Expression.Lambda<Func<int>>(
Expression.Call(null, typ.GetMethod("ThisMethodIsStatic")!, Expression.Constant(1000)))
.Compile()()); //=1042
Now that C# supports static abstract interface members, you might be wondering if we can use these static methods as interface implementations. The answer is yes, but it’s a bit more convoluted than overriding instance methods since we can’t use the Virtual attribute on static methods. First, let’s update IMyInterface to include a static abstract method, and add a helper function that we can use to call this method from a type.
public interface IMyInterface {
public int MyInt { set; }
public int GetDifference(int from);
public int DefaultImpl() => GetDifference(100);
public static virtual int ThisMethodIsStatic(int x) => 0;
public static int CallStaticMethod<T>(int x) where T : IMyInterface => T.ThisMethodIsStatic(x);
}
Next, let’s invoke CallStaticMethod
using our runtime type.
Console.WriteLine(typeof(IMyInterface).GetMethod("CallStaticMethod")!.MakeGenericMethod(typ)
.Invoke(null, [1000]));
If you run this code, it will print 0 instead of 1042 (though I suspect this may be a bug, and it may be changed in the future). At the time of writing, static interface methods are not automatically overridden in MSIL. In fact, if you make the interface definition of ThisMethodIsStatic
abstract, then an exception will be thrown during type creation. To resolve this, we have to explicitly link the method override before we create the type.
typCreator.Define().DefineMethodOverride(staticMeth.Define(),
typeof(IMyInterface).GetMethod("ThisMethodIsStatic")!);
var typ = typCreator.CreateType();
This fixes the code, and now CallStaticMethod will return 1042 as expected.
Conclusion
I initially investigated runtime type construction because I had a usecase where I wanted to create runtime subclasses of a known parent type, where the subclasses had extra fields that could be accessed in expression tree functions called on the parent type. However, this ultimately didn’t go very far because runtime type construction doesn’t work on AoT platforms, and this usecase was for a game engine, which should ideally support AoT compilation. Oops!
Honestly, I don’t think there are many good use cases for runtime type construction. If you’re not using runtime types to implement existing interfaces or extend existing classes, then the overhead of using reflection/dynamic access makes them unsuitable for production. However, it’s not clear to me why you would want to create a type at runtime to implement existing interfaces or extend existing classes, unless you’re doing something like compiling a custom language. In comparison, I find myself reaching for expression trees fairly often.
If there’s one thing I got out of this, it’s that it now bothers me even more that C# doesn’t allow you to call a default interface implementation from its override. It’s possible in MSIL using Call
, and apparently there have been plans as far back as 2019 to add support for base(IMyInterface).InterfaceMethod()
to C#. However, the proposal appears to be dead in the water.
That’s all for this article. You can see the example code in this article compiled in this gist (licensed under CC0). Hopefully the next one will be about a more useful topic!