.NET COM Support

I l @ ve RuBoard

For Managed code to consume the services provided by a COM component, a bit of translation must take place. After all, you won't find CoInitialize or CoCreateInstance within the .NET framework. Such arcane practices, which most of us spent considerable time learning, are no more. Instead, the .NET environment introduces the notion of a Runtime Callable Wrapper or RCW. From the viewpoint of the Managed client there is no real difference between using the RCW and a managed component. Figure 5.3.1 depicts a very generic view of a Managed client using a RCW to interact with a classic COM component.

Figure 5.3.1. Generalized view of RCW.

graphics/0503fig01.gif

An RCW has several responsibilities that shield the developer from tasks such as reference counting, QI, and memory allocation/de-allocation.

RCW's responsibilities include the following:

  • Preserving Object Identity

  • Maintaining Object Lifetime

  • Proxying Unmanaged Interfaces

  • Marshaling Method Calls

  • Consuming Selected Interfaces

Preserving Object Identity

There is only one RCW for an underlying COM component. This allows the RCW to ensure the identity of the COM object by comparing objects against the IUnknown interface. A RCW will appear to implement several interfaces, as provided by the underlying COM object, but the RCW actually only hides the QueryInterface calls from the developer. When casting for an interface, the RCW checks for a cached interface of the requested type and, if found, returns it; otherwise , a QI is invoked. When the RCW invokes a QI on the underlying COM object, the returned interface is added to the cache maintained by the RCW. If a requested interface is not found, the standard InvalidCastException is thrown by the RCW.

Maintaining Object Lifetime

A single instance of the RCW is necessary for the proper lifetime management of the underlying COM object. The RCW handles the necessary calls to AddRef() and Release() , thus ensuring proper reference counting and finalization of the COM object. As with any other Managed Object, the RCW is also garbage collected. When the RCWs finalize method is invoked by the GC, the RCW will call Release() on all cached interfaces for the underlying COM object. This frees the developer from having to worry about proper AddRef() to Release() ratios because the RCW is now responsible for reference counting.

Even though the RCW manages the lifetime of the underlying COM object, there are times when it is necessary to force the release of a COM object. Consider a COM object that requires precious resources such as socket connections, database connections, or shared memory. Because there is no deterministic finalization within .NET due to the GC, it might be necessary to pragmatically release the underlying COM object. When a COM object is imported for use in a Managed environment (a topic that will be covered later), the Marshal class includes a static method called ReleaseComObject( object o) . In effect, this causes the RCW to call Release() on each cached interface within the RCW.

Proxying Unmanaged Interfaces

A typical COM object consists of a coclass that implements one or more custom interfaces. By custom interface, I'm referring to any interface not defined by COM. Standard COM interfaces include IUnknown , IDispatch , ISupportErrorInfo , IConnectionPoint , and others. Custom interfaces refer to developer-defined interfaces such as ILoan , IBank , and ISpy . The RCW must provide some mechanism that allows Managed clients to access these Unmanaged interfaces. As such, the RCW appears to implement all interfaces provided by the COM object. In effect, this allows for interface method invocation without explicitly casting to the necessary interface. Consider the following example:

Given the COM object described in Listing 5.3.1, a Managed client can invoke any interface method without an explicit cast for that interface. Listing 5.3.2 demonstrates how to interact with the class COM object detailed in Listing 5.3.1.

Listing 5.3.1 COM Component Definition
 1:  [uuid(...)]  2: interface IFoo : IDispatch {  3:     [id(...)] HRESULT FooMethod( ... );  4: }  5:  6: [uuid(...)]  7: interface IBar : IDispatch {  8:     [id(...] HRESULT BarMethod( ... );  9: } 10: 11: [uuid(...)] 12: coclass FooBar { 13:     [default] interface IFoo; 14:     interface IBar; 15: } 
Listing 5.3.2 Managed Wrapper Code
 1: FooBar fb = new FooBar( ); 2: //Invoke IFoo.FooMethod 3: //No need for explicit IFoo cast 4: fb.FooMethod( ); 5: 6: //Invoke IBar.BarMethod 7: //No need for explicit IBar cast 8: fb.BarMethod( ); 

In the classic COM world, it would be necessary to QI for a particular interface to invoke the methods associated with that particular interface.

Marshaling Method Calls

To invoke a COM Interface method, there are several tasks that need attention. Specifically, these tasks include the following:

  • Transition to Unmanaged Code

  • Error handling such as HRESULTs to exceptions

  • Parameter marshaling such as [in] , [out] , [in,out] , and [out,retval]

  • Converting between CLR data types and COM data types

Once again, .NET comes to the rescue because this functionality is provided by the generated RCW.

Consuming Selected Interfaces

Because a Managed client has no need for IUnknown or IDispatch , neither of these interfaces are exposed by the RCW. A complete listing of consumed interfaces can be found on MSDN; therefore, this chapter will not go into great detail about them.

Some of the most common interfaces, such as ISupportErrorInfo , IConnectionPoint , and IEnumVARIANT , are mapped to Managed concepts. In the case of ISupportErrorInfo , the extend error information will be propagated into the exception being thrown.

The IConnectionPoint interface maps to the concept of events and delegates within .NET. When importing a COM object that supports IConnectionPoint , all event methods will be converted to the proper event construct in C# along with the delegate definition.

TlbImp.exe

Up to this point, the concept of RCW has merely been in the abstract without a true physical entity. When it is necessary for a Managed Client to consume a class COM object, there needs to exist a Managed Wrapper ”the RCW. This task is generally accomplished with the TlbImp.exe utility that ships with the .NET SDK. TlbImp.exe can be used to create a Managed Wrapper from a COM Type Library or any COM .dll or .exe .

The following is the basic syntax for TlbImp.exe :

 TlbImp.exe TypeLibName [/out:<FileName>] 
Simple Object

To gain a better understanding of the relationship between classic COM and .NET RCW, consider the following example COM object (see Listing 5.3.3).

Listing 5.3.3 SimpleObject.h
 1: // SimpleObject.h : Declaration of the CSimpleObject  2:  3: #pragma once  4: #include "resource.h"       // main symbols  5:  6:  7: // ISimpleObject  8: [  9:     object, 10:     uuid("6D854C55-4549-44FB-9CDF-6079F56B232E"), 11:     dual,    helpstring("ISimpleObject Interface"), 12:     pointer_default(unique) 13: ] 14: __interface ISimpleObject : IDispatch 15: { 16: 17: 18:     [id(1), helpstring("method SayHello")] HRESULT SayHello([in] BSTR Name, [out, graphics/ccc.gif retval] BSTR* Message); 19: } ; 20: 21: 22: 23: // CSimpleObject 24: 25: [ 26:     coclass, 27:     threading("apartment"), 28:     aggregatable("never"), 29:     vi_progid("SimpleATL.SimpleObject"), 30:     progid("SimpleATL.SimpleObject.1"), 31:     version(1.0), 32:     uuid("D82A38B8-5392-4D3D-ADEC-516C18E6A092"), 33:     helpstring("SimpleObject Class") 34: ] 35: class ATL_NO_VTABLE CSimpleObject : 36:     public ISimpleObject 37: { 38: public: 39:     CSimpleObject() 40:     { 41:     } 42: 43: 44:     DECLARE_PROTECT_FINAL_CONSTRUCT() 45: 46:     HRESULT FinalConstruct() 47:     { 48:         return S_OK; 49:     } 50: 51:     void FinalRelease() 52:     { 53:     } 54: 55: public: 56: 57: 58:     STDMETHOD(SayHello)(BSTR Name, BSTR* Message); 59: } ; 60: 

SimpleObject.h defines a single interface ISimpleObject and a coclass CSimpleObject that implements the ISimpleObject interface. ISimpleObject defines a single method SayHello that takes a BSTR input and returns a BSTR. The RCW will marshal the BSTR type as a CLR string. Listing 5.3.4 provides the implementation for the SayHello method.

Listing 5.3.4 SimpleObject.cpp
 1: // SimpleObject.cpp : Implementation of CSimpleObject  2: #include "stdafx.h"  3: #include "SimpleObject.h"  4:  5: // CSimpleObject  6:  7: STDMETHODIMP CSimpleObject::SayHello(BSTR Name, BSTR* Message)  8: {  9:     // TODO: Add your implementation code here 10:     CComBSTR Msg( "Hello " ); Msg += Name; 11:     *Message = ::SysAllocString( Msg.m_str ); 12: 13:     return S_OK; 14: } 15: 

The SayHello method implementation merely creates a concatenated string. The ATL source was created using VS.NET and attributed ATL, in case you're interested.

To create a RCW for SimpleATL.dll , issue the following command:

 TlbImp SimpleATL.dll /out:SimpleATLImp.dll 

This will produce the necessary RCW for use by a .NET Managed Client. The Managed Wrapper can then be inspected with ILDASM; as depicted in Figure 5.3.2.

Figure 5.3.2. Managed Wrapper in ILDASM.

graphics/0503fig02.gif

Early Binding

With the RCW created, using the COM object is a simple matter of adding the appropriate reference to the project. When a reference is added to the project, the object is available for Early Binding. Early Binding allows for compile time type checking and method verification (see Listing 5.3.5). For a COM object to be available for Early Binding, it must support dual interfaces. For details on dual/dispatch interfaces, refer to a COM text. If a COM object only supports IDispatch , first ”the creator of the COM object should be flogged, and then the object can only be accessed with late binding and reflection.

Listing 5.3.5 Early Binding
 1: namespace EarlyBinding  2: {  3:     using System;  4:  5:     class Early  6:     {  7:         static void Main(string[] args)  8:         {  9:             SimpleATLImp.CSimpleObject o = new SimpleATLImp.CSimpleObject( ); 10:             Console.WriteLine(o.SayHello( "Richard" )); 11:         } 12:     } 13: } 14: 

With the generated RCW, using a COM object requires no additional code as far as the Managed client is concerned . As far as the client is aware, it is merely making use of another object.

Late Binding

Late binding refers to the use of a COM objects IDispatch interface for runtime discovery of services. The purpose of an IDispatch interface is to provide an interface for use with scripting clients. Scripting clients, such as VBScript or JScript, are unable to make use of raw interfaces and require late bound IDispatch .

.NET also provides the ability to make use of late binding through the IDispatch interface of a COM object (see Listing 5.3.6). Doing so, however, does not allow for compile time type checking. The developer must make use of the Reflection API to access instance methods and properties.

Listing 5.3.6 Late Binding
 1: namespace LateBinding  2: {  3:     using System;  4:     using System.Reflection;  5:     using System.Runtime.InteropServices;  6:  7:     class Late  8:     {  9:         static void Main(string[] args) 10:         { 11:             try 12:             { 13:                 Type SimpleObjectType = graphics/ccc.gif Type.GetTypeFromProgID("SimpleATL.SimpleObject"); 14:                 object SimpleObjectInstance = Activator.CreateInstance( graphics/ccc.gif SimpleObjectType ); 15: 16:                 Console.WriteLine("SimpleObjectType = { 0} ", graphics/ccc.gif SimpleObjectType.ToString( ) ); 17:                 Console.WriteLine("SimpleObjectInstance Type = { 0} ", 18:                  SimpleObjectInstance.GetType( ).ToString( ) ); 19: 20:                 //Invoke the SayHello Instance Method 21:                  string Message = (string)SimpleObjectType.InvokeMember("SayHello", 22:                          BindingFlags.Default  BindingFlags.InvokeMethod, 23:                          null, 24:                          SimpleObjectInstance, 25:                          new object[] {  "Richard" }  ); 26:                 //Did it work? 27:                 Console.WriteLine(Message); 28:             } 29:             catch( COMException e ) 30:             { 31:                 Console.WriteLine("What up? { 0}  : { 1} ", e.ErrorCode, e.Message ); 32:             } 33:          } 34:       } 35: } 36: 

Obviously, using a COM object with late binding requires just a bit of code. The advantage of learning how to use late binding is the ability to dynamically load COM objects and use reflection to discover the services it provides. Also, you may end up having a COM object that only supports the IDispatch interface, in which case late binding is the only way.

COM Inheritance? Blasphemy!

You can't inherit from a COM coclass; that's against the rules of COM. COM states that only interface implementation is possible and not implementation inheritance. Not anymore! Because there exists a Managed RCW for the COM object, it, like any other object, can serve as a base class in .NET.

Listing 5.3.7 highlights the extensibility of .NET and the advanced language interoperability. The details of COM have been abstracted away, thus freeing the developer to concentrate on other issues.

Listing 5.3.7 Extending CSimpleObject
 1: namespace Inherit  2: {  3:     using System;  4:  5:     /// <summary>  6:     /// Use the CSimpleObject coclass as the base class  7:     /// </summary>  8:     public class SimpleInherit : SimpleATLImp.CSimpleObject  9:     { 10: 11:         /// <summary> 12:         /// Extend the functionality of CSimpleObject 13:         /// </summary> 14:         public void NewMethod( ) 15:         { 16:             Console.WriteLine("Cool, derived from COM coclass"); 17:         } 18:     } 19: 20: 21:     class COMdotNETStyle 22:     { 23:         static void Main(string[] args) 24:         { 25:             //Create new derived class. 26:             SimpleInherit simpleInherit = new SimpleInherit( ); 27: 28:             //Invoke COM SayHelloMethod 29:             string s = simpleInherit.SayHello("Me"); 30:             Console.WriteLine(s); 31: 32:             //Invoke derived class method 33:             simpleInherit.NewMethod( ); 34:         } 35:     } 36: } 37: 
IConnectionPoint

.NET introduces the notion of events and delegates that handle those events. In COM, there exists a source and a sink. These entities are attached through the IConnectionPoint interface in which the sink requests to be advised of events from the source. When an RCW is created for a COM object that implements the IConnectionPoint interface, each event method is translated into a corresponding delegate. The notion of events and delegates with .NET is a vast improvement over the work required for classic COM events.

Each event exposed by the COM object will have a delegate with the following naming convention:

 _I<EventInterface>_<EventName>EventHandler 

Although it may not be pretty to look at, it does the job and saves you a considerable amount of work. To illustrate the process, consider a simple COM object which implements the following event interface (see Listing 5.3.8).

Listing 5.3.8 _ ISourceObjectEvents Interface
 1: // _ISourceObjectEvents  2: [  3:     dispinterface,  4:     uuid("F0507830-BD45-479D-849F-35E422A5C7FA"),  5:     hidden,  6:     helpstring("_ISourceObjectEvents Interface")  7: ]  8: __interface _ISourceObjectEvents  9: { 10: 11:     [id(1), helpstring("method OnSomeEvent")] void OnSomeEvent([in] BSTR Message); 12: } ; 13: 

The event method OnSomeEvent , line 11, will translate into an event/delegate pair when the RCW is created. The delegate will have the following, somewhat hideous, name:

 _ISourceObjectEvent_OnSomeEventEventHandler 

This delegate can then be created and attached to the event OnSomeEvent (see Listing 5.3.9).

Listing 5.3.9 SimpleSink Source
 1: namespace CSharpSink  2: {  3:     using System;  4:     using ATLSourceImp;  5:  6:     /// <summary>  7:     ///        Summary description for Class1.  8:     /// </summary>  9:     class SimpleSink 10:         { 11:             static void Main(string[] args) 12:             { 13: 14:                 CSourceObject source = new CSourceObject( ); 15:                 source.OnSomeEvent += 16:                 new _ ISourceObjectEvents_OnSomeEventEventHandler(OnSomeEvent); 17: 18:                 source.MakeEventFire( "Hello" ); 19:             } 20: 21:             public static void OnSomeEvent( string Message ) 22:            { 23:                 Console.WriteLine("Have Event: { 0} ", Message ); 24:            } 25:     } 26: } 27: 

The event hookup is no different than with other .NET event/delegate pairs. The CSimpleObject class invokes the OnSomeEvent whenever the method is invoked. The COM Interop layer handles the marshaling of the COM event and directing the event to the proper delegate on your behalf . For any of you who have had the pleasure of sinking a COM event in raw C++, this is definitely a better way.

Threading Issues

COM introduced a plethora of threading models from which to choose. COM components could be Apartment, Both, Free, MTA, Single, or STA ”so many choices, so many possible penalties. If you've never bothered to truly dive into the available COM threading models and understand COM apartments, consider yourself fortunate. However, when you need to work with classic COM objects, it is important to understand the implications of the various threading models.

A managed client creates threads within an STA. When using a COM object that also is STA, there is no penalty incurred when invoking methods on the various interfaces. However, if the COM object is MTA, it's time to pay the piper . The performance hit comes when marshaling calls from STA to MTA. Fortunately, a Managed client can change the current threading model, create the COM object, and save some cycles in the process.

To change the threading model, access the CurrentThread and set the ApartmentState as needed. Note that the ApartmentState can only be set once. Depending on the application, it might be beneficial to create a worker thread to access the classic COM object with the thread's ApartmentState set to the necessary Apartment type. Listing 5.3.10 changes the ApartmentState of the current thread to MTA to match that of the MTAObject being accessed.

Listing 5.3.10 Changing Apartments
 1: Thread.CurrentThread.ApartmentState = ApartmentState.MTA; 2: //Create MTA Object MTAObject o = new MTAObject( ); 3: o.Foo( ); 

Making use of threading model changes, the overhead of marshalling COM calls across various apartments will be reduced. In server-side code where speed is critical, saving overhead is paramount, and knowing the threading model used by the COM object allows for maximum call efficiency.

COM Types to .NET Types

The mapping of supported COM types to .NET types is fairly easy to guess. Table 5.3.1 maps out some of the most common COM types and their .NET equivalents.

Table 5.3.1. COM to .NET Type Mappings
COM Type .NET Type
BSTR string
VARIANT object
SAFEARRAY array[]
I1 sbyte
I2 short
I4 int
I8 long
UI1 byte
UI2 ushort
CHAR char
UI4 uint
R4 float
R8 double
IUnknown** object
IDispatch** object
I<SomeInterface> I<SomeInterface>

Although Table 5.3.1 is by no means complete, it does convey the basic types and their conversions. For a complete listing of the various type conversions, reference MSDN.

I l @ ve RuBoard


C# and the .NET Framework. The C++ Perspective
C# and the .NET Framework
ISBN: 067232153X
EAN: 2147483647
Year: 2001
Pages: 204

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net