Wednesday 8 April 2009

Re-implementing Expression for Compact Framework


In this previous post I gave an indication of how the RPC stack of protobuf-net is implemented - in particular (in .NET 3.5) with Expression - allowing:
  ProtoClient<IMyService> client = ...
double amount = client.Invoke(svc => svc.DoSomeMaths(
12, someObj.Qty));
where DoSomeMaths is a method defined on the IMyService interface. This works well, and avoids the need to do any kind of code generation, yet still have full static (compile time) checking that we're using methods that are defined on our service.

So what is the problem?

As always, there is a fly in the ointment; it transpires that the Expression API doesn't exist on Compact Framework 3.5. Perhaps Microsoft thought that it served little purpose without a matching Reflection.Emit implementation (allowing you to compile expressions to delegates) - who knows. But it is vexing that it works everywhere (including Silverlight 2.0) except here. So what can we do?

Well, pretty-much all of C# 3.0 is actually compiler magic hitting known types in the base class. For example, you can use LINQ (with delegates) with C# 3.0 targetting .NET 2.0 by declaring ExtensionAttribute and adding a pile of extension methods on IEnumerable etc (or more simply: download LINQBridge). So can we do the same here? I've previously always been dismissive of this, but it turns out that yes, we can.

What, all of Expression?

First - a caveat; I don't need all of Expression; for example, I don't need any compilation features, I don't need to be able to add things etc - all I need is:
  • a lambda that accepts parameters
    • that can invoke methods
    • that can use constant argument values
    • that can use properties and fields as values
    • that can use captured values (which are implemented as fields)
Anything more complex, and the caller can simply pre-calculate their argument value into a variable, and pass that in via a capture - i.e. this won't work:
  double amount = client.Invoke(svc => svc.DoSomeMaths(
12, someObj.Qty * 2));
but this will:
  int qty = someObj.Qty * 2;
double amount = client.Invoke(svc => svc.DoSomeMaths(
12, qty));
What is involved?

Actually, not a vast amount - but we are hampered a bit; the C# spec doesn't actually indicate what Expression features it uses, nor when. Very unusual for C#, which is usually very detailed in this sort of thing... but actually, we can get an appreciation of what we need from looking at what the compiler does behind the scenes (as discussed previously). Given the requirements above, it turns out that we need the static methods:
  • Expression.Constant
  • Expression.Parameter
  • Expression.Field
  • Expression.Property
  • Expression.MethodCall
  • Expression.Lambda<T>
and a few classes to hold the state for each of these, so that we can inspect it afterwards. Not a huge task at all.

plz send teh codez

OK, enough already... the code for this (and it isn't very much) is now committed into protobuf-net (but only applies to the CF35 build) - and is available here - at just over 250 lines (including comments), this seems a fairly pragmatic way of unifying service access over the different frameworks. Enjoy.