| I l@ve RuBoard |
|
Messages as Method CallsThe 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. Figure 7.1. Method Invocation and Stack Frames
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;
using System.Collections;
namespace System.Runtime.Remoting.Messaging {
public interface IMessage {
IDictionary Properties { get; }
}
public interface IMethodMessage : IMessage {
object GetArg(int index);
string GetArgName(int index);
int ArgCount { get; }
object[] Args { get; }
bool HasVarArgs { get; }
LogicalCallContext LogicalCallContext { get; }
MethodBase MethodBase { get; }
string MethodName { get; }
object MethodSignature { get; }
string TypeName { get; }
string Uri { get; }
}
}
Figure 7.2. The IMessage Type Hierarchy
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) {
IMethodCallMessage call = (IMethodCallMessage)msg;
Console.WriteLine("<{0} >", call.MethodName);
for (int i = 0; i < call.ArgCount; ++i)
Console.WriteLine(" <{0} >{1} </{0} >",
call.GetArgName(i),
call.GetArg(i), call.GetArgName(i));
Console.WriteLine("</{0} >", call.MethodName);
}
Suppose that this routine were presented the message that corresponds to the following invocation: int b = 2; int c = 3; int n = foo.MyMethod(1, ref b, out c); The following would be displayed: <MyMethod> <arg1>1</arg1> <arg2>2</arg2> <arg3>3</arg3> </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;
using System.Collections;
namespace System.Runtime.Remoting.Messaging {
public interface IMethodCallMessage : IMethodMessage {
object GetInArg(int index);
string GetInArgName(int index);
int InArgCount { get; }
object[] InArgs { get; }
}
public interface IMethodReturnMessage : IMethodMessage {
object GetOutArg(int index);
string GetOutArgName(int index);
object ReturnValue { get; }
Exception Exception { get; }
int OutArgCount { get; }
object[] OutArgs { get; }
}
}
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. Figure 7.3. Method Invocation and Messages
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.Messaging {
public class ReturnMessage : IMethodReturnMessage,
IMethodMessage,
IMessage {
// normal termination constructor
public ReturnMessage(object returnValue,
object[] outArgs,
int outArgCount,
LogicalCallContext callCtx,
IMethodCallMessage request);
// abnormal termination constructor
public ReturnMessage(System.Exception ex,
IMethodCallMessage request);
// remaining members elided for clarity
}
}
Note 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 ICalculator {
double Add(double x, double y);
double Multiply(double x, double y);
}
Ultimately, 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 IMethodReturnMessage
ProcessMessage(IMethodCallMessage request) {
switch (request.MethodName) {
case "Add": {
double x = (double)request.GetInArg(0);
double y = (double)request.GetInArg(1);
double result = x + y;
return new ReturnMessage(result,null,0,null,request);
}
case "Multiply": {
double x = (double)request.GetInArg(0);
double y = (double)request.GetInArg(1);
double result = x * y;
return new ReturnMessage(result,null,0,null,request);
}
default: {
String exm = String.Format("{0} not implemented",
request.MethodName);
Exception ex = new NotImplementedException(exm);
return new ReturnMessage(ex, request);
}
}
}
Note 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. |
| I l@ve RuBoard |
|