.NET Remoting

Create a Custom Marshaling Implementation Using .NET Remoting and COM Interop

Jim Sievert

Code download available at:CustomMarshaling.exe(4,233 KB)

This article assumes you're familiar with the CLR, C#, and COM

Level of Difficulty123

SUMMARY

The .NET Framework offers several methods for customizing the presentation of native .NET and COM object types. One such technique, custom marshaling, refers to the notion of specializing object type presentations. There are times, like when a legacy COM component needs to implement a new interface or when you need to make calls across process or machine boundaries, when custom marshaling saves the day. Elements of COM Interop permit the customizing of COM types while .NET Remoting offers the developer the ability to tailor native .NET types. This article examines these techniques.

Contents

Remoting Proxies
Extending the RealProxy
An Object with a Different View
COM Custom Marshaling
Redefining the Existing Interface
Defining the Adapter Class
Defining the Custom Marshaler
A More Sophisticated Example
More Advanced COM Interop
Conclusion

Sometimes a data object must be transmitted to a receiver that exists in a separate, predefined context, such as a machine, process, thread, class, or managed/unmanaged space. During this transmission, the data may need to undergo a transformation to make it suitable for use by the recipient. This process of converting a data object between types when sending it across contexts is commonly called marshaling.

Marshaling is employed by several technologies within the Microsoft® .NET Framework, including Remoting and COM Interop. Within Remoting and COM Interop, the .NET Framework allows programmers to tailor or customize client object presentations through the use of custom marshaling. For example, programmers typically think of implementing interfaces statically at compile time; however, occasionally you need a class that can dynamically implement the public interface of another referenced class instance. Such situations typically arise when performing method interception—intercepting method calls to a target object in order to perform some behavior completely orthogonal to the target object itself. Many scenarios probably come to mind. Method tracing, authentication, authorization, and transaction semantics are just some examples. You can probably think of some other implementations, such as COM+ and the Universal Delegator. With the .NET Remoting technology, you can implement dynamic public interfaces through the use of specialized proxies.

Similarly, it may be useful in some .NET-based client applications that use COM Interop to change or extend the public interface of legacy or third-party COM components. This scenario can occur when a legacy COM component doesn't or can't implement a newer interface used by a .NET-based client. Extended interface support for these client apps can be accomplished by using the COM custom marshaling capabilities in the .NET Framework.

Finally, there are advanced cases in which a combination of extended .NET Remoting proxies and COM custom marshaling is in order. These situations exemplify the power and flexibility of the .NET Framework application environment. This article examines some of the advanced aspects of the customized marshaling capabilities available in .NET Remoting and COM Interop.

Remoting Proxies

Of all technologies that the .NET Framework encompasses, Remoting ranks as one of the most interesting, challenging, and flexible. Remoting is itself comprised of many individual technologies such as contexts, messages, channels, proxies, sinks, and configuration. Of all of these concepts, the flexibility that Remoting proxies provide might get the least attention, even though coverage of the topic of Remoting proxies is quite extensive.

As its name implies, a Remoting proxy is a stand-in for a remote object. To a caller, it looks and acts like the actual remote object. A Remoting proxy hides all of the details of transporting and executing a method call issued from a caller to the remote object.

For example, in an inter-AppDomain Remoting scenario, one that perhaps crosses machine boundaries, a Remoting proxy is the first step towards getting a method call forwarded to the remote object through the Remoting channel. The Remoting proxy obscures all details of such behavior from the caller. For instance, in Figure 1 the caller views method calls being executed directly on the target object. These method calls are actually executed against a proxy object, not the original. The proxy hides the underlying details of the method-call transport mechanism across message sinks and the channel to the remote object itself.

Figure 1 Marshal By Reference

Figure 1** Marshal By Reference **

In the Microsoft .NET Framework there are two classes that work together to form the Remoting proxy: System.Runtime.Remoting.Proxies.TransparentProxy and System.Runtime.Remoting.Proxies.RealProxy. The client of a remote object instance always communicates with that remote object through an instance of a TransparentProxy, which gives the caller the appearance of the target object.

The TransparentProxy instance presents the interface of the remote object through close cooperation with a peer object—an instance of the RealProxy class. The RealProxy class is the source of information about and communication with the actual remote object, but it's an abstract class that must be extended. This override allows a derived class to properly configure the RealProxy. TransparentProxy, on the other hand, cannot be extended and derives its operation strictly from information obtained from and services provided by the associated RealProxy. A caller obtains an instance of the TransparentProxy class from the RealProxy in which the TransparentProxy is contained.

Extending the RealProxy

The following code defines the three basic elements that are involved in extending the RealProxy class: the constructor, the virtual CreateObjRef method override, and the abstract Invoke method override:

public abstract class RealProxy { protected RealProxy( Type classToProxy ); public virtual ObjRef CreateObjRef( Type requestedType ); public abstract IMessage Invoke( IMessage message ); ••• }

The key to extending a RealProxy is specifying the type of object that the resulting TransparentProxy will represent. A RealProxy-derived class accomplishes this type specification through the protected constructor of the RealProxy class. This constructor takes a single parameter, a System.Type instance called classToProxy. This parameter represents the type or class of object that is to be proxied. That is, the TransparentProxy will customize its behavior to act as an instance of the type specified in the classToProxy parameter. While the constructor of the RealProxy takes a generic System.Type object as a parameter, class ToProxy must be one of two permissible types or the RealProxy constructor will throw a System.ArgumentException.

The first permissible System.Type argument to the RealProxy constructor is an instance of a System.MarshalByRefObject type. MarshalByRefObject is the base class for any and all objects that can be marshaled by reference from one AppDomain to another. It goes without saying that remotely referenced objects require proxy support. This is especially true given all the implementation details necessary to get a method call from one AppDomain to another, as illustrated in Figure 1.

In general, the TransparentProxy for a System.MarshalByRefObject will appear to a caller as an actual instance of the remote type. In the case of remotely referenced objects, this makes perfect sense. Passing an instance of the System.MarshalByRefObject type into the RealProxy constructor introduces some subtle concerns, including propagation of the proxy to the new AppDomain and the possible performance implications of this action.

The second permissible type of RealProxy constructor argument is an interface. In this case, the resulting TransparentProxy will appear to a caller as an object implementing the interface type specified in the RealProxy constructor. Such a caller view presents some interesting implementation possibilities in extending the RealProxy class. The first obvious possibility is that this new public view can be applied to objects whose type is not derived from System.MarshalByRefObject. The other notable implementation possibility is that a TransparentProxy can expose a completely different public view than that exposed by the actual target class. This article will expand further on these implementation possibilities in subsequent sections.

The next task in extending a RealProxy is overriding the CreateObjRef method. The .NET Framework calls CreateObjRef whenever it attempts to remote an object reference. Considering only the case where the RealProxy constructor argument is an interface type, CreateObjRef should be overridden as follows:

public override ObjRef CreateObjRef( Type requestedType ) { throw new NotSupportedException(); }

As emphasized here, a class implementing the interface type exposed by the TransparentProxy does not necessarily inherit from System.MarshalByRefObject. Remoting of such a class is not possible. As a result, the creation of an ObjRef is not supported.

The final task in extending a RealProxy, which must be done for all derived classes, is to override the abstract Invoke method. Invoke initiates the transmission of a method call to a remote object. The type of this transmission may vary depending on the particular remoting situation. For example, when remoting a method call between AppDomains, Invoke transmits the method call through a Remoting channel. In other cases, a method call can reach the target object through other means, such as direct reflection.

The TransparentProxy forwards the actual method being called to the overridden Invoke method on a derived RealProxy implementation. The form or representation of an actual method call is that of a Remoting message. In the case of Invoke, a Remoting message contains all the information necessary to communicate a method call to a remote object, including the actual method being called. The goal of the Invoke implementation is to transmit the actual method call message to the remote object, receive the results, and form a return message that encompasses the return value and modified parameter values of the method call.

The code segment that is shown in Figure 2 combines all of the elements necessary to form a RealProxy-derived class called MyProxy. When using the techniques described here, this code segment provides a general template for the extension of the RealProxy class. You should note that the code segment contains a public static method called Marshal from which a caller can obtain a TransparentProxy instance that wraps a specified object instance. The Marshal method removes the burden of callers having to manually obtain the TransparentProxy instance by dealing directly with the derived RealProxy.

Figure 2 RealProxy Class Derivation

internal class MyProxy : RealProxy { public static IMySpecial Marshal( object instance ) { MyProxy myProxy = new MyProxy( instance ); return myProxy.GetTransparentProxy(); } private MyProxy( object instance ) : base( typeof( IMySpecial ) ) { m_instance = instance; } public override ObjRef CreateObjRef( Type requestedType ) { throw new NotSupportedException(); } public override IMessage Invoke( IMessage message ) { // Process message and form a return message, the details // of which are ignored for now. IMessage returnMessage = ...; return returnMessage; } private object m_instance; }

The name of the static method, Marshal, is also significant. Calling the Marshal method represents a context boundary in which a caller's view of the original object may be different from that view presented by the resulting object. Herein lies one aspect of custom marshaling within .NET Remoting.

Because it contains the public static Marshal method, the constructor of MyProxy is private. Additionally, the constructor takes one parameter, an object instance against which the derived proxy will operate in the overridden Invoke method. The constructor of MyProxy initializes the base RealProxy with the interface type, IMySpecial. This action results in the TransparentProxy instance supporting the interface IMySpecial.

I've left out the details of the Invoke method for the time being, but I'll cover them in subsequent samples. It should be apparent to you that method call interception on the client or caller side begins in a RealProxy-derived class. The remainder of this article builds on this method call interception capability that is inherent in RealProxy-derived classes.

An Object with a Different View

As previously described, constructing a RealProxy with an interface type results in a TransparentProxy presenting that interface as the public view of the underlying object. What's more, this public view may be different than that of the underlying object. Imagine for a moment the possibilities. A RealProxy-derived class can intercept one method and potentially forward that call to the target object as another, entirely different method. A further possibility is that the RealProxy-derived class can intercept a method call and implement it locally without forwarding it to the target object at all. It is also entirely possible to swap out the target object held by the RealProxy-derived object while allowing current clients to hold onto the existing proxy.

A RealProxy-derived class can take its stealthiness one step further by implementing the System.Runtime.Remoting.IRemotingTypeInfo interface. When implemented by a RealProxy-derived class, IRemotingTypeInfo provides additional type information to the TransparentProxy. More specifically, the TransparentProxy calls IRemotingTypeInfo.CanCastTo to determine whether or not a caller can cast to a particular interface type.

The decision of whether or not to allow specific casts depends on the RealProxy-derived class. A RealProxy-derived class can allow a cast to any interface, intercept a method call on that interface, and direct that method call to another method on a completely different interface. Furthermore, a RealProxy-derived class could disallow some casts and restrict the methods that are accessible on the target object.

It should be noted that the capabilities I just outlined apply to both remotely referenced classes (those that derive from System.MarshalByRefObject) and non-remotable types.

The sample in Figure 3 wraps a target object in an Interposer. It follows the basic template for extending RealProxy, as shown in Figure 2. The TransparentProxy obtained from an Interposer supports the IInterposed interface, which allows a caller to retrieve the target object instance. The Interposer class implements the IInterposed interface locally within the Invoke method. Also, the implementation of IRemotingTypeInfo ensures that all caller-issued interface cast attempts against a TransparentProxy of an Interposer succeed only if the target object implements them. Even though I've already described some method interception implementations, the sample in Figure 3 forwards method calls to the target object using reflection. This call will either succeed or fail with the results returned. The Invoke method implementation in Figure 3 can be expanded to implement some of the more sophisticated method interception capabilities described previously.

Figure 3 Interposer Sample

// This interface is supported directly by the Interposer class itself. // This interface allows a caller to retrieve the object instance on // which the Interposer performs method invocation through reflection. public interface IInterposed { object Target { get; } } public class Interposer : RealProxy, IRemotingTypeInfo { // Standard Marshal method that returns a TransparentProxy instance // that can act as ANY interface. public static object Marshal( object target ) { Interposer interposer = new Interposer( target ); return interposer.GetTransparentProxy(); } // The basic constructor initializes the RealProxy with the // IInterposed interface type and saves away the target instance. private Interposer( object target ) : base( typeof( IInterposed ) ) { m_target = target; m_targetType = m_target.GetType(); } public override ObjRef CreateObjRef( Type requestedType ) { // We don't support marshaling objects wrapped in a // Interposer, mainly because objects wrapped // by this proxy may not be marshalable. That is they // may not extend System.MarshalByRefObject. throw new NotSupportedException( "Remoting of an interposed object is not supported." ); } public override IMessage Invoke( IMessage message ) { // The actual message coming in at this point should be // an IMethodCallMessage. The MethodCallMessageWrapper // holds onto the arguments within the message for the // duration of the Invoke call. IMethodCallMessage methodMessage = new MethodCallMessageWrapper( (IMethodCallMessage) message ); // Obtain the actual method definition that is being called. MethodBase method = methodMessage.MethodBase; object returnValue = null; // Handle IInterposed methods/properties here. Right now, there is // only one property, so don't do any extensive checking to // determine the method or property being invoked. if ( method.DeclaringType == typeof( IInterposed ) ) { returnValue = m_target; } else { returnValue = method.Invoke( m_target, methodMessage.Args ); } // Finally, form a return message containing the return // value along with any and all parameters that may have been // modified by the method call (those that are out or ref). IMessage returnMessage = new ReturnMessage( returnValue, methodMessage.Args, methodMessage.ArgCount, methodMessage.LogicalCallContext, methodMessage ); return returnMessage; } public bool CanCastTo( Type type, object o ) { // Allow a cast to IIinterposed or to any type supported by the // underlying target object. bool valid = type == typeof( IInterposed ) || type.IsAssignableFrom( m_targetType ); return valid; } public string TypeName { get { throw new NotSupportedException(); } set { throw new NotSupportedException(); } } private object m_target; private Type m_targetType; }

COM Custom Marshaling

COM custom marshaling allows you to customize the client view of objects across the managed/unmanaged code boundary. In his information-packed tome on COM Interop, .NET and COM: The Complete Interoperability Guide (SAMS, 2002), Adam Nathan devotes an entire chapter to the topic of COM custom marshaling. While this article is not meant to rehash the coverage contained in Adam's book, it may be helpful to summarize the premise behind COM custom marshaling.

In general, the .NET Framework COM Interop layer hides all that is COM. The Framework wraps COM objects that are accessed from the managed environment in what's called a runtime-callable wrapper (RCW). From the point of view of a .NET caller, an RCW makes a COM object look like any other .NET-based object. An RCW, in essence, acts as a proxy to a COM object. In a similar way, the .NET Framework wraps .NET-based objects in something called a COM-callable wrapper (CCW) to make them accessible to COM clients. The CCW exposes the public interfaces implemented on a given .NET object.

COM custom marshaling allows a programmer to tailor the view presented by an RCW or CCW. Such customization can be useful in situations where a programmer wants to enhance or change object functionality without affecting legacy code. As was the case when examining Remoting proxies, customized RCWs and CCWs can provide behavior completely orthogonal to the target object. Furthermore, COM custom marshaling permits customization of RCWs and CCWs at the method level, so customization can vary depending on the particular method called.

As an example, consider a legacy, COM-based arithmetic library. The library supports a number of calculator-like classes. One of these classes is an Adder, which returns the sum of two integers. The following IDL-based interface describes the function of the Adder:

[ object, uuid(A67A7777-B16E-45D3-953B-F5915E3EBFBB), oleautomation ] interface IAdder : IUnknown { HRESULT Add( [in] int a1, [in] int a2, [out, retval] int *sum ); };

A COM client of the arithmetic library obtains an instance of an Adder through a calculator factory instance. The calculator factory has an IDL-based interface like the following:

[ object, uuid(0A4043BF-D9D0-47EB-AC23-4D5E62154A7C), oleautomation ] interface ICalculatorFactory : IUnknown { HRESULT CreateAdder( [out, retval] IAdder **adder ); };

Now suppose you've been asked to change the Adder function itself when called via the common language runtime (CLR)—instead of returning the sum of two integers, the Adder is to return the sum for a vector of integers using the legacy COM library. In addition, suppose that a further requirement suggests that a change to the legacy library is not permitted.

You can implement the new Adder class through the use of COM custom marshaling, creating a customized RCW for the Adder. The implementation requires you to redefine the existing interface, define an Adapter class, and define the custom marshaler. In the following sections, I will describe these steps in detail.

Redefining the Existing Interface

The requirements for the new Adder class specify that an instance must return the sum for a vector of integers. So, given an instance of the calculator factory, the programmer's job is to provide a caller with an Adder whose public interface accepts an array of integers and returns the sum as an integer. Such a change to the calculator factory interface flies in the face of the legacy calculator factory interface provided by the type library import process.

The process of importing a type library for the legacy COM-based calculator library generates a COM Interop assembly. In this example, the COM Interop assembly is called CalculatorLibraryLib. This assembly provides a .NET-based client with all the metadata required to instantiate and use objects within the legacy library. However, the Adder returned by the calculator factory simply returns the sum of two integers. The first change requires that the calculator factory interface provide an Adder class that supports returning the sum for a vector of integers.

Because the other requirement for the new Adder class stipulates that the legacy library can't be changed, you have to resort to another technique to change the return type of the ICalculatorFactory.CreateAdder method. You can make this change through the use of the ComImport attribute, which lets you define the exact binary representation of a COM interface. So the interface for the calculator factory can now be defined within the .NET Framework environment as follows:

[ ComImport, Guid( "0A4043BF-D9D0-47EB-AC23-4D5E62154A7C" ), InterfaceType( ComInterfaceType.InterfaceIsIUnknown ) ] public interface INewCalculatorFactory { [return: MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( AdderMarshaler ) )] NewAdder CreateAdder(); }

There are three important points of interest in this interface definition besides the use of the ComImport attribute. The first is that the interface Guid and IUnknown interface inheritance is the same as that specified in the original IDL definition of the interface. Note that the interface name is also different from the original IDL definition. Such a name change is not strictly necessary, but it helps to disambiguate references in this article pertaining to the original IDL definition versus the new C# definition.

The second point of interest is that the return type from CreateAdder is no longer IAdder. Instead, CreateAdder returns a new type called NewAdder. The MarshalAs attribute specified for the return type of CreateAdder facilitates the return of the NewAdder instance. The parameters to the MarshalAs attribute state that a COM custom marshaler instance of type AdderMarshaler will be used to generate the NewAdder instance.

The third point of interest is that a programmer specifies COM custom marshaling on a per-method-parameter basis. That is, custom RCWs and CCWs can only be used when required. In general, there is no way to specify the use of custom marshaling on a per-type basis.

Defining the Adapter Class

You can refer to the NewAdder type as an Adapter class. An Adapter class is an intermediate type that supports the new public view which the Adapter class can implement using any reasonable approach. Typically, it will use the functionality of the legacy object to implement the new public view. Such will be the case with the NewAdder implementation shown in Figure 4. In effect, the Adapter class is an intermediary or proxy that stands between a caller and the ultimate target CCW or RCW.

Figure 4 NewAdder Class

public class NewAdder { internal NewAdder( CalculatorLibraryLib.IAdder remoteAdder ) { m_remoteAdder = remoteAdder; } public int Add( int[] ints ) { int sum = 0; foreach ( int i in ints ) { sum = m_remoteAdder.Add( sum, i ); } return sum; } private CalculatorLibraryLib.IAdder m_remoteAdder; }

The constructor for a NewAdder takes as input a single parameter that specifies the legacy instance responsible for generating the vector summation. The COM custom marshaler will supply this legacy instance. The following section describes the complete actions of the COM custom marshaler. The public Add method takes as input a vector of integers and uses the legacy COM Adder to do the real work.

Defining the Custom Marshaler

The definition of a COM custom marshaler is the final step in completing the implementation of the new Adder class. The custom marshaler is responsible for generating the proper transition environment between the managed and unmanaged worlds. This transition environment is either a CCW or RCW Adapter object.

A COM custom marshaler has two implementation requirements. The first is that a COM custom marshaler must implement the interface, ICustomMarshaler, which can be found in the System.Runtime.InteropServices namespace. The following code snippet describes the ICustomMarshaler interface:

interface ICustomMarshaler { IntPtr MarshalManagedToNative( object ManagedObj ); object MarshalNativeToManaged( IntPtr pNativeData ); void CleanUpManagedData( object ManagedObj ); void CleanUpNativeData( IntPtr pNativeData ); int GetNativeDataSize(); }

The key methods in this interface are MarshalManagedToNative and MarshalNativeToManaged. The .NET Framework calls MarshalManagedToNative when a method parameter makes a transition from the .NET-managed environment to the unmanaged COM environment. A call to MarshalManagedToNative results in the creation of a CCW Adapter object. Likewise, the .NET Framework calls MarshalNativeToManaged when a method parameter makes a transition from the unmanaged COM environment to the .NET-managed environment. In this particular case, a call to MarshalNativeToManaged will result in the creation of an RCW Adapter object.

The .NET Framework calls the methods CleanUpManagedData and CleanUpNativeData after any of the previously described marshaling calls have been completed. The .NET Framework invokes CleanUpManagedData to release any resources that may have been allocated during the MarshalManagedToNative call. Likewise, the .NET Framework invokes CleanUpNativeData to release any resources that may have been allocated during the MarshalNativeToManaged call. In general, you should find marshalers to be essentially stateless. As a result, releasing managed or unmanaged resources should not be required.

Finally, GetNativeDataSize returns the size required to marshal a value type parameter. The current release of the .NET Framework (version 1.1) does not support the marshaling of value types. This method should simply return -1.

The second requirement for a COM custom marshaler is the implementation of the following method:

static ICustomMarshaler GetInstance( string cookie )

A typical implementation of GetInstance makes the custom marshaler a singleton. A singleton is used because the .NET Framework calls GetInstance only once per custom marshaler type, no matter how many parameters or return types specify the use of the particular custom marshaler type. A reasonable GetInstance implementation might look like this:

class Marshaler { ••• public static ICustomMarshaler GetInstance( string cookie ) { return m_marshaler; } private static ICustomMarshaler m_marshaler = new Marshaler(); ••• }

The cookie parameter can be specified in the public MarshalCookie field of the MarshalAs attribute. The cookie value can be used in any manner by the GetInstance method of the custom marshaler. Note that the cookie value can even be ignored as in the example that was previously shown.

The code in Figure 5 represents a complete implementation of a COM custom marshaler for the improved Adder class. The only method of interest is MarshalNativeToManaged. As presented earlier, MarshalNativeToManaged creates an RCW Adapter object that implements the new public view using the legacy COM Adder. MarshalNativeToManaged does this by first obtaining an RCW for the legacy Adder using the Marshal.GetObjectForIUnknown method. It then creates and returns a NewAdder Adapter instance to wrap the RCW.

Figure 5 COM Custom Marshaler Implementation

internal class AdderMarshaler : ICustomMarshaler { public static ICustomMarshaler GetInstance( string cookie ) { // Return the static marshaler instance. return m_marshaler; } public object MarshalNativeToManaged( System.IntPtr pNativeData ) { // Get a managed RCW corresponding to the native legacy // Adder instance. object remoteAdder = Marshal.GetObjectForIUnknown( pNativeData ); // Wrap the legacy Adder in a NewAdder instance and return the // NewAdder instance. object newAdder = new NewAdder( (CalculatorLibraryLib.IAdder) remoteAdder ); return newAdder; } public void CleanUpNativeData( System.IntPtr pNativeData ) { // There is no data to release. Do nothing. } public System.IntPtr MarshalManagedToNative( object managedObj ) { // This method should never be called. Marshaling a NewAdder // to unmanaged code is not supported. throw new NotSupportedException(); } public void CleanUpManagedData( object managedObj ) { // This should never be called. Do nothing in any case. } public int GetNativeDataSize() { // This method should never be called. Do nothing in any case. return -1; } private AdderMarshaler() { } private static AdderMarshaler m_marshaler = new AdderMarshaler(); }

At this point, a .NET-based client program can make use of the NewAdder instance with code like the following:

object rawFactory = new CalculatorLibraryLib.CalculatorFactoryClass(); INewCalculatorFactory factory = (INewCalculatorFactory) rawFactory; NewAdder adder = factory.CreateAdder(); int[] ints = new int[] { 1, 2, 3, 4 }; int sum = adder.Add( ints );

Notice that the client program instantiates a legacy factory instance using the original Interop assembly. It then caches the legacy factory in the rawFactory variable. Following that, it casts the legacy factory instance into INewCalculatorFactory. The .NET Framework permits such a cast because the Guid attribute supplied on the INewCalculatorFactory definition corresponds to the one that's already supported by the legacy factory. From this point on, any reference to INewCalculatorFactory.CreateAdder will fire up the COM custom marshaling infrastructure in the .NET Framework, resulting in an instance of a NewAdder. With a NewAdder instance in hand, the client program can initiate the summation of an integer vector.

A More Sophisticated Example

The Interposer sample and COM custom marshaling together form the basis for more complex custom marshaling scenarios. It's worth considering one more example in which a RealProxy-based interception method coupled with COM custom marshaling may be useful. The sample in question goes beyond the typical uses of method interception and attempts to correct a shortcoming that's in the .NET Framework.

Let's consider a sample .NET-based application that is required to work with instances of .NET-based classes as well as instances of legacy COM IDispatch-based classes. From the application's point of view, it would be convenient to operate homogeneously across all instances, whether the instance type is based in COM or in the .NET Framework.

Assume that the basic operation of the sample .NET-based application uses reflection to determine the methods implemented by these various instances. Based on the methods that these instances implement, the application performs some internal event monitoring. When the application detects an internal event, the application calls the instance methods corresponding to the event.

The goal of this sample application appears simple enough; however, it should be noted that a reader familiar with COM Interop will discover an insidious problem. While the .NET Framework allows an application to invoke IDispatch-based COM object methods through reflection, strangely enough it does not permit an application to directly discover the method signatures of such COM objects using reflection.

Based on this information, it's clear that the sample application can't homogeneously reflect upon the various instances that it may encounter. Assuming that IDispatch-based COM instances can, and generally do, return type information through their IDispatch implementations, it is possible to overcome the inherent shortcomings of .NET Framework reflection with respect to IDispatch. Obtaining this type information is the first step toward accomplishing the application goal of treating all classes equally. Moreover, a RealProxy-derived class combined with COM custom marshaling is a key element in supporting this goal. This is because a TransparentProxy is the only type that can be dynamically configured as supporting a given interface.

More Advanced COM Interop

The IDL code shown in Figure 6 contains the canonical IDispatch interface declaration. The IDispatch GetTypeInfo method can be used in order to obtain an ITypeInfo interface. Given an ITypeInfo interface, it is possible for you to obtain instance method names and parameter information.

Figure 6 Canonical IDispatch

[ object, uuid(00020400-0000-0000-C000-000000000046), pointer_default(unique) ] interface IDispatch : IUnknown { HRESULT GetTypeInfoCount( [out] UINT *pctinfo ); HRESULT GetTypeInfo( [in] UINT iTInfo, [in] LCID lcid, [out] ITypeInfo **ppTInfo ); HRESULT GetIDsOfNames( [in] REFIID riid, [in, size_is(cNames)] LPOLESTR *rgszNames, [in] UINT cNames, [in] LCID lcid, [out, size_is(cNames)] DISPID *rgDispId ); HRESULT Invoke( [in] DISPID dispIdMember, [in] REFIID riid, [in] LCID lcid, [in] WORD wFlags, [in, out] DISPPARAMS *pDispParams, [out] VARIANT *pVarResult, [out] EXCEPINFO *pExcepInfo, [out] UINT *puArgErr ); }

As stated earlier, the .NET Framework hides any notion of COM from .NET-based callers. This holds true even for an IDispatch-based COM instance where the RCW completely obscures the IDispatch interface. The problem then becomes how to obtain the IDispatch interface from a given RCW. This can be done by using the COM Interop method presented previously. Recall that a binary version of a COM interface can be defined in C# using the ComImport attribute. The code in Figure 7 uses this attribute to define a somewhat radical view of IDispatch compared to the definition in canonical IDL.

Figure 7 Revised IDispatch Definition

[ ComImport, Guid( "00020400-0000-0000-C000-000000000046" ), InterfaceType( ComInterfaceType.InterfaceIsIUnknown ) ] private interface IDispatch { void Unused1(); [PreserveSig] int GetTypeInfo( uint nInfo, int lcid, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( TypeToTypeInfoMarshaler ) )] out System.Type type ); }

Because the .NET-based application will exclusively call IDispatch.GetTypeInfo, it's only necessary to define the binary version of IDispatch up to and including that method. Intervening methods can simply be tagged with names like "Unused1" that act as placeholders.

The C# IDispatch definition shown in Figure 7 also plays a trick quite similar to that presented in the Adder example. Notice that in the original canonical IDispatch IDL (shown in Figure 6), the last parameter of GetTypeInfo returns an ITypeInfo interface; however, the C# version defines that same parameter to return a System.Type. It just so happens that there is a custom marshaling routine called TypeToTypeInfoMarshaler that transforms an ITypeInfo interface into a System.Type object. The System.Type object is actually a dynamically generated interface whose metadata describes the methods of the underlying IDispatch implementation. This custom marshaling routine can be found in the CustomMarshaler.dll assembly located in the .NET Framework installation directory. As described previously, the use of the MarshalAs attribute applies TypeToTypeInfoMarshaler to the System.Type parameter in IDispatch.GetTypeInfo.

Calling IDispatch.GetTypeInfo as I've defined results in the return of an interface type that describes the methods and parameters of the particular COM instance implementing IDispatch. The resulting interface type is a great boon to the sample .NET-based application and can be applied to a RealProxy-derived class. Furthermore, the resultant TransparentProxy will present the public view defined by this interface. The bottom line is that the sample .NET Framework-based application will now be able to reflect on the resulting TransparentProxy (using Type.GetMethod) as it would any other .NET Framework-based object. In addition, the sample application will also be able to invoke methods in a consistent way across all classes.

Figure 8 presents the code for a RealProxy-derived class called GenericReflectionProxy that handles reflection against IDispatch-based COM object instances. Note that GenericReflectionProxy follows the basic template illustrated in Figure 2. In addition to the basic template, GenericReflectionProxy adds some code to the Marshal method that discriminates between .NET-based objects and COM objects. That is, the Marshal method only returns a TransparentProxy when the instance provided is COM-based.

Figure 8 GenericReflectionProxy

internal class GenericReflectionProxy : RealProxy { public static object Marshal( object instance ) { // Any null references are simply returned back. if ( instance == null ) { return null; } // Next, we'll see if the instance attempting to be wrapped // is a COM object. If it's not, there's no need to put a // proxy around it because .NET objects support reflection. // Instead we'll just return the instance itself. Type type = instance.GetType(); if ( !type.IsCOMObject ) { return instance; } // At this point, we're dealing with a COM object. We'll get the // default interface type and wrap the COM object in a proxy. Type interfaceType = GetInterface( instance ); GenericReflectionProxy proxy = new GenericReflectionProxy( interfaceType, instance ); return proxy.GetTransparentProxy(); } public override ObjRef CreateObjRef( Type requestedType ) { throw new System.NotSupportedException(); } public override IMessage Invoke( IMessage message ) { IMethodCallMessage methodMessage = new MethodCallMessageWrapper( (IMethodCallMessage) message ); MethodBase method = methodMessage.MethodBase; object returnValue = null; // If the method that we're attempting to call is "GetType", // return the original interface type. if ( method.Name == "GetType" ) { returnValue = m_interfaceType; } else { // We're attempting to execute some other method. // Because we're going to execute using the IDispatch interface // on the COM object, we need to set up parameter modifiers for // all arguments that are out or ref. These modifiers allow the // COM Interop layer to pass the arguments as VT_BYREF. ParameterModifier[] modifiers = null; if ( methodMessage.ArgCount > 0 ) { ParameterModifier modifier = new ParameterModifier( methodMessage.ArgCount ); modifiers = new ParameterModifier[] { modifier }; int index = 0; foreach ( ParameterInfo pi in method.GetParameters() ) { modifier[ index++ ] = pi.ParameterType.IsByRef; } } // Select the correct binding flags for invoking a method. It // should be noted that as written, this code will not get or // set IDispatch-based properties. This exercise is left to the // reader. ;-) const BindingFlags flags = BindingFlags.InvokeMethod; // Call the actual method using the IDispatch interface. returnValue = m_instanceType.InvokeMember( method.Name, flags, null, m_instance, methodMessage.Args, modifiers, null, null ); // We now have to look at all values to be returned. We // make sure that if they are COM objects, that we wrap // them as necessary in a GenericReflectionProxy. returnValue = Marshal( returnValue ); int j = 0; foreach ( ParameterInfo pi in method.GetParameters() ) { if ( pi.ParameterType.IsByRef ) { methodMessage.Args[ j ] = Marshal( methodMessage.Args[ j ] ); } j++; } } IMessage returnMessage = new ReturnMessage( returnValue, methodMessage.Args, methodMessage.ArgCount, methodMessage.LogicalCallContext, methodMessage ); return returnMessage; } private GenericReflectionProxy( Type interfaceType, object instance ) : base( interfaceType ) { m_interfaceType = interfaceType; m_instance = instance; m_instanceType = instance.GetType(); } private static Type GetInterface( object instance ) { // The Marshal method has filtered out non-COM objects. We // now have to determine whether or not the incoming COM object // supports IDispatch. Furthermore if it does support IDispatch, // it must support type information. If the incoming object // fails to support both, we'll return the type of interface as // "IDontSupportReflection". This interface supports no methods. Type defaultType = typeof( IDontSupportReflection ); Type type = defaultType; // Make sure that the incoming object supports IDispatch. If it // doesn't we'll just return the default type. IDispatch dispatch = instance as IDispatch; if ( dispatch != null ) { // The incoming object supports IDispatch. Attempt to get // type information from the object. This may fail. If it // does fail, return the default type. int status = dispatch.GetTypeInfo( 0, 0, out type ); if ( status != 0 || type == null ) { type = defaultType; } } return type; } [ ComImport, Guid( "00020400-0000-0000-C000-000000000046" ), InterfaceType( ComInterfaceType.InterfaceIsIUnknown ) ] private interface IDispatch { void Reserved(); [PreserveSig] int GetTypeInfo( uint nInfo, int lcid, [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( TypeToTypeInfoMarshaler ) )] out System.Type typeInfo ); } private interface IDontSupportReflection { } private Type m_interfaceType; private object m_instance; private Type m_instanceType; }

GenericReflectionProxy also adds a private static method called GetInterface. This method returns an interface type describing the COM object. If the COM object supports IDispatch, and that IDispatch implementation provides type information, GetInterface returns an interface type that describes the methods supported by the IDispatch implementation. If the COM object doesn't support IDispatch or the IDispatch implementation does not provide type information, GetInterface returns an internal interface type that supports no methods. In this way, a .NET-based application will find no supported methods when doing reflection.

Conclusion

This article has presented insight into important, yet little-known custom marshaling techniques available in .NET Remoting and COM custom marshaling. You can extend a RealProxy using interfaces to control the overall public view presented by the resulting TranparentProxy. In addition, COM custom marshaling provides capabilities to change object views across the managed/unmanaged boundary. In many cases, some powerful method interception behaviors can be achieved when combining these techniques.

Even though the custom marshaling techniques I've presented demonstrate the extreme flexibility of the .NET Framework, this flexibility doesn't come without a price. There is a significant performance penalty for using an Interposer-based solution. Compared with reflection, invoking a method on an Interposed object is an operation that turns out to be roughly 10 times slower. Likewise, COM custom marshaling adds about 50 percent more execution time to the marshaling process. For the designer, the usual consideration of performance versus flexibility arises; however, in many cases the flexibility of these marshaling techniques far outweigh any performance penalty.

For related articles see:
.NET Remoting: Design and Develop Seamless Distributed Applications for the Common Language Runtime
.NET Interop: Get Ready for Microsoft .NET by Using Wrappers to Interact with COM-based Applications
Extending RealProxy

For background information see:
Side-By-Side Execution for COM Interop
.NET and COM: The Complete Interoperability Guide by Adam Nathan (SAMS, 2002)

Jim Sievert is a professional consultant for Unisys Corporation. He'll often be found with the mud of new technologies all over his hands. His responsibilities include the pursuit of new technologies that enhance system control software for the Unisys ES7000 line of enterprise computer systems. He can be reached at <james.sievert@unisys.com.>.