CATEGORII DOCUMENTE |
Asp | Autocad | C | Dot net | Excel | Fox pro | Html | Java |
Linux | Mathcad | Photoshop | Php | Sql | Visual studio | Windows | Xml |
Chapter 6 looked at various facilities for invoking methods. However, once the method has been invoked, control is passed to the target object's method and the fun part is over. This chapter looks at the various facilities provided by the CLR for getting between the caller and the callee and intercepting the invocation process.
It is important to note that parts of the method invocation architecture described in this chapter are likely to evolve considerably in future versions of the CLR. That stated, many of the the concepts and techniques described in this chapter will survive well into the future. The discussion of context in particular is presented here more for completeness than as a recommendation to the reader.
Most ideas in software engineering are focused on managing complexity. Structured programming attempted to reduce complexity through coarse-grained partitioning of code and design. Object-oriented programming attempted to reduce complexity by building abstractions that marry state and behavior. Component software attempted to reduce complexity by partitioning applications based on abstract interfaces or protocols. The dream of component software was a world in which components could be assembled by less-skilled programmers using higher-level languages and tools. This, of course, assumes a world in which the problem domain can be neatly factored into discrete components that interact via simple method invocation.
The basic premise of component software ignores the fact that certain aspects of a program tend to permeate all parts of an application. Security is one such aspect. So is thread management. So is concurrency control. The list goes on.
Invariably, an application tends to become polluted with tiny snippets of code to deal with those aspects of the program that are not central to the problem domain. More often than not, such aspects tend to cross problem domains and beg for a reusable solution. Providing reuse mechanisms for these types of problems is the focus of aspect-oriented programming (AOP), a term coined in 1997 by Gregor Kiczales, then of Xerox PARC.
AOP focuses on providing mechanisms for factoring out pieces of the application that are not germane to the problem domain based on the principle of separation of concerns. This has two benefits. For one thing, the application code will no longer be cluttered with 'plumbing' code that has no bearing on the problem at hand. The second benefit is that by factoring out aspects of the code that cross problem domains, one can conceivably reuse those solutions in other applications.
Microsoft Transaction Server (MTS) was arguably the first broadly adopted application of AOP. MTS provided an attribute mechanism that lets one express aspects of the program, such as transactioning and security, outside the normal code stream. MTS implemented these aspects through interception. Specifically, MTS inserted itself between the caller and the component and preprocessed or postprocessed every method call into the component. During the pre- and postprocessing, the MTS executive would manage transactions, handle security checks, and deal with object activation. All of this required no explicit coding on the part of the component developer. Although transactions in particular tended to impact the overall design of the component, there was no explicit code required to manage the transactions. Rather, the MTS executive would handle all of this quietly behind the scenes.
Building MTS-style interception plumbing was extremely difficult in the COM era. One reason was the lack of high-fidelity extensible metadata. That obviously is no longer a problem in the CLR-based world. The other problem, however, was dealing with the peculiarities of IA-32 call stacks in order to transparently inject one's code without stack corruption. This invariably required resorting to IA-32 assembly language in order to keep the stack from melting. One of the primary goals of the CLR architects was to provide a general-purpose interception mechanism that would allow one to inject user-defined aspects without resorting to hideous low-level coding techniques. The most fundamental concept of the CLR's interception mechanism is that calls can be viewed as message exchanges.
The CLR provides a rich architecture for modeling method invocation as an exchange of messages. This architecture is useful for building AOP-style interception. This architecture is useful for building RPC-style communication mechanisms. This architecture is also useful for handling asynchronous invocation and, in fact, it is used internally by the asynchronous method call facilities described in Chapter 6. The key to understanding this architecture is to reexamine what a method actually does. Ultimately, a method is simply a transformation of memory on the stack. Parenthetically, functional programming advocates would argue that this is all that a method is. The caller forms a call stack by pushing the appropriate parameters onto the stack in a format that the method has specified. After control is passed to the method, the method's primary job is to process the parameters passed on the stack and rewrite the stack to indicate the results of the processing. Figure 7.1 illustrates this process.
The CLR allows one to model this transformation of stack frames in terms of message exchanges. In this message exchange model, a method invocation has two messages: one representing the invocation request, and another representing the result of the invocation. To make this accessible programmatically, the CLR models each of these messages as an object that implements the generic System.Runtime.Remoting.Messaging.IMessage interface. As shown in Figure 7.2, IMessage acts as a base interface to several other interfaces in the suite. The most interesting of these interfaces is IMethodMessage:
using System.Reflection;
Notice that in addition to providing access to the method arguments, the IMethodMessage interface makes the metadata for the method available via the MethodBase property.
The advantage to having the stack frame exposed via this generic interface is that it allows one to access the contents of the stack without low-level knowledge of the stack frame layout. For example, consider the following code:
static void WireTap(IMethodMessage msg) >', call.MethodName);Suppose that this routine were presented the message that corresponds to the following invocation:
int b = 2;The following would be displayed:
<MyMethod>The power of this mechanism is that the WireTap method did not need a priori knowledge of the signature of MethodName. This is because the IMethodMessage interface virtualizes the call stack into a reasonably programmable abstraction.
The parameters of a method invocation are accessible via the IMethodMessage.GetArg method just demonstrated. The CLR also defines two strongly typed interfaces that are specific to the request and response messages:
using System.Reflection;Note that the response message (IMethodReturnMessage) not only has methods that interrogate the output parameters but also provides access to the typed return value of the method via the ReturnValue property. Figure 7.3 shows the relationship between these two interfaces and a method invocation.
It is important to note that at the messaging level, a method invocation that results in abnormal termination simply generates a different response message. In either the normal or the abnormal termination case, there is a message that conveys the results of the invocation. However, a method invocation that results in abnormal termination will not have a valid ReturnValue property. Rather, the exception object that signals the error condition is made available via the Exception property.
At some point during message processing, someone needs to transform the request message into a response message. The CLR provides a concrete class, System.Runtime.Remoting.Messaging.Return Message, for exactly this purpose. The ReturnMessage class implements the IMethodReturnMessage interface and supports two constructors: one for indicating normal termination, and one for indicating abnormal termination.
namespace System.Runtime.Remoting.MessagingNote that both constructors accept a request message as the final parameter. This allows the ReturnMessage object to recover the metadata for the method being invoked.
At this point in the discussion, an example might be in order. First, consider the following simple interface:
public interface ICalculatorUltimately, each of these method definitions corresponds to a potential message exchange. For a moment, imagine that the CLR provided a way to encode a stack frame into a request message. Given this piece of CLR-based plumbing, the following code would be a reasonable implementation of this interface:
public static IMethodReturnMessageNote that the first two branches of the switch statement extract the input parameters from the request message and cache them in local variables. After the result of the method is known, the CLR constructs a new ReturnMessage object, passing the typed return value as the first constructor parameter. In the third branch of the switch statement, one indicates an abnormal termination by using the ReturnMessage constructor that accepts a System.Exception as its first parameter. By returning a valid message containing an exception, the processing code is indicating that there was no problem processing the message; rather, the processing correctly produced an exception as the result. Had there been a problem processing the message, then it would have been appropriate for the processing code to explicitly throw an exception because processing could not take place.
Although the message can be processed without a target object, the normal usage of the messaging facility is to ultimately forward the call to an actual object. This will be described in detail in the next section.
The previous section ended by asking the reader to imagine a CLR-provided facility for creating request messages. Such a facility does exist, and the CLR exposes it to programmers via the transparent proxy mechanism.
A transparent proxy (TP) is a special kind of object that is created by the CLR and exists solely to convert method calls into message exchanges. A transparent proxy is always affiliated with a buddy object known as the real proxy (RP). There is a one-to-one relationship between a transparent proxy object and a real proxy object. This relationship is shown in Figure 7.4. The real proxy is an instance of a type that derives from the System.Runtime.Remoting.Proxies.RealProxy abstract base type. One can acquire the transparent proxy from a real proxy by calling the RealProxy.GetTransparentProxy method. One can go the other way by calling the GetRealProxy static method on the System.Runtime.Remoting.RemotingServices class. To that end, one can call the RemotingServices.IsTransparentProxy static method to test an arbitrary object reference to determine whether it points to a transparent proxy or a 'real object.'
The transparent proxy object ultimately must act as if it is an instance of a particular type. To that end, the constructor of the RealProxy type requires the derived type to pass a System.Type object that the CLR will use to determine the effective type of the transparent proxy object. When an isinst or a castclass CIL opcode operates on a transparent proxy, the CLR reroutes the call to simply interrogate the real proxy's System.Type object:
// pseudo-code for isinst, refined versionGiven this implementation, now consider the following real proxy implementation:
public class MyProxy : RealProxyGiven this proxy implementation, the following code would trigger the isinst implementation just described:
MyProxy rp = new MyProxy();Because the MyProxy constructor passed the System.Type object for ICalculator to the RealProxy constructor, the GetProxiedType method will return a type object that indicates true for compatibility with ICalculator.
A real proxy implementation can override the behavior of isinst and castclass by implementing the IRemotingTypeInfo interface. This interface has two members, the main member being the CanCastTo method:
namespace System.Runtime.RemotingBoth of the CLR's implementations of isinst and castclass look for this interface when a transparent proxy is in use:
// pseudo-code for isinstWith this refined implementation in place, now consider the following real proxy implementation:
public class MyProxy : RealProxy, IRemotingTypeInfoAssuming this implementation of IRemotingTypeInfo.CanCastTo, all casts on a MyProxy's transparent proxy will succeed no matter what the requested type is. More exotic implementations would likely look at the requested type and use some reasonable algorithm for deciding whether or not to fail the cast.
After a program acquires a reference to a transparent proxy, it is highly likely that the program will make a method call against the proxy object. When the program makes a method call against a transparent proxy object, the CLR creates a new message object that represents the invocation. The CLR then passes this message to the real proxy object for processing. One expects the real proxy object to produce a second message that represents the results of the invocation. The transparent proxy then uses this second message to transform the call stack, which transparently conveys the results to the caller. If the response message returned by the real proxy contains an exception, it is the job of the transparent proxy to rethrow this exception, again transparently conveying the results to the caller.
The message exchange between the transparent proxy and the real proxy takes place via the real proxy's Invoke method. The Invoke method accepts a request message as its sole parameter and returns a response message that conveys the results of the invocation:
public abstract IMessage Invoke(IMessage request);Note that the Invoke message is marked abstract in the RealProxy type. It is the job of the derived type to implement this method and provide a meaningful implementation that produces a response message. The ProcessMessage routine shown in the previous section is one reasonable implementation of this processing.
Method invocations against a transparent proxy object cause a transition from the world of stack-based processing to the world of message-based processing. The real proxy's Invoke implementation may forward the message to any number of processing nodes prior to returning the response message. However, it is highly likely that the final processing node will ultimately need to convert the request message back into a stack frame in order to forward the call to an actual object. The CLR provides a piece of plumbing to perform this translation generically. This piece of plumbing is called the stack builder sink and is exposed programmatically via the RemotingServices.ExecuteMessage static method:
namespace System.Runtime.RemotingThe ExecuteMessage method accepts two parameters: a message that corresponds to a method request, and a reference to the object that will be the target of the invocation. The ExecuteMessage method simply creates a stack builder sink for the target object and forwards the message to the sink. The stack builder sink then does the low-level stack manipulation and invokes the actual method. When the method terminates, the stack builder sink will translate the resultant stack frame into a response message, which the CLR then returns as the result of the ExecuteMessage call.
Figure 7.5 shows the relationship between the transparent proxy and the stack builder sink. Note that the stack builder sink transitions from the world of message-based processing to the world of stack-based processing, effectively reversing the transition performed by the transparent proxy.
The 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 BobIn 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;Based on the descriptions of MarshalByRefObject and ContextBoundObject, one can make the following assertions:
using System.Runtime.Remoting;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 : RealProxyThis 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 : RealProxyNow one simply needs a factory method to insert the interceptor transparently:
public class MyCalc : MarshalByRefObject, ICalculatorNote 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.ProxiesWhen 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;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.
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)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) ]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.
The previous section illustrated how a custom proxy can pre- and postprocess method calls simply by getting between the transparent proxy and the stack builder sink. What if one wanted to inject more than one stage of pre- and postprocessing? Yes, one could simply layer proxy upon proxy; however, redundant stack-to-message transitions would occur at each stage, resulting in undue performance costs. A far preferable approach is to chain together processing stages at the message level, thus paying the cost of stack-to-message transition only once per call and not once per stage per call. This technique is based on message sinks. To facilitate this in a generic manner, the CLR defines the System.Runtime.Remoting.Messaging.IMessageSink interface:
namespace System.Runtime.Remoting.Messaging {Note that the NextSink property models the message sink as a link in a chain. In most scenarios, the last link in the chain will be a stack builder sink, which implements IMessageSink. One expects message sinks to hold a reference to the next downstream link in the chain and return that reference in their NextSink implementation. More importantly, when an incoming method call occurs, the message sink's SyncProcessMessage method will be called. Rather than call RemotingServices.ExecuteMessage to forward the call, the message sink simply calls SyncProcessMessage on the next downstream link in the chain. Because the penultimate link in the chain typically holds a reference to the stack builder sink, the last call to SyncProcessMessage will have the same effect as a call to RemotingServices.ExecuteMessage. Figure 7.7 illustrates this process.
Message sink implementations typically provide a constructor that accepts the next downstream link in the chain. The following class shows our priority proxy functionality refactored as a message sink:
using System.Runtime.Remoting.Messaging;Note that this SyncProcessMessage implementation looks identical to our original PriorityProxy.Invoke method except that in step 2, we forward the message to the next sink in the chain rather than call RemotingServices.ExecuteMessage.
The code fragment just shown did not illustrate the third IMessageSink method: AsyncProcessMessage. The CLR messaging system treats asynchronous calls differently. Specifically, the BeginInvoke request will trigger a call to AsyncProcessMessage rather than SyncProcessMessage. These AsyncProcessMessage calls will occur on a worker thread and not on the caller's thread. This means that even if your message sink took a long time to preprocess the call, the caller's thread has long since gone on to its next piece of work. That stated, the AsyncProcessMessage retains the asynchronous invocation style during message processing. To that end, each message sink in the chain is given the opportunity only to preprocess the call and not to postprocess it. If one needs postprocessing, one uses the second parameter to AsyncProcessMessage to build a reply chain to handle the results of the call. When the stack builder sink's implementation of AsyncProcessMessage has finished dispatching the call, it will send the response message up the reply chain. Because the reply chain was constructed in reverse order, the postprocessing steps will be run in the reverse order of the preprocessing steps (which is how synchronous processing works). Figure 7.8 illustrates this overall dispatching process.
Given the mechanisms shown so far, the only way for the message sinks to communicate with one another is via messages. The type and contents of these messages are determined based on which method the caller originally invoked. It is often useful for message sinks to piggyback additional data in a message, either to allow one message sink to send out-of-band information to another sink in the chain, or more interestingly, to communicate additional information to either the caller or the callee. One exposes the ability to add additional information to a message via call context.
Both the request and the response messages have a call context property, which is exposed via the IMethodMessage.LogicalCallContext property. Call context is a per-call property bag that allows one to associate arbitrary objects with uniquely named slots. For example, consider the following SyncProcessMessage implementation:
public IMessage SyncProcessMessage(IMessage request)This message sink inserts the value 52 into the logical call context slot named mykey. Downstream message sinks can then retrieve this data as follows:
public IMessage SyncProcessMessage(IMessage request)This implementation simply grabs the value in slot mykey and stores it in a local variable. Notice that the implementation checks to see whether the value is actually present. Additionally, after it retrieves the value, it then releases the slot in call context, preventing downstream sinks from seeing the value. This step is optional; however, the slot will remain occupied indefinitely otherwise.
A more interesting application of call context occurs when either the caller or the callee accesses it. This capability is exposed via the System.Runtime.Remoting.Messaging.CallContext type. This type exposes three static methods (GetData, SetData, and FreeNamedDataSlot) that manipulate the implicit call context for the current thread. Both the transparent proxy and the stack builder sink use this implicit call context to keep the implicit context in sync with the explicit LogicalCallContext property used in messages.
The following client code, which manually populates the call context, is functionally identical to the SyncProcessMessage shown earlier:
using System.Runtime.Remoting.Messaging;Similarly, the target method can also access the call context:
using System.Runtime.Remoting.Messaging;This target method doesn't care whether the call context was populated by the caller or by an intermediate message sink.
One can also send call context as part of the response message for a call. Had the target method called CallContext.SetData, the stack builder sink would have used that information to populate the response message, making any call context available to both upstream message sinks as well as the original caller.
Call context is typically bound to a particular thread. However, some message sinks may cause a thread switch or may even send a serialized version of the message over the network to a remote application. In the general case, call context will not be propagated across physical thread boundaries. However, you can change this behavior on a slot-by-slot basis. If the data stored in the slot implements the System.Runtime.Remoting.ILogicalThreadAffinative interface, message sinks that hop thread, application, process, or machine boundaries are required to propagate that slot to the next node in the chain. The ILogicalThreadAffinative interface has no methods; rather, it simply acts as a marker to inform the messaging plumbing which call context slots require propagation. Unfortunately, if all you want to propagate is a primitive, you will need to write a wrapper class that implements ILogicalThreadAffinative and holds the primitive as a public field.
One particularly nasty problem that occurs when one is building interception plumbing is the need to prevent non-intercepted references to an object from leaking out to the outside world, thus giving callers an opportunity to bypass the services provided by the interceptors. Consider, for example, the following simple class:
public class Bob : MarshalByRefObject {In this simple example, the GetIt method returns a reference to the target object. Had a transparent proxy been between the caller and the object, the client would now have two ways to reference the object: one direct and one indirect via the proxy. Now consider the following client code:
static void UseIt(Bob proxy)Had the proxy performed some critical service (e.g., concurrency management or security), that service would be bypassed on the second call to Bob.f.
Some interception-based systems require programmers to explicitly protect their this reference using a system-provided facility. MTS provided the SafeRef API, and Enterprise Java Beans (EJB) provides the EJBContext.getEJBHome method. In both cases, these routines force the developer to be explicit about proxy management, thereby defeating the whole idea of transparent interception.
To avoid these problems, the CLR provides an architecture by which one can simply use objects as expected, and the CLR guarantees that the appropriate interception code is run. This architecture is based on associating each object with a context that represents the required services that are associated with the object.
Every CLR application is divided into one or more contexts. Contexts are themselves objects that are instances of the System.Runtime.Remoting.Contexts.Context type. Objects that are compatible with ContextBoundObject are bound to a particular context at creation time; these objects are referred to as context-bound. All other objects are considered context-agile and are not affiliated with any particular context (see Figure 7.9). Context-agile objects are ignorant of the context architecture and conceptually span all contexts in an application.
Threads can enter and leave contexts at will. There is a static per-thread property, System.Threading.Thread.CurrentContext, that returns a reference to the context the current thread is executing in. One context is allocated for each application when the application starts, and it is in this context that threads begin executing. This initial context is called the default context, and one can access it via the Context.DefaultContext static property.
To illustrate the difference between context-agile and context-bound objects, consider the following two types:
public class Bound : ContextBoundObjectThe CLR makes no guarantees as to which context the Agile.f method will run because Agile is not a context-bound type. In contrast, the CLR ensures that the thread executing the Bound.f method will always run in the same context-in particular, the context the object was bound to at creation time. The CLR is able to make this guarantee by ensuring that all references to context-bound objects are in fact references to a proxy. It is impossible to acquire a direct reference to a context-bound object. That means that one can make the following assertion:
public class Bound : ContextBoundObjectAs shown here, not even the this reference is a direct reference to the object. This solves our earlier problem of ensuring that no raw references escape to the client. Because not even the object can acquire a raw reference, there is no opportunity for the interception layer to be bypassed. For efficiency, the standard proxy used for context-bound objects simply forwards the call to the target object if the calling thread is already in the correct context. Otherwise, the proxy goes through the standard message processing chain described in the previous section. To ensure that the appropriate context is always used for the target object, at creation time the CLR inserts a message sink (of type CrossContextChannel) into the chain. The message sink switches the context of the calling thread to that of the target object and then switches it back to the caller's context after the call has been dispatched. The following pseudo-code illustrates how CrossContextChannel works:
namespace System.Runtime.Remoting.ChannelsThis pseudo-code is a vast simplification of the actual implementation. The reader is encouraged to use ILDASM.EXE to look at the genuine code in mscorlib.dll.
Like call context, contexts are property bags that allow arbitrary data to be associated with a named slot. Unlike call context, data associated with a context will stay with the objects in that context no matter where a given method call may come from. The following code demonstrates the use of a context as a property bag:
public class Bound : ContextBoundObjectNote that unlike call context which used simple text-based names as keys, the GetData and SetData methods use LocalDataStoreSlots.
In addition to the simple associative array just described, contexts have zero or more named context properties. Context properties act as logical fields of the context, and, as with fields, the set of context properties does not change for the lifetime of the context. Context properties are not arbitrary data. Rather, they are objects that implement the System.Runtime.Remoting.Contexts.IContextProperty interface. Each context property has a unique text-based name, and one can access it by calling the Context.GetProperty method on a particular context. Additionally, one can get references to all of a context's context properties using Context.ContextProperties, which returns an array of context properties.
Context properties differ from the simple SetData and GetData in that context properties are established when a context is first created. Like traditional fields, context properties are uniquely named and are bound to a context at creation time. Also like fields, context properties stay with the context throughout its lifetime. As we will see in the next section, context properties also get the opportunity to inject message sinks between the proxy and the context-bound object, a capability that arguably is their primary reason for existence.
Context attributes add context properties to a context at creation time. Context attributes are custom attributes that support the IContextAttribute interface:
namespace System.Runtime.Remoting.ContextsWhen a newobj opcode is executed against a context-bound type, the CLR enumerates all of the attributes of that type looking for context attributes. When it finds the context attributes, the CLR then presents the current context and a message representing the construction call to each context's IsContextOK method. The context attribute is expected to look at the current context and determine whether or not that context is appropriate. How it makes this determination is specific to each attribute; however, the attribute typically looks at the current context to see whether the appropriate context properties are present. If the current context is acceptable, the context attribute returns true. If the current context is not acceptable, the context attribute returns false, indicating that the CLR must create a new context for the new object.
If all of the context attributes return true from IsContextOK, then the CLR will bind the new object to the current context. If at least one context attribute returns false, then the CLR will bind the new object to a new context. To ensure that the new context is properly initialized, the CLR will make a second pass across the type's context attributes, asking each of them to contribute any context properties that it wishes to be bound to the new context. This is done via the GetPropertiesForNewContext.
At this point in the discussion, an example is in order. Consider our thread priority proxy from earlier in this chapter. We can refactor that proxy fairly simply to use the context infrastructure. Rather than simply force our interception code into every proxy, we would prefer to avoid doing the adjustment twice if one object calls another with the same priority setting. To accomplish this, we will add a context property that contains the priority setting to be used for all objects in that context. This will have the effect of partitioning the application into priority domains, with all objects in a particular priority domain sharing a priority level. This is illustrated in Figure 7.10.
Our context property will be fairly simple because it needs to have only one public property, that is, the thread priority:
using System.Runtime.Remoting.Contexts;To provide a way to inject our context property, we will need to define a supporting context attribute:
using System.Runtime.Remoting.Contexts;The IsContextOK method checks for the presence of our property in the current context. If it is not there, we reject the current context, forcing the CLR to create a new context for the new object. Similarly, if our context property is present in the current context but has a different priority level than the one our attribute requires, then, again, we reject the current context. These two tests guarantee that all objects having a particular context will have the same thread priority level. It also allows two or more objects whose priority requirements are the same to share a context. This reduces resource consumption and speeds method invocation within a context, thereby improving the program's overall performance.
Our context attribute's GetPropertiesForNewContext simply creates a new context property and associates it with the construction call message. When the CLR allocates the object, it will also allocate a new context, initializing its context properties from the ContextProperties of the construction call message.
With our context property and attribute in place, we can now write the following code to use it:
[ Priority(ThreadPriority.Highest) ]Note that this code can rely on the context property being initialized correctly. However, based on the implementation we have seen so far, the CLR will make no actual adjustment of thread priority. Achieving that is the topic of the next and final section of this chapter.
The CLR gives context attributes the chance to put context properties in place as a context is being created. The CLR gives context properties the chance to inject message sinks between a proxy and a context-bound object when the object's proxy is created. One could argue that the primary motivation for context properties is to act as a factory for message sinks.
The message sinks injected by a context property are responsible for implementing the aspect that their property (and its attribute) represents. For example, our thread priority property would need to inject a message sink to adjust the thread priority prior to entering the context and then reset it upon leaving the context. Message sinks typically manipulate the state of threads, contexts, and other pieces of the program's overall execution state.
Proxies to context-bound objects support the insertion of message sinks at several well-known regions. Specifically, the context architecture partitions the chain of message sinks into four distinct regions. As shown in Figure 7.11, these regions are delimited by system-provided terminator sinks that demarcate the sink chain. Recall that it is the job of the proxy to properly adjust the execution state to switch to the target object's context. Typically, the sinks injected by the context property will also adjust aspects of the execution state. Of the four distinct regions of sinks, the CLR runs envoy sinks and client context sinks prior to switching the context. That means that if an envoy sink were to call Thread.CurrentContext, it would get back the context of the caller and not that of the target object. In contrast, object and server context sinks run after switching the context, and this means that if either of these sinks were to call Thread.CurrentContext, it would receive the context of the target object and not that of the caller.
To understand exactly why four regions of sinks are needed, it is useful to examine who needs to inject these sinks. For the remainder of this discussion, we will consider only the case in which a proxy is called from a context different from that of the target object. In this case, there are two contexts to consider: the caller's context and the target object's context.
It should be obvious that the properties of the target context will likely want to adjust the execution state prior to entering the context. This adjustment is the role of server context sinks. The CLR gives each context property in the target context that implements the IContributeServerContextSink the opportunity to inject a message sink in this region. Object sinks are similar to server context sinks. Like server context sinks, object sinks execute after the context has been switched to the target context. Unlike server context sinks, which execute no matter which object the call is destined for, object sinks are specific to one particular object in the target context. Object sinks are useful for implementing features that are specific to one object, such as MTS-style just-in-time activation. Object sinks are contributed by context properties that implement the IContributeObjectSink interface.
Server context sinks intercept calls coming into the target context. However, if a context property wishes to adjust the execution state as calls are made from the current context, that context property must contribute a client context sink. Client context sinks are contributed by context properties that implement the IContributeClientContextSink interface. Client context sinks typically undo or suspend the work done by the server context sink. The canonical example of using client and server context sinks together is to implement call timing. If a server context sink were used by itself, one could imagine easily recording the time that elapsed between entering the context and leaving the context:
public class ServerTimingSink : IMessageSinkHowever, this sink will consider the time spent in this context as well as in any child contexts that the target object may call into. To eliminate from consideration the time spent calling outside the target context, one could also inject a client context sink that would note that we are leaving the current context and adjust the accumulated time accordingly:
public class ClientTimingSink : IMessageSinkTo inject both sinks in the appropriate places, the following context property would suffice:
using System.Runtime.Remoting.Contexts;The GetServerContextSink method will be called for all proxies coming into the current context. The CLR will call the GetClientContextSink method for all proxies that call out of the current context.
The last region of message sinks to be discussed is envoy sinks. Like client context sinks, envoy sinks execute prior to the switching of context. However, client context sinks are contributed by the caller's context. In contrast, envoy sinks are contributed by the target context. Envoy sinks act as ambassadors for the server context sinks in the caller's context. Envoy sinks typically exist to inspect pieces of the caller's context (e.g., security information, locale, transaction identifiers) and send them to the server context sink via call context properties. That communication can work both ways; a server context sink can ship information upstream to its envoy sink as part of a response message, and the envoy sink can then adjust the execution state of the caller prior to finally returning control from the proxy. Envoy sinks are contributed by context properties in the target context; these properties must implement the IContributeEnvoySink interface.
To understand the role of envoy sinks, we need to look at our thread priority aspect one last time. Our previous implementations always set the thread priority to an absolute level without regard to the existing priority. What if we wanted to implement an interceptor that boosted the thread priority by one level? One approach would be to do the following in a server context sink:
IMessage SyncProcessMessage(IMessage m)Unfortunately, this works only if we are still running on the original thread of the caller. We cannot make this guarantee in the face of arbitrary message sinks. Rather, we need to build an envoy sink that propagates the original priority to our server context sink as an additional piece of call context. Moreover, to ensure that this call context is always propagated, we will need our extra data to support the ILogicalThreadAffinative interface. Here is all we need:
internal class EnvoyData : ILogicalThreadAffinativeOur envoy sink will simply add the priority to the message prior to forwarding it down the chain:
public class PriorityEnvoySink : IMessageSinkThis allows our downstream server context sink to scrape the caller's priority out of call context as follows:
public class PriorityServerSink : IMessageSinkNow all that we need is a simple context property to inject both message sinks:
public class BoostPropertyThe context attribute that inserts this property is extremely boilerplate, with one exception. Because the newobj opcode executes prior to our envoy sink's being in place, the first call to our server context sink's SyncProcessMessage will not have the benefit of our EnvoyData. To address this, our context attribute will need to insert our call context property into the construction call message in its GetPropertiesForNewContext method:
public voidIn essence, the context attribute is playing the role of the envoy sink for the constructor call.
The CLR provides a rich architecture for viewing method invocation as an exchange of messages. The key to this architecture is the transparent proxy facility provided by the CLR's remoting library. Transparent proxies live at the boundary of the world of call stacks and allow developers to view the world as a pipeline of message filters. The CLR also provides a strong notion of context, which allows one to transparently integrate interception into a type-centric programming model.
Politica de confidentialitate | Termeni si conditii de utilizare |
Vizualizari: 1341
Importanta:
Termeni si conditii de utilizare | Contact
© SCRIGROUP 2024 . All rights reserved