[Steve’s note: I wrote this post a year or so ago and never had posted it. It’s still relevant today, but I don’t recommend using shady things like this in production code without understanding the implications.]
A side project I worked on recently involved building an abstraction layer around DB access, specifically calling stored procedures. My goal was to reduce code redundancy and add type-safety to the parameters of stored proc calls. The road I went down involved creating an interface for a group of stored procs, with the interface methods mapping to the stored procs. I might resurrect this as an example and post the code on github, because it actually worked out really well.
What's a RealProxy?
The .NET remoting infrastructure is built around 3 key classes:
- This is the "opt-in" marker for by-ref object marshaling.
- This is the base type for method/constructor call dispatch over remoting. The CLR has special knowledge of this class, and this is the managed entry/exit point for calls dispatched through a TransparentProxy. Calls enter and leave managed code via RealProxy.PrivateInvoke.
- The other piece of the proxy pair. The CLR has special knowledge of this class in terms of type casting and method invocation.
Implementing RealProxy only requires you to implement Invoke(IMessage msg) and do whatever you want inside it. In my above example, I took the method name and parameters and handed them off to a SqlCommand and ran ExecuteReader, ExecuteNonQuery, etc. The implementation details for the synchronous case aren't that exciting.
One big feature I wanted to implement was asynchronously executing SQL via BeginInvoke/EndInvoke on the proxy methods. Unfortunately, Microsoft made this impossible to do without resorting to some sketchy reflection. Inside RealProxy.PrivateInvoke, there's a block of code that looks like this:
if (!this.IsRemotingProxy() && ((msgFlags & 1) == 1))
Message m = reqMsg as Message;
AsyncResult ret = new AsyncResult(m);
retMsg = new ReturnMessage(ret, null, 0, null, m);
There are a few things going on here
- The constants for msgFlags are defined in System.Runtime.Remoting.Messaging.Message. We'll need this value later on. The interesting values are:
- BeginAsync (eg a call to BeginInvoke) = 1
- EndAsync (EndInvoke) = 2
- OneWay (eg a method with a [OneWay] attribute on it) = 8
- The consequence of the block of code above is that the remoting infrastructure will run BeginInvoke synchronously for any RealProxy that isn't a RemotingProxy. I'll get into details on this in a bit.
- IsRemotingProxy checks the _flags field on RealProxy. This is set in the RealProxy constructor if the type is RemotingProxy. We can't inherit from RemotingProxy (it's internal), but we can use reflection in our own constructor to set this to RealProxyFlags.RemotingProxy (1). Once we do this, a "normal" Invoke implementation won't work correctly for async invocations.
How proxy invocation works
So back to msgFlags and how asynchronous method invocation works with the remoting infrastructure. As I mentioned above, there's two possible code paths for an async invoke on a RealProxy, the first is the "normal" code path that you get when you invoke a delegate bound to a non-RemotingProxy instance. In this case, the code path looks like this:
- Native CLR
- RealProxy.PrivateInvoke, msgFlags = 1 (BeginAsync)
- PrivateInvoke calls our Invoke(...)
- Since our Invoke isn't aware of anything async (in fact, with the public API there's no way to know), it runs synchronously.
- PrivateInvoke sets up an AsyncResult instance with the result from Invoke(...) and returns this. Note, at this point, everything has run synchronously, our BeginInvoke was exactly the same as an Invoke, except the last step of returning an IAsyncResult. At this point, all our work is done.
- We call someDelegate.EndInvoke(iar)
- RealProxy.PrivateInvoke, msgFlags = 2 (EndAsync)
- PrivateInvoke calls EndInvokeHelper, which takes care of returning the return value or throwing an exception if one occurred.
The pro of this code path is that you (the RealProxy implementer) don't need to worry about the async code path, in fact, you can't. The con is that there's no way to get a truly async invocation with this.
Now, the alternate code path is "enabled" by telling the infrastructure that we're a RemotingProxy. This code path looks roughly like this
- Native CLR
- RealProxy.PrivateInvoke, msgFlags = 1
- PrivateInvoke calls our Invoke, this time however, it's expecting an IAsyncResult in return.
- We set up whatever async stuff we want to do here and start it.
- FUN FACT: You can actually return whatever you want here! The CLR is missing the type check to make sure your ReturnMessage actually returns an IAsyncResult. The resulting chaos that ensues from not returning the correct type is entertaining. I made a post about it on StackOverflow awhile ago here.
- PrivateInvoke returns, we have an IAsyncResult
- We call someDelegate.EndInvoke(iar)
- PrivateInvoke calls Invoke, msgFlags = 2
- Invoke handles getting the correct return value or throwing an exception if one occurred. Possibly through RealProxy.EndInvokeHelper as above.
As you can see here, there's a lot more burden on us to "get things right." with the implementation. We now need to handle synthesizing an IAsyncResult, invoking a callback (if passed in), raising an exception in the EndInvoke (if thrown), and getting the value back. Also, since none of this is exposed publically via the API, it makes things even more difficult because we now need to rely on reflection. I'm going to drill into step 4 of the second code path and explain how it works.
Implementing an async-aware Invoke
The first problem of implementing an async-aware Invoke is getting the msgFlags. This is the core of Invoke, and we'll be using it to branch to synchronous/BeginInvoke/EndInvoke/OneWay code paths. Getting the value is pretty easy, looking at System.Runtime.Remoting.Messaging.Message, you can see there's a method called GetCallType. Unfortunately this isn't exposed in any of the public interfaces Message implements, and Message itself is internal, so we need to use reflection to call it. (int)msg.GetType().GetMethod("GetCallType").Invoke(msg, null) will do the trick.
At this point, we need to branch for a few conditions. The 4 main cases are: Begin, End, OneWay, Synchronous. The simplest way is to create a method for each one and call them in Invoke depending on call type. Begin is the most interesting case because it requires the most work, so I'm going to focus on that.
BeginInvoke is responsible for 3 main tasks: creating the IAsyncResult to return, starting the async work, and handling the task completion. It turns out the framework exposes a class System.Runtime.Remoting.Messaging.AsyncResult that wraps a lot of the more complicated async completion logic. For example, the message we get in Invoke doesn't contain the last two "extra" parameters to BeginInvoke, the AsyncCallback delegate and state (it accomplishes this through Message.GetAsyncBeginInfo.) AsyncResult will get these for us, and once we give the AsyncResult the IMessage result, it'll handle invoking the callback if needed.
I’ll write more about EndInvoke at a later point, but it’s a fairly simple implementation.