Thursday 6 March 2014

IL, connecting people...

After a quite good amount of work on the User interface (more post to follow on this part), I decided to go back into the inner tech of things, and try to find some different approaches.

One important thing when you are a user and you use graphical programming, you need to connect elements. With class (reference types), this is rather straightforward, you use Type.IsAssignableFrom and you're more or less done.

Now let's have a bit more interest into value types.

In that case the above (of course) doesn't work. let's make a very simple Pin model:

Code Snippet
  1. public class InputPin<T>
  2. {
  3.     private T data;
  4.  
  5.     public T Data
  6.     {
  7.         get { return this.data; }
  8.         internal set
  9.         {
  10.             { this.data = value; }
  11.         }
  12.     }
  13. }
  14.  
  15. public class OutputPin<T>
  16. {
  17.     public T Data { get; set; }
  18. }

As you see, nothing fancy.

Now let's say we have those :

Code Snippet
  1. var input = new InputPin<double>();
  2. var output = new OutputPin<float>();

They of course can't natively connect (Type.IsAssignable) with not work. But in c#:

Code Snippet
  1. float f = 150.0f;
  2. double d = f;

This is perfectly valid, so there must be a way.

So how to handle connections in that case?
We have a few cases that we can thing of:

  • This is a native type
  • The type already implements either implicit or explicit
  • We want to do it ourselves and provide our own.
I'll start by option 2 (will explain the reason below).

By reflection, to check if a type is assignable to another one, this is what we do instead:

Code Snippet
  1. private MethodInfo FindMethod(Type t)
  2. {
  3.     return t.GetMethods().Where(
  4.         m => m.Name == "op_Implicit"
  5.         && m.ReturnType == typeof(TTarget)
  6.         && m.GetParameters().Count() == 1
  7.         && m.GetParameters()[0].ParameterType == t).FirstOrDefault();
  8. }

If we have a MethodInfo, we are able to invoke this and convert our type.

So after we can easily build a small IL Generator that builds either a Func or a small class wrapping instances. In each case this is simple:

Code Snippet
  1. public Action<TSourceType, TDestType> CreateAction()
  2. {
  3.     Type[] helloArgs = { typeof(TSourceType), typeof(TDestType) };
  4.     DynamicMethod hello = new DynamicMethod(Guid.NewGuid().ToString(), typeof(void), helloArgs, typeof(TSourceType).Module);
  5.  
  6.     ILGenerator il = hello.GetILGenerator();
  7.     il.Emit(OpCodes.Ldarg_1);
  8.     il.Emit(OpCodes.Ldarg_0);
  9.  
  10.     this.EmitGetter(il);
  11.     this.emit(il);
  12.  
  13.     this.EmitSetter(il);
  14.     il.Emit(OpCodes.Ret);
  15.  
  16.     Type t = typeof(Action<TSourceType, TDestType>);
  17.  
  18.     Action<TSourceType, TDestType> res = (Action<TSourceType, TDestType>)hello.CreateDelegate(t);
  19.     return res;
  20. }

EmitGetter and Setter just take care of automatic property/field to choose a CallVirt/LdFld operator.

Now from that you can simply do :

Code Snippet
  1. var input = new InputPin<double>();
  2. var output = new OutputPin<float>();
  3. output.Data = 154.0f;
  4.  
  5. var dbl = new DoubleNativeTypeConverter();
  6. var gen = dbl.CreateGenerator<OutputPin<float>, InputPin<double>>(
  7.     Select<OutputPin<float>, float>(tp => tp.Data),
  8.     Select<InputPin<double>, double>(t => t.Data)
  9.  );
  10.  
  11. var act = gen.CreateAction();
  12.  
  13. act(output, input);


The BIG difference, you can do all this at runtime, with reflection. No need to build two million converters.

Of course an acute reader would notice, I have : DoubleNativeTypeConverter

Yes basic primitive types (as of double, int, float...) don't expose this to reflection, so in that case you have to implement this manually (doing this for 20 types instead of thousands is a trade off I happily take, and in those cases you can simply use the Conv_(Type) opcode and just build a list of supported types, not a biggie.

Please note that in this case I emit a Func (eg: a dynamic method), which in some cases is not always what you want (If your converter needs to hold resources for example, you'd need to generate a class with IDisposable, wich would just have an "update function'". You could also simply register this class an write it by hand. In neither case it's an issue.

Possibilities are a bit endless, more on that in the next post (which might be a little while due to my project workload)...