| I l@ve RuBoard |
|
Proxiable TypesThe previous section ended by looking at the RemotingServices.ExecuteMessage method. Careful readers may have noticed the introduction of a new type that was not explained. This type was System.MarshalByRefObject. It is no coincidence that the first type used to demonstrate proxies was an interface. Interfaces have one characteristic that makes them especially proxy-friendly: Interfaces, unlike classes, always imply a virtual method dispatch. For that reason, the JIT compiler will never inline a method call through an interface-based reference. The same cannot be said for classes. To understand the issues related to using transparent proxies with classes, consider the following class definition:
public sealed class Bob {
int x;
public void DoIt(int n) { x += n; }
}
In all likelihood, the JIT compiler will inline calls to Bob.DoIt and no method invocation will actually happen. Rather, the JIT compiler will simply insert the code for incrementing the Bob.x field wherever a call site to Bob.DoIt appears. This inline expansion bypasses the transparent proxy's attempts to intercept the call. To that end, if one were to pass typeof(Bob) to the RealProxy constructor, an exception would be thrown because the proxy infrastructure cannot guarantee that it can intercept all calls through references of type Bob. This is not a problem for interfaces because the JIT compiler never inlines interface-based method calls. However, it would be useful in many circumstances to allow classes to be used for transparent proxies. Enter System.MarshalByRefObject. The System.MarshalByRefObject type is misnamed. Its primary function is to suppress inlining by the JIT compiler, thereby allowing the transparent proxy to do its magic. When the JIT compiler tries to inline a method, it first checks to see whether the method's type derives from System.MarshalByRefObject. If it does, then no inlining will take place. Moreover, any accesses to the type's instance fields will go through two little-known methods on System.Object: the FieldGetter and FieldSetter methods. This allows the transparent proxy to expose the public fields of a class and still be notified of their access. Instances of classes that derive from System.MarshalByRefObject may or may not have a proxy associated with them. In particular, the this reference inside a method will never be a proxy; rather, it will always be a raw reference to the object. It is possible to indicate that a proxy is always needed to access the object. To do so, one derives from System.Context BoundObject. The System.ContextBoundObject method derives from System.MarshalByRefObject and informs the runtime to always insert a transparent proxy in front of the object. For example, consider the following three types:
using System;
public class Bob {}
public class Steve : MarshalByRefObject {}
public class George : ContextBoundObject {}
Based on the descriptions of MarshalByRefObject and ContextBoundObject, one can make the following assertions:
using System.Runtime.Remoting;
public sealed class Utils {
public static void Proof(Bob b, Steve s, George g) {
// g is always a TP
Debug.Assert(RemotingServices.IsTransparentProxy(g));
// b is never a TP
Debug.Assert(!RemotingServices.IsTransparentProxy(b));
// s may or may not be a TP
bool whoKnows = RemotingServices.IsTransparentProxy(s);
}
}
George derives from ContextBoundObject and therefore all non-null references of type George by definition refer to a transparent proxy (including the this reference!). Because Bob does not derive from Marshal ByRefObject, references of type Bob never refer to a proxy, and, for that reason, the JIT compiler may opt to inline methods through references of type Bob. In contrast, Steve derives from MarshalByRefObject, and therefore references of type Steve may refer to a transparent proxy. This suppresses the JIT compiler from inlining method calls through references of type Steve. For a class to be proxiable, it must derive from MarshalByRefObject. Armed with that knowledge, one can easily write the no-op proxy as follows:
public class NoOpProxy : RealProxy {
readonly MarshalByRefObject target;
public NoOpProxy(MarshalByRefObject target)
: base(target.GetType())
{
this.target = target;
}
public override IMessage Invoke(IMessage request) {
IMethodCallMessage call = (IMethodCallMessage)request;
return RemotingServices.ExecuteMessage(target, call);
}
}
This proxy does nothing other than sit between the transparent proxy and the target object and forward all method invocations through a stack builder sink. The whole purpose of supporting interception is to enable pre- and postprocessing of method calls, typically in type-independent ways. For the remainder of this chapter, we will use the example of an interceptor that boosts thread priority prior to invoking the target method and then restores the priority prior to returning control to the caller. The following proxy implementation implements that aspect using the mechanisms described so far in this chapter:
public class PriorityProxy : RealProxy {
readonly MarshalByRefObject target;
readonly ThreadPriority level;
public PriorityProxy(MarshalByRefObject target,
Type type,
ThreadPriority level)
: base(type)
{ this.target = target; this.level = level; }
public override IMessage Invoke(IMessage request) {
IMethodCallMessage call = (IMethodCallMessage)request;
// step 1 : adjust priority
Thread here = Thread.CurrentThread;
ThreadPriority old = here.Priority;
here.Priority = level;
// step 2 : forward call
IMessage response
= RemotingServices.ExecuteMessage(target, call);
// step 3 : restore old priority
here.Priority = old;
// step 4 : return response message to TP
return response;
}
}
Now one simply needs a factory method to insert the interceptor transparently:
public class MyCalc : MarshalByRefObject, ICalculator {
public static MyCalc Create(ThreadPriority level) {
MyCalc target = new MyCalc();
PriorityProxy rp = new PriorityProxy(target,
typeof(MyCalc), level);
return (MyCalc)rp.GetTransparentProxy();
}
private MyCalc(){}
public double Add(double x, double y) {return x+y;}
public double Multiply(double x, double y) {return x*y;}
}
Note that the factory method (Create) injects the proxy between the client and the newly minted target object. The MyCalc methods do not need to deal with thread priority, and the client need only create objects using this factory method. One guarantees the use of the factory method by making the constructor private. The previous example provided thread priority adjustment via transparent interception, which removed the priority code from the client and the target class. However, the injection of this interceptor was far from transparent. Rather, the class implementer had to write a special factory method, and the client had to use that factory method. This resulted in slightly more work for the class implementer, but, worse, it led to a some what unnatural usage model for the client. Of course, had the developer given the client access to the default constructor, then the new object would not have had the benefits of our interceptor. If our target type had derived from ContextBoundObject rather than MarshalByRefObject, we could have exploited the fact that the CLR handles newobj requests against ContextBoundObject-derived types differently from the way it handles normal (non-context-bound) types. When the CLR encounters a newobj CIL opcode against a type that derives from ContextBoundObject, rather than just allocate memory, the CLR goes through a fairly sophisticated dance that allows third-party extensions to become involved with the instantiation of the new object. This overall process is called object activation. Object activation allows us to inject our custom proxy transparently, while allowing clients to use the far more natural new keyword in their language of choice. Before allocating the memory for the new context-bound object, the CLR looks at the custom attributes that have been applied to the target type being instantiated. In particular, the CLR is looking for custom attributes that implement the System.Runtime.Remoting.Contexts.IContextAttribute interface. The CLR gives each of these special attributes (commonly known as context attributes) the opportunity to process the newobj request; however, because newobj looks only for context attributes when operating on types that derive from ContextBoundObject, applying a context attribute to a non-ContextBoundObject type has no meaningful effect. Context attributes are discussed in all their glory later in this chapter. For now, we will focus on one predefined context attribute: System.Runtime.Remoting.Proxies.ProxyAttribute. The ProxyAttribute type hides much of the complexity of implementing a context attribute. Essentially, the ProxyAttribute type refactors IContextAttribute into two simple virtual methods, only one of which is needed to inject our custom proxy:
namespace System.Runtime.Remoting.Proxies {
public class ProxyAttribute
: Attribute,
IContextAttribute
{
public virtual MarshalByRefObject CreateInstance(Type t);
// remaining members elided for clarity
}
}
When a newobj is executed against a type bearing a ProxyAttribute, the ProxyAttribute will call its CreateInstance virtual method. This method is expected to return an uninitialized instance of the presented type. Note the term uninitialized. Because this is happening during the activation phase, you must not call the constructor of the object you return in your overridden CreateInstance method. Rather, the CLR will invoke the appropriate constructor against whatever gets returned by the CreateInstance method. The ProxyAttribute.CreateInstance method is marked as virtual. The default implementation simply allocates an uninitialized instance of the requested type. However, because the method is marked virtual, we now have an opportunity to get into the activation process without the complexity of writing our own context attribute. To inject our custom attribute, our overridden implementation of CreateInstance will look strikingly like the factory method implemented on the original MyCalc:
using System;
using System.Runtime.Remoting.Proxies;
[ AttributeUsage(AttributeTargets.Class) ]
public class PriorityProxyAttribute : ProxyAttribute
{
ThreadPriority level;
public PriorityProxyAttribute(ThreadPriority level)
{ this.level = level; }
public override
MarshalByRefObject CreateInstance(Type t){
// note that we delegate to our base to get an
// uninitialized instance!
MarshalByRefObject target = base.CreateInstance(t);
RealProxy pp = new PriorityProxy(target, t, level);
return (MarshalByRefObject)pp.GetTransparentProxy();
}
}
Pay close attention to the first line of the CreateInstance method. Because we are in the middle of a newobj instruction, we can't use the new operator or any of the other facilities for creating a new object because they would trigger yet another call to our CreateInstance method (which eventually would result in stack overflow). By calling CreateInstance on our base type, we get back an uninitialized instance of the target type, which is exactly what we need. Technically, because the target type derives from ContextBoundObject, we are actually holding a transparent proxy to the target object. This is illustrated in Figure 7.6. Figure 7.6. Result of ProxyAttribute.CreateInstance
Readers who have been around object-oriented programming for any length of time are likely concerned about the target object at this point. By calling ProxyAttribute.CreateInstance, we were able to acquire a reference to an object whose constructor has never executed. Your cause for concern is justified. If we were to do anything meaningful with the object, the results would be undefined. However, all we are doing is caching the reference inside our custom proxy—no more, no less. Fortunately, as soon as we return our custom proxy from our overridden CreateInstance method, the CLR will enter phase 2 of activation. In this phase, the constructor will be invoked; however, it will be invoked through our custom proxy, giving us the opportunity (and the responsibility) of intercepting the constructor invocation. As with any other method call, constructor invocations against ContextBoundObject-derived types are represented as message exchanges. In fact messages for constructor calls implement an additional interface (IConstructionCallMessage, IConstructionReturnMessage) so that one can easily detect that the call is not just another method call. Implementing custom proxies that handle constructor calls is somewhat tricky. For one thing, we cannot use RemotingServices.ExecuteMessage to forward the call. Fortunately, the RealProxy base type provides a method called InitializeServerObject that will do it for us. The InitializeServerObject will return a response message that our proxy's Invoke method could in fact return; however, this message contains the unwrapped object reference. To ensure that the creator gets a reference to our transparent proxy, we will need to construct a new response message that contains our custom proxy and not the "raw" object we are intercepting calls for. Ideally, we could just create a new ReturnMessage that contains our object reference. Unfortunately, we can't. Instead, we must use the EnterpriseServicesHelper.CreateConstructionReturnMessage static method. The following code shows the modifications needed for our Invoke routine to properly handle construction calls. Note that all of the special-case handling of the construction call takes place in step 2 of the method:
public override IMessage Invoke(IMessage request) {
// step 1 : adjust priority
Thread here = Thread.CurrentThread;
ThreadPriority old = here.Priority;
here.Priority = level;
// step 2 : forward call
IMessage response = null;
IMethodCallMessage call = (IMethodCallMessage)request;
IConstructionCallMessage ctor
= call as IConstructionCallMessage;
if (ctor != null) {
// we are holding a TP, so grab its RP
RealProxy defaultProxy
= RemotingServices.GetRealProxy(target);
// ask intermediate RP to invoke constructor
defaultProxy.InitializeServerObject(ctor);
// get OUR TP
MarshalByRefObject tp =
(MarshalByRefObject)this.GetTransparentProxy();
// return a message containing our TP as the result of the
// constructor call
response = EnterpriseServicesHelper
.CreateConstructionReturnMessage(ctor, tp);
}
else
response = RemotingServices.ExecuteMessage(target, call);
// step 3 : restore old priority
here.Priority = old;
// step 4 : return response message to TP
return response;
}
It is worth mentioning that only custom proxy implementers need to write this code. When all of this code is in place, using the code becomes as simple as applying the attribute:
[ PriorityProxy(ThreadPriority.Highest) ]
public class MyCalc3 : ContextBoundObject, ICalculator {
public double Add(double x, double y) {return x+y;}
public double Multiply(double x, double y) {return x*y;}
}
Yes, the developer of the custom proxy had to go through some interesting hoops to get the proxy to work; however, users of the proxy now have an extremely simple usage model. No weird factory methods are required. Simply calling new against MyCalc3 will trigger all of the code just described. |
| I l@ve RuBoard |
|