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.
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 IdentityThere 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 LifetimeA 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 InterfacesA 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 Definition1: [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 Code1: 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 CallsTo invoke a COM Interface method, there are several tasks that need attention. Specifically, these tasks include the following:
Once again, .NET comes to the rescue because this functionality is provided by the generated RCW. Consuming Selected InterfacesBecause 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.exeUp 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 ObjectTo 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.h1: // 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, 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.cpp1: // 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.
Early BindingWith 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 Binding1: 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 BindingLate 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 Binding1: 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 = Type.GetTypeFromProgID("SimpleATL.SimpleObject"); 14: object SimpleObjectInstance = Activator.CreateInstance( SimpleObjectType ); 15: 16: Console.WriteLine("SimpleObjectType = { 0} ", 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 CSimpleObject1: 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 Interface1: // _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 Source1: 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 IssuesCOM 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 Apartments1: 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 TypesThe 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
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 |