Friday, 15 November 2013

Dynamic Compilation (Part 4)

I already explained how to quickly generate code from functions in previous posts.

Code generation has some interesting advantages:
  • You can export code to a project/file
  • This allows you to easily debug
Now this also has some drawbacks:
  • You need a good template engine, or create your own.
  • You can start to have a lot of different templates
  • String generation is feeling a bit clunky at times.
  • You are not allowed to dump your assembly, and in most cases you'll also have to create one temporary assembly per node (not always true).

In the other hand, it's also possible to generate IL code directly, or have it generated via Expression.

Expression is awesome, you can really do cool things with it, but in my use case it's not ideal (I need to reflect existing functions).I'll go back to expressions another time, since now we'll look at IL generation.

Instead of doing all this string cooking, we can just build our class at runtime emitting IL code (which is some kind of "high level assembler" to simplify.

This has the advantage that it's faster to build, you can create several nodes in the same assembly any time during your application lifecycle, dump/replace a method on the fly, and you learn a bit into the depths of .net ;)

So first you need to prepare a few builders:

Code Snippet
  1. AssemblyName myAssembly = new AssemblyName(Guid.NewGuid().ToString());
  2. AssemblyBuilder asm = AppDomain.CurrentDomain.DefineDynamicAssembly(myAssembly, AssemblyBuilderAccess.Run);
  4. ModuleBuilder mb = asm.DefineDynamicModule("MyModule");
  6. TypeBuilder tb = mb.DefineType("Hello");
  7. tb.AddInterfaceImplementation(typeof(IAutomatonNodeInstance));

Then you need a default constructor, it doesn't create one for you:

Code Snippet
  1. ConstructorBuilder cb = tb.DefineDefaultConstructor(MethodAttributes.Public);

That was hard :)

Now you need to also create fields to accept your method delegate:

Code Snippet
  1. ParameterInfo[] prms = method.GetParameters();
  2. List<FieldBuilder> fields = new List<FieldBuilder>();
  4. FieldBuilder containerfield = tb.DefineField("container", typeof(IAutomatonNodeContainer), FieldAttributes.Private);
  5. FieldBuilder routput = tb.GetSourcePin(method.ReturnParameter.ParameterType, "output");
  7. foreach (ParameterInfo pi in prms)
  8. {
  9.     fields.Add(tb.DefineField(pi.Name, this.GetSinkType(pi.ParameterType), FieldAttributes.Private));
  10. }

Basically I create one field to accept the Node container, one field for the ouput.
Then you iterate on Parameter list and create one field for each of those (oh and you can just specify type, no crazy using namespaces or adding them all around).

Now let's go to the interesting bit, my node has 2 methods : Initialize (container), Evaluate (no parameters)

Initialize does the following:
  • Assign container
  • Create pins/parameters.
Evaluate does:
  • Read parameters
  • Call function
  • Assign output
So to initialize, we do the following:

Code Snippet
  1. MethodBuilder assign = tb.DefineMethod("AssignContainer", MethodAttributes.Public | MethodAttributes.Virtual, CallingConventions.HasThis, typeof(void), new Type[] { typeof(IAutomatonNodeContainer) });
  2. ILGenerator assignopcodes = assign.GetILGenerator();
  4. assignopcodes.Emit(OpCodes.Ldarg_0);
  5. assignopcodes.Emit(OpCodes.Ldarg_1);
  6. assignopcodes.Emit(OpCodes.Stfld, containerfield);
  8. int i = 0;
  9. foreach (ParameterInfo pi in prms)
  10. {
  11.     assignopcodes.CreateParameter(pi, fields[i]);
  12.     i++;
  13. }
  15. assignopcodes.Emit(OpCodes.Ldarg_0);
  16. assignopcodes.Emit(OpCodes.Ldarg_1);
  17. assignopcodes.Emit(OpCodes.Callvirt, typeof(AutomatonNodeContainer).GetProperty("PinFactory").GetGetMethod(true));
  18. assignopcodes.Emit(OpCodes.Ldstr, "Output");
  19. assignopcodes.Emit(OpCodes.Callvirt, method.ReturnParameter.ParameterType.CreateSourcePinMethod());
  20. assignopcodes.Emit(OpCodes.Stfld, routput);
  21. assignopcodes.Emit(OpCodes.Ret);

We create a method (make it public and virtual since we implement an interface).
CallingConvetion.HasThis is necessary since it's a class method (basically this will be pushed as first argument, eg: OpCodes.Ldarg_0)

Then we create parameter like this:

Code Snippet
  1. public static void CreateParameter(this ILGenerator gen, ParameterInfo pi, FieldBuilder field)
  2. {
  3.     gen.Emit(OpCodes.Ldarg_0);
  4.     gen.Emit(OpCodes.Ldarg_1);
  5.     gen.Emit(OpCodes.Ldstr,pi.Name);
  6.     gen.Emit(OpCodes.Callvirt, pi.ParameterType.CreateParameterMethod());
  7.     gen.Emit(OpCodes.Stfld, field);
  8. }

We push this + container.
Push parameter name on the stack.
Emit a callvirt on the method info.
Stfld will store the result into our private field.

Testing in debug, and here we go, all our parameters are properly injected, I can see my node fully initialized.

Of course for now it does nothing. So second part: evaluate:

This is mostly as easy.

  • Push all the parameters necessary to call the static method (here i need to be careful of Getters/Setters).
  • Call the static method.
  • Push the return value in the output field.
Here we are :

Code Snippet
  1. MethodBuilder eval = tb.DefineMethod("Evaluate", MethodAttributes.Public | MethodAttributes.Virtual, CallingConventions.HasThis, null, new Type[] { });
  2. ILGenerator evalopcodes = eval.GetILGenerator();
  3. evalopcodes.Emit(OpCodes.Nop);
  4. evalopcodes.Emit(OpCodes.Ldarg_0);
  5. evalopcodes.Emit(OpCodes.Ldfld, routput);
  7. i = 0;
  8. foreach (ParameterInfo pi in prms)
  9. {
  10.     evalopcodes.Emit(OpCodes.Ldarg_0);
  11.     evalopcodes.Emit(OpCodes.Ldfld, fields[i]);
  12.     evalopcodes.Emit(OpCodes.Callvirt, pi.ParameterType.ParemeterGetterMethod());
  13.     i++;
  14. }
  16. evalopcodes.Emit(OpCodes.Call, method);
  17. evalopcodes.Emit(OpCodes.Callvirt, method.ReturnParameter.ParameterType.SourcePinSetterMethod());
  18. evalopcodes.Emit(OpCodes.Nop);
  19. evalopcodes.Emit(OpCodes.Ret);
  21. Type t = tb.CreateType();

First we push our return field first (since it needs to receive the value).

Then we push on the stack all fields (for loop).
Call the function.
Push the output result (on top of the stack) in our output pin.

Et voila.

As you can notice, code is decently verbose, but it's not too bad, and a lot of the code can be reused to build different type of nodes.

Please of course note that I can also just export this assembly as a file.

That's it for now, probably more to come !

No comments:

Post a Comment