Do you know how .net Remoting works? It relys on a very cool feature of the .net framework: Interception.
What is Interception?
In short, you replace existing code with new code, at runtime. There are quite a lot of different solutions to replace code. The easiest solution is Inheritance. You can override virtual methods and replace its implementation with anything else. At runtime you can swap the instances as you want. The same works with Interface Dispatch*. The problem of this solution is, that you can only intercept virtual methods.
There may be the need to intercept all method calls. The best example is Remoting. You want to access an object as if it lives in the current application domain though it might be located at any other app domain which in turn may be located on another computer at the other side of the earth! Fortunately you can. The answer is Interception.
How does it work?
Everything you need is to create an instance from a RealProxy and obtain a __TransparentProxy from it. Now you can cast the __TransparentProxy to the actual type that you want to intercept and access it like the original object. Sounds easy. Of course, this approach is very easy to use and that makes it so cool. The CLR had made a big effort to have it working. Chris Brumme wirtes about it in his article about the __TransparentProxy.
You can intercept everything: Virtual methods, non-virtual methods and also field-access. Because properties and events are both a pair of two methods, you can intercept them too. The only things you can't intercept are static methods and static field-access. It wouldn't make sense anyway. Every field-access is translated to a call to System.Object's private FieldGetter and FieldSetter method. This methods use reflection to get or set a field and as you know, reflection is very slow in comparision to direct access. Though you shouldn't allow direct field access (always make fields private!), it doesn't matter.
How can we inject code?
There are two different ways to inject code. The easiest way is to use a ContextBoundObject and a ContextAttribute. There's an article at the msdn magazin about Contexts in .NET. This article desicribes very detailed how you can inject your custom code into a method call with this approach.
The second approach is to implement your own RealProxy. There is one short article about it at msdn: Extending RealProxy.
Here's a sample-implementation:
class InterceptionProxy : RealProxy
{
MarshalByRefObject target;
public InterceptionProxy(Type classToProxy, MarshalByRefObject target)
: base(classToProxy)
{
this.target = target;
}
public override IMessage Invoke(IMessage msg)
{
IMethodCallMessage callMsg = msg as IMethodCallMessage;
if (callMsg != null)
{
OnMethodCall(callMsg);
IMethodReturnMessage retMsg = InvokeMethod(callMsg);
OnMethodReturn(retMsg);
return retMsg;
}
else
{
throw new NotSupportedException();
}
}
IMethodReturnMessage InvokeMethod(IMethodCallMessage callMsg)
{
return RemotingServices.ExecuteMessage(target, callMsg);
}
void OnMethodCall(IMethodCallMessage callMsg)
{
}
void OnMethodReturn(IMethodReturnMessage retMsg)
{
}
}class Program : MarshalByRefObject
{
void Test()
{
Console.WriteLine("Test");
}
static void Main(string[] args)
{
Program program = new Program();
RealProxy proxy = new InterceptionProxy(typeof(Program), program);
Program proxiedProgram = (Program)proxy.GetTransparentProxy();
proxiedProgram.Test();
}
}
If you debug the code, you can see that proxiedProgram is a __TransparentProxy instead of a Program. Every call to the __TransparentProxy is converted to a call of our InterceptionProxy's Invoke method. In this method we can do whatever we want. We just have to return a valid return message.
In our example we're only interested in method calls. We could also handle constructor calls (IConstructionCallMessage) by applying a ProxyAttribute on a ContextBoundObject:
class InterceptionProxy : RealProxy
{
public InterceptionProxy(Type classToProxy)
: base(classToProxy)
{
}
public override IMessage Invoke(IMessage msg)
{
IConstructionCallMessage ctorMsg = msg as IConstructionCallMessage;
if (ctorMsg != null)
{
return this.InitializeServerObject(ctorMsg);
}
else
{
throw new NotSupportedException();
}
}
}
class InterceptionProxyAttribute : ProxyAttribute
{
public override MarshalByRefObject CreateInstance(Type serverType)
{
RealProxy proxy = new InterceptionProxy(serverType);
return (MarshalByRefObject)proxy.GetTransparentProxy();
}
}[InterceptionProxy]
class Program : ContextBoundObject
{
static void Main(string[] args)
{
Program program = new Program();
}
}
When a new Program is created, first the InterceptionProxyAttribute's CreateInstance method is called. This method returns a __TransparentProxy instead of the Program. Notice that we have already a proxy before the constructor is called! Now the __TransparentProxy converts the call to the constructor to a call to our InterceptionProxy's Invoke method. To dispatch the contructor call, we use the RealProxy's InitializeServerObject method. You could get the created object by using the RealProxy's protected GetUnwrappedServer method.
Be aware that future method calls to the proxied instance aren't intercepted because the InitializeServerObject method told the __TransparentProxy that the proxied object is in the current context. It's actually the __TransparentProxy's stubData that holds this information. If it matches with the current context's InternalContextID, the __TransparentProxy invokes the called method directly instead of converting it to a call to the associated RealProxy's Invoke method. Fortunatly we can manipulated the stubData by the RealProxy's static GetStubData and SetStubData methods. If we set the stubData back to an IntPtr(-1), method calls are intercepted again.
Dispatching Messages
You saw two different ways to dispatch messages till now. Constructor calls were dispatched with the RealProxy's InitializeServerObject method and method calls were dispatched with the RemotingServices.ExecuteMessage method. These methods are quite slow because they add a lot of overhead. Actually all the arguments passed in by the message's Args property are checked if they match the called method's parameters. Therefor reflection is used which is very slow. This is really annoying because we are sure that the arguments must match the method's parameters. Otherwise the caller hadn't been able to call the method because already the compiler (C# compiler and JIT compiler) had issued an error. To circumvent the checks, we would need to call the internal System.Runtime.Remoting.Messaging.StackBuilderSink's PrivateProcessMessage method. This shouldn't be a problem if our assembly runs under full trust. With some hacks you can get even faster: Instead of using the IMethodCallMessage's MethodBase property (which is only created at the first access of the property) to obtain a RuntimeMethodHandle, you can use the Message's private _methodDesc field to create a new RuntimeMethodHandle from it. Be careful: This code depends on implementation details which are subject to change. The code can brake after you install a new patch for the framework!
Here's the code:
class InterceptionProxy : RealProxy
{
delegate Object PrivateProcessMessage(
RuntimeMethodHandle md, Object[] args, Object server, int methodPtr,
bool fExecuteInContext, out Object[] outArgs);
delegate RuntimeMethodHandle GetMethodHandle(IMethodCallMessage message);
static GetMethodHandle getMethodHandle = CreateGetMethodHandle();
object target;
PrivateProcessMessage privateProcessMessage;
static GetMethodHandle CreateGetMethodHandle()
{
Type messageType = Type.GetType("System.Runtime.Remoting.Messaging.Message");
DynamicMethod dm = new DynamicMethod("", typeof(RuntimeMethodHandle),
new Type[] { typeof(IMethodCallMessage) }, typeof(InterceptionProxy), true);
ILGenerator il = dm.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Castclass, messageType);
il.Emit(OpCodes.Ldfld, messageType.GetField("_methodDesc",
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance));
il.Emit(OpCodes.Newobj, typeof(System.RuntimeMethodHandle).GetConstructor(
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
null, new Type[] { typeof(IntPtr) }, null));
il.Emit(OpCodes.Ret);
return (GetMethodHandle)dm.CreateDelegate(typeof(GetMethodHandle));
}
public InterceptionProxy(Type classToProxy, object target)
: base(classToProxy)
{
this.target = target;
Type stackBuilderSinkType = Type.GetType(
"System.Runtime.Remoting.Messaging.StackBuilderSink");
object stackBuilderSink = Activator.CreateInstance(stackBuilderSinkType, target);
privateProcessMessage = (PrivateProcessMessage)Delegate.CreateDelegate(
typeof(PrivateProcessMessage), stackBuilderSink,
stackBuilderSinkType.GetMethod("PrivateProcessMessage",
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance));
}
IMethodReturnMessage InvokeMethod(IMethodCallMessage callMsg)
{
object ret;
object[] outArgs;
try
{
ret = privateProcessMessage(getMethodHandle(callMsg),
callMsg.Args, target, 0, false, out outArgs);
}
catch(Exception ex)
{
return new ReturnMessage(ex, callMsg);
}
return new ReturnMessage(ret, outArgs, outArgs == null ? 0 : outArgs.Length,
null, callMsg);
}
// other methods omitted
}
If you don't need any details of the IMethodReturnMessage in the OnMethodReturn method, you can pass null instead of the callMsg to the constructor of the ReturnMessage to increase the performance even further. (The constructor would copy the MethodBase, along with other properties, from the callMsg to its own fields and so trigger the lazy creation of the MethodBase.)
Interception also works with interfaces and not only with childs of MarshalByRefObject. RemotingServices.ExecuteMessage required a MarshalByRefObject but our new approach doesn't require a special type.
I chose an even faster approach instead of using the StackBuilderSink's PrivateProcessMessage method. At the first call of a method, I create a DynamicMethod and cache it for future calls to the method. This approach is only faster if the same method is called a lot of times because the overhead of creating the DynamicMethod must pay off with the performance-win of the calls to the generated method.
The proxy would look like the following:
class InterceptionProxy : RealProxy
{
delegate RuntimeMethodHandle GetMethodHandle(IMethodCallMessage message);
delegate object DynamicMethodDelegate(object target, object[] args);
static GetMethodHandle getMethodHandle = CreateGetMethodHandle();
[ThreadStatic]
static Dictionary<RuntimeMethodHandle, DynamicMethodDelegate> cache;
object target;
static GetMethodHandle CreateGetMethodHandle()
{
Type messageType = Type.GetType("System.Runtime.Remoting.Messaging.Message");
DynamicMethod dm = new DynamicMethod("", typeof(RuntimeMethodHandle),
new Type[] { typeof(IMethodCallMessage) }, typeof(InterceptionProxy), true);
ILGenerator il = dm.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Castclass, messageType);
il.Emit(OpCodes.Ldfld, messageType.GetField("_methodDesc",
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance));
il.Emit(OpCodes.Newobj, typeof(System.RuntimeMethodHandle).GetConstructor(
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
null, new Type[] { typeof(IntPtr) }, null));
il.Emit(OpCodes.Ret);
return (GetMethodHandle)dm.CreateDelegate(typeof(GetMethodHandle));
}
static DynamicMethodDelegate CreateDynamicMethod(MethodInfo method)
{
ParameterInfo[] pi = method.GetParameters();
DynamicMethod dm = new DynamicMethod("", typeof(object),
new Type[] { typeof(object), typeof(object[]) },
typeof(InterceptionProxy), true);
ILGenerator il = dm.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
for (int i = 0; i < pi.Length; i++)
{
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldc_I4, i);
Type parameterType = pi[i].ParameterType;
if (parameterType.IsByRef)
{
parameterType = parameterType.GetElementType();
if (parameterType.IsValueType)
{
il.Emit(OpCodes.Ldelem_Ref);
il.Emit(OpCodes.Unbox, parameterType);
}
else
{
il.Emit(OpCodes.Ldelema, parameterType);
}
}
else
{
il.Emit(OpCodes.Ldelem_Ref);
if (parameterType.IsValueType)
{
il.Emit(OpCodes.Unbox, parameterType);
il.Emit(OpCodes.Ldobj, parameterType);
}
}
}
if ((method.IsAbstract || method.IsVirtual)
&& !method.IsFinal && !method.DeclaringType.IsSealed)
{
il.Emit(OpCodes.Callvirt, method);
}
else
{
il.Emit(OpCodes.Call, method);
}
if (method.ReturnType == typeof(void))
{
il.Emit(OpCodes.Ldnull);
}
else if (method.ReturnType.IsValueType)
{
il.Emit(OpCodes.Box, method.ReturnType);
}
il.Emit(OpCodes.Ret);
return (DynamicMethodDelegate)dm.CreateDelegate(typeof(DynamicMethodDelegate));
}
public InterceptionProxy(Type classToProxy, object target)
: base(classToProxy)
{
this.target = target;
}
IMethodReturnMessage InvokeMethod(IMethodCallMessage callMsg)
{
DynamicMethodDelegate method;
if (cache == null)
{
cache = new Dictionary<RuntimeMethodHandle, DynamicMethodDelegate>();
}
if (!cache.TryGetValue(getMethodHandle(callMsg), out method))
{
method = CreateDynamicMethod((MethodInfo)callMsg.MethodBase);
cache.Add(callMsg.MethodBase.MethodHandle, method);
}
object ret;
object[] args = callMsg.Args;
try
{
ret = method(target, args);
}
catch(Exception ex)
{
return new ReturnMessage(ex, callMsg);
}
return new ReturnMessage(ret, args, args.Length, null, callMsg);
}
// other methods omitted
}
I made the cache ThreadStatic to avoid the synchronization overhead for a shared dictionary.
There's one final word to say: Be careful with the logical call context! After our RealProxy's Invoke method returns, the current logical call context is set to the one specified in the return message. When we create the return message, we pass in null for the logical call context. This stores the current logical call context in the return message. When the current logical call context is changed between the creation of the return message and the return of the Invoke method (e.g. because of remote calls in the OnMethodReturn method), we will lose this changes.
Interception is a quite complex topic. Hopefully my research on Interception in the .net framework is useful for some of you. It took me quite a lot of time to figure all that stuff out. You may have a lot of questions to it. Just post a comment!
* Vance Morrison has written a cool article about how Interface Dispatch is implemented in the framework - it's quite complex.