+ 3 - 5 | § ¶Another TreeView
I'm
also working on a TreeView, and mine already has checkboxes...

+ 5 - 5 | § ¶A new code generation API
I find System.Reflection.Emit too low level for some kind of uses. If
you are writing a compiler, SRE is just perfect, since you need to have
full control of the IL being generated. However, if you are writing,
guess what, an xml serializer, SRE is not so useful. An xml serializer
needs to generate code for writing and reading the contents of an
object, and this code is not simple (it will include type conversions,
method calls, loops, etc.). For complex types the IL code may be very
long, and debugging this code if something goes wrong would be really
hard.
The existing XmlSerializer does not generate IL, instead
it generates C# code and compiles it using the C# compiler. This
solution has several problems: the generation of the serializer is slow
and the generated code is limited to what C# can do (for example the
serializer can't access private fields).
That's why I've been
thinking on an new kind of IL generation API. With this API you can
generate methods and classes using high level expressions, that will
later generate the IL. It's a kind of on-the-fly compiler. You have the
benefits of SRE (the generation of the code is very fast, and it is not
limited to the features of any language), and the benefits of using a
rather high level language.
Here is a example. This code generates a class with a static method that adds two numbers and returns the result:
CodeClass cls = CodeModule.Shared.CreateClass ("TestClass");
CodeMethod method = cls.CreateStaticMethod ("AddNumbers", typeof(int), typeof(int), typeof(int));
CodeBuilder cb = method.CodeBuilder;
cb.Return (method.GetArg (0) + method.GetArg (1));
cls.CreateType ();
object res = method.MethodInfo.Invoke (null, new object[] {2,3});
Console.WriteLine ("Result: " + res);
Console.WriteLine (cls.PrintCode ());I think the code is easy to understand by itself. The first two lines create the class and the static method.
CodeModule,
CodeClass and
CodeMethod wrap ModuleBuilder, TypeBuilder and MethodBuilder respectively, and add some high level methods to make things easier.
CodeBuilder
can be used to generate the code of the method. In this example, the
only statement needed is a return statement, which returns the addition
of the method arguments.
What is interesting is how the addition is generated. "method.GetArg (n)" returns an object of type
CodeExpression that represents the n-th argument of the method. The operator + for CodeExpression is overloaded, and returns a
CodeAdd expression object that generates the addition of both expressions (other binary operators are also overloaded).
The call to CodeClass.CreateType() generates the IL for the method and
creates the type. What is also interesting is the method
CodeClass.PrintCode(). This method returns a C# representation of the
generated IL. This is not fully compilable C#, its goal is only to give
an overview of the code that has been generated and make easy to verify
that it is generating what it is supposed to generate. It will show
something like this:
public class TestClass
{
static System.Int32 AddNumbers (System.Int32 arg0, System.Int32 arg1)
{
return arg0 + arg1;
}
TestClass ()
{
}
} Now let's see a more complex example. This example generates a
method that calculates the Fibonacci serie up to a number entered by
the user:
CodeClass cls = CodeModule.Shared.CreateClass ("TestClass");
CodeMethod method = cls.CreateStaticMethod ("Run", typeof(void));
CodeBuilder cb = method.CodeBuilder;
CodeValueReference max = cb.DeclareVariable (typeof(int));
CodeValueReference v1 = cb.DeclareVariable (typeof(int), Exp.Literal (0));
CodeValueReference v2 = cb.DeclareVariable (typeof(int), Exp.Literal (1));
CodeValueReference sum = cb.DeclareVariable (typeof(int));
cb.ConsoleWriteLine (Exp.Literal ("Enter a number:"), max);
CodeExpression txt = Exp.Call (typeof(Console), "ReadLine");
cb.Assign (max, Exp.Call (typeof(int), "Parse", txt));
cb.ConsoleWriteLine (Exp.Literal ("Generating Fibonacci numbers until {0}"), max);
cb.While (v2 < max);
cb.Assign (sum, v1 + v2);
cb.Assign (v1, v2);
cb.Assign (v2, sum);
cb.ConsoleWriteLine (v2.CallToString());
cb.EndWhile ();
cb.ConsoleWriteLine ("Done");
cls.CreateType ();
Console.WriteLine (cls.PrintCode ());
method.MethodInfo.Invoke (null, null);
In this example CodeBuilder is used to declare variables, to start and end a While loop, to assign values and so on. If
CodeBuilder is the class to use to generate statements, the
Exp class is the starting point for generating expressions. It can be used to generate literal values, object creation, etc.
It is also interesting to note that all expression types are checked by
the generator, and it will throw an exception if you try, for example,
to assign an expression to a variable that has not been declared with
the same type, or if you are calling a method with incorrect parameters.
The last example: a very simple serializer generator. The method
GenerateSerializer takes a type as parameter and returns an object of
type ISerializer that can be used to serialize objects of that type.
using System;
using System.Reflection;
using Mono.CodeGeneration;
using System.Xml;
public class Test
{
public interface ISerializer
{
void WriteObject (XmlWriter writer, object obj);
}
public static void Main ()
{
ISerializer ser = GenerateSerializer (typeof(Data));
XmlWriter writer = new XmlTextWriter (Console.Out);
ser.WriteObject (writer, new Data ());
}
public static ISerializer GenerateSerializer (Type targetType)
{
CodeClass cls = CodeModule.Shared.CreateClass ("SerializerFor" + targetType.Name, typeof(object), typeof(ISerializer));
CodeMethod method = cls.ImplementMethod (typeof(ISerializer), "WriteObject");
CodeBuilder cb = method.CodeBuilder;
CodeValueReference obj = cb.DeclareVariable (targetType, method.GetArg(1).CastTo (targetType));
cb += method.GetArg(0).Call ("WriteStartElement", Exp.Literal (targetType.Name));
foreach (FieldInfo field in targetType.GetFields (BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance))
cb += method.GetArg(0).Call ("WriteElementString", Exp.Literal (field.Name), obj[field].CallToString());
cb += method.GetArg(0).Call ("WriteEndElement");
Console.WriteLine (cls.PrintCode ());
Type t = cls.CreateType ();
return (ISerializer) Activator.CreateInstance (t);
}
}
public class Data
{
int a = 2;
string b = "hi!";
byte c = 44;
}
Notice how "obj[field]" is used to reference a field of an object
(obj["fieldName"] would also be ok). Notice also that the generator
picks the correct WriteElementString overload according to the
parameter number and types. This is what PrintCode writes:
public class SerializerForData : Test+ISerializer
{
System.Void WriteObject (System.Xml.XmlWriter arg0, System.Object arg1)
{
Data v0;
v0 = ((Data)arg1);
arg0.WriteStartElement ("Data");
arg0.WriteElementString ("a", v0.a.ToString ());
arg0.WriteElementString ("b", v0.b.ToString ());
arg0.WriteElementString ("c", v0.c.ToString ());
arg0.WriteEndElement ();
}
}
The source code of the generator together with some examples can be downloaded from
here. Beware that not everything has been tested and it may generate wrong IL in some cases. If you find any issues drop me an
email.
Now, let's keep working on the serializer...