Understanding Classic COM Interoperability w/.NET Apps
Disclaimer
The information in this article & source code are published in accordance with the Beta 1 bits of the .NET framework SDK.Ever wondered how all those COM components that we have written through the years play along with the .NET runtime. If you are a diehard COM developer interested in knowing how Classic COM Components (Yikes !.It does hurt to see COM being called Classic COM) are positioned in the .NET world, read on.
Introduction
Getting Started
Generating metadata from the COM Typelibrary
Binding to & Invoking our COM component from a .NET Application
Accessing other supported interfaces and Dynamic Type Discovery
Late Binding to COM Objects
Understanding COM Threading models & Apartments from a .NET application's perspective
COM's position in the .NET world.
After playing around with the .NET Technology Preview & recently the .NET Beta 1 bits , there is no doubt in most developers' mind that the .NET technology is a powerful way to build components and distributed systems for the enterprise. But then, what about the tons of existing reusable COM components that you've built through the last few years, not to mention all those cups of coffee & sleepless nights. Is it the end of all those components in the .NET world ? . Will those components work hand-in-hand with the .NET managed runtime ? . For all those of us who program COM for a living , and for those who live by the 'COM is love' mantra, there is good news. COM is here to stay and .NET framework managed applications can leverage existing COM components. Certainly, Microsoft wouldn't want to force companies to abandon all their existing components, especially components that were written in one of the most widely used object model for developing both desktop & distributed applications. Classic COM components interoperate with the .NET runtime through an interop layer that will handle all the plumbing between translating messages that pass back and forth between the managed runtime and the COM components operating in the unmanaged realm, and vice versa. In this article, we will focus on how you can get COM components to work with the .NET managed runtime.
So let's get started right away. Let's write a simple COM component using ATL that gives us the arrival details for a specific airline. For simplicity, we always return details for only the 'Air Scooby IC 5678' airline and return an error for any other airline. That way, you can also take a look at how the error raised by the COM component can be propagated back so that it can be caught by the calling .NET client application.
Here's the IDL definition for the IAirlineInfo interface:
interface IAirlineInfo : IDispatch { [id(1), helpstring("method GetAirlineTiming")] HRESULT GetAirlineTiming([in] BSTR bstrAirline, [out,retval] BSTR* pBstrDetails); [propget, id(2), helpstring("property LocalTimeAtOrlando")] HRESULT LocalTimeAtOrlando([out, retval] BSTR *pVal); };
And here's the implementation of the GetAirlineDetails method:
STDMETHODIMP CAirlineInfo::GetAirlineTiming(BSTR bstrAirline, BSTR *pBstrDetails) { _bstr_t bstrQueryAirline(bstrAirline); if(NULL == pBstrDetails) return E_POINTER; if(_bstr_t("Air Scooby IC 5678") == bstrQueryAirline) { // Return the timing for this Airline *pBstrDetails = _bstr_t(_T("16:45:00 - Will arrive at Terminal 3")).copy(); }//if else { // Return an error message return Error(LPCTSTR(_T("Airline Timings not available for this Airline" )), __uuidof(AirlineInfo), AIRLINE_NOT_FOUND); } return S_OK; }
So now, since we are ready with our component, let's take a look at generating some metadata from the component's type library so that the .NET client can use this metadata to talk to our component and invoke it's methods.
Generating metadata from the COM Typelibrary :
Figure 1: How the COM interop works
A .NET application that needs to talk to our COM component cannot directly consume the functionality that's exposed by it. So we need to generate some metadata. This metadata layer is used by the runtime to ferret out type information, so that it can use this type information at runtime to manufacture what is called as a Runtime Callable Wrapper (RCW). The RCW handles the actual activation of the COM object and handles the marshalling requirements when the .NET application interacts with it .The RCW also does tons of other chores like managing object identity, object lifetime, and interface caching. Object lifetime management is a very critical issue here because the .NET runtime moves objects around and garbage collects them. The RCW serves the purpose of giving the .NET application the notion that it is interacting with a managed .NET component and it gives the COM component in the unmanaged space, the impression that it 's being called by a good old COM client. The RCW's creation & behavior varies depending on whether you are early binding or late binding to the COM object. Under the hood, the RCW is doing all the hard work and thunking down all the method invocations into corresponding vtable calls into the COM component that lives in the unmanaged world. It's an ambassador of goodwill between the managed world and the unmanaged IUnknown world.
So let's generate the metadata wrapper for our Airline COM component. To do that, we need to use a tool called the TLBIMP.exe. The Type library Importer (TLBIMP ) ships with the .NET SDK and can be found under the Bin subfolder of your SDK installation. The Typelibrary Importer utility reads a typelibrary and generates the corresponding metadata wrapper containing type information that the .NET runtime can comprehend.
From the DOS command line, type the following command :
TLBIMP AirlineInformation.tlb
/out:AirlineMetadata.dll
This command tells the TLBIMP to read your AirlineInfo COM typelibrary and
generate a corresponding metadata wrapper called AirlineMetadata.dll. If
everything went off well, you should see a message such as the following :
TypeLib imported successfully to AirlineMetadata.dll
So what kind of type information does this generated metadata contain and how does it look like. As COM folks, we have always loved our beloved OleView.exe, at times when we felt we needed to take a peek at a typelibrary's contents, or for the tons of other things that OleView is capable of doing. Fortunately, the .NET SDK ships with a disassembler called ILDASM that allows us to view the metadata & the Intermediate language (IL) code generated for managed assemblies. Every managed assembly contains self-describing metadata and ILDASM is a very useful tool when you need to peek at that metadata. So go ahead and open AirlineMetadata.dll using ILDASM. Take a look at the metadata generated and you see that the GetAirlineTiming method is listed as a public member for the AirlineInfo class. There is also a constructor that gets generated for the AirlineInfo class. The method parameters also have been substituted to take their equivalent .NET counterparts. In our example the BSTR has been replaced by the System.String parameter. Also notice that the parameter that was marked [out,retval] in the GetAirlineTiming method was converted to the actual return value of the method (returned as System.String ). Any failure HRESULT values that are returned back from the COM object (in case of an error or failed business logic) are thrown back as exceptions.
Figure 2 : IL Disassembler - a great tool for viewing metadata and MSIL for managed assemblies
Binding to & Invoking our COM component from a .NET Application:
Now that we have generated the metadata that's required by a .NET client, let's try invoking the GetAirlineTiming method in our COM object from the .NET Client. So here's a simple C# client application that creates the COM object using the metadata that we generated earlier and invokes the GetAirlineTiming method.
String strAirline = "Air Scooby IC 5678"; String strFoodJunkieAirline = "Air Jughead TX 1234"; try { AirlineInfo objAirlineInfo; objAirlineInfo = new AirlineInfo(); // Call the GetAirlineTiming() method System.Console.WriteLine("Details for Airline {0} --> {1}", strAirline,objAirlineInfo.GetAirlineTiming(strAirline)); // This should make the COM object throw us an exception System.Console.WriteLine("Details for Airline {0} --> {1}", strFoodJunkieAirline,objAirlineInfo.GetAirlineTiming(strFoodJunkieAirline)); }//try catch(COMException e) { System.Console.WriteLine("Oops- We encountered an error. The Error message is : {0}. The Error code is {1}",e.Message,e.ErrorCode); }//catch
Under the hood, the runtime fabricates an RCW and this maps the metadata class methods and fields to methods and properties exposed by the interface that the COM object implements. One RCW instance is created for each instance of the COM object. The .NET runtime only cares about managing the lifetime of the RCW and garbage collects the RCW. It's the RCW that takes care of maintaining reference counts on the COM object that it's mapped to, thereby, shielding the .NET runtime from managing the reference counts on the actual COM object. As shown in the ILDASM view, the AirlineInfo metadata is defined under a namespace called AIRLINEINFORMATIONLib. The .NET client sees all the interface methods as if they were class members of the AirlineInfo class. All we need to do is, just create an instance of the AirlineInfo class using the new operator and call the public class methods of the created object. When the method is invoked, the RCW thunks down the call to the corresponding COM method call. The RCW also handles all the marshalling & object lifetime issues. To the .NET client it looks nothing more than it's actually creating a managed object and calling one of it's public class members. Anytime the COM method raises an error, the COM error is trapped by the RCW, and the error is converted into an equivalent COMException class (found in the System.Runtime.InteropServices namespace). Of course, the COM object still needs to implement the ISupportErrorInfo interface for this error propagation to work, so that the RCW knows that your object provides extended error information. The error can be caught by the .NET client by the usual try-catch exception handling mechanism and has access to the actual error number, description, the source of the exception and other details that would have been available to any COM aware client.
Accessing other supported interfaces and Dynamic Type Discovery:
So how does the classic QueryInterface scenario work from the perspective of the .NET client when it wants to access another interface implemented by the COM object. To QI for another interface, all you need to do is cast the current object to the other interface that you need, and voila, your QI is done. You are now ready to invoke all the methods/properties of the other interface. It's that simple. Again, the RCW does the all the hard work under the covers. It's a lot like how the VB runtime shields us from having to write any explicit QueryInterface related code and simply does the QI for you when you set one object type to an object of another associated type. In our example, suppose you wanted to call the methods on the IAirportFacilities interface which is another interface implemented by our COM object, you then simply cast the AirlineInfo object to the IAirportFacilities interface. You can now call all the methods that are a part of the IAirportFacilities interface. But before performing the cast, you may want to check if the object instance that you are currently holding supports or implements the interface type that you are querying for. You can do this by using the IsInstanceOf method in the System.Type class. If it returns TRUE, then you know that the QI succeeded. You can then safely perform the cast. In case you cast the object to some arbitrary interface that the object does not support, a System.InvalidCastException exception is thrown. This way the RCW ensures that you are casting to only interfaces that are implemented by the COM object and not just any arbitrary interface type.
try { AirlineInfo objAirlineInfo; IAirportFacilitiesInfo objIFacilitiesInfo; // Create a new object objAirlineInfo = new AirlineInfo(); // Check if the object implements the // IAirportFacilitiesInfo interface if(objIFacilitiesInfo.GetType().IsInstanceOf(objAirLineInfo)) { // Peform the cast objIFacilitiesInfo = (IAirportFacilitiesInfo)objAirlineInfo; // Call the method of the other interface System.Console.WriteLine("{0}",objIFacilitiesInfo.GetInternetCafeLocations()); }//if ISomeInterface objISomeJunk; //Will throw an InvalidCastException objISomeJunk = (ISomeInterface) objAirlineInfo; }//try catch(InvalidCastException eCast) { System.Console.WriteLine("We got an Invalid Cast Exception - Message is {0}", eCast.Message); }//catch
Late Binding to COM Objects :
All the examples that you saw above used the RCW metadata to early bind the .NET Client to the COM object. Though early binding provides a whole smorgasbord of benefits like strong type checking at compile time, providing auto-completion capabilities from type-information for development tools, and of course, better performance, there may be instances when you really need to late bind to a Classic COM object when you don't have the compile time metadata for the COM object that you are binding to. You can achieve late binding to a COM object through a mechanism called Reflection. This does not apply to COM objects alone. Even .NET managed objects can be late bound using Reflection. Also, if your object contains a pure dispinterface only, then you are pretty much limited to only using Reflection to activate your object and invoke methods on the interface. For late binding to a COM object, you need to know the object's ProgID. The CreateInstance static method of the System.Activator class, allows you to specify the Type information for a specific class and it will automatically create an object of that specific type. But what we really have is a ProgID and not true .NET Type Information. So we need to get the Type Information from the ProgID for which we use the GetTypeFromProgID method of the System.Type class. The System.Type class is one of the core enablers for Reflection. So now that you have created an object instance, you can invoke any of the methods/properties supported by the object's default interface using the System.Type::InvokeMember method of the Type object that you got back from GetTypeFromProgID. All we need to know is the name of the method or property and the kind of parameters that the method call accepts. The parameters are bundled up in a generic System.Object array and passed away to the method.You would also need to set the appropriate binding flags depending on whether you are invoking a method or getting/setting the value of a property. That's all there is to late binding to a COM object.
try { object objAirlineLateBound; Type objTypeAirline; object[] arrayInputParams= { "Air Scooby IC 5678" }; //Get the type information from the progid objTypeAirline = Type.GetTypeFromProgID("AirlineInformation.AirlineInfo"); // Create an instance of the object objAirlineLateBound = Activator.CreateInstance(objTypeAirline); // Invoke the 'GetAirlineTiming' method String str = (String)objTypeAirline.InvokeMember("GetAirlineTiming", BindingFlags.Default | BindingFlags.InvokeMethod, null, objAirlineLateBound, arrayInputParams); System.Console.WriteLine("Late Bound Call - Air Scooby Arrives at : {0}",str); // Get the value of a property String strTime = (String)objTypeAirline.InvokeMember("LocalTimeAtOrlando", BindingFlags.Default | BindingFlags.GetProperty, null, objAirlineLateBound, new object[]{}); Console.WriteLine ("The Local Time at Orlando,Florida is : {0}", strTime); }//try catch(COMException e) { System.Console.WriteLine("Oops- We encountered an Error. The Error message is : {0}. The Error code is {1}", e.Message,e.ErrorCode); }//catch
Understanding COM Threading models & Apartments from a .NET application's perspective :
I remember that when I first started programming
in COM, I had not yet stepped then into the murky waters of COM
Threading models and apartments and had little knowledge of what they really were. I thought it
was cool that my object was free threaded and simply assumed that it would be
the best performing threading model. Little did I realize, what was happening
under the covers. I never knew the performance penalties that would be incurred
when an STA client thread created my MTA
object. Also, since my object was not thread safe, I
never knew I would be in trouble when concurrent threads accessed my object. Truly at that
time, ignorance of COM threading models was bliss. Well, that bliss was only ephemeral and
my server started crashing unexpectedly. It was then that I was forced to get my
feet wet in the waters of COM Threading models & learn how each of those
models behaved, how COM managed apartments, and what were the performance
implications that arose when calling between two incompatible Apartments. As you
know, before a thread can call into a COM object, it has to declare it's
affiliation to an apartment by declaring whether it will enter an STA
or MTA. STA client threads call CoInitialize(NULL) or
CoInitializeEx(0, COINIT_APARTMENTTHREADED) to enter an STA
apartment and MTA threads call CoInitializeEx(0,
COINIT_MULTITHREADED) to enter an MTA. Similarly, in the .NET
managed world, you have the option of allowing the calling thread in the managed
space declare it's apartment affinity. By default, the calling thread in a
managed application chooses to live in a MTA. It's as if the calling
thread initialized itself with CoInitializeEx(0,
COINIT_MULTITHREADED).But think about the overhead and the performance
penalties that would be incurred if it were calling a classic STA COM
component that was designed to be apartment threaded. The incompatible
apartments will incur the overhead of an additional proxy/stub pair and this is
certainly a performance penalty. You can override the default choice of
Apartment for a managed thread in a .NET application by using the
ApartmentState property of the System.Threading.Thread
class.The ApartmentState property takes one of the following enumeration values:
MTA, STA, Unknown. The ApartmentState.Unknown is equivalent to
the default MTA behavior.You will need to specify the ApartmentState
for the calling thread before you make any calls to the COM object. It's not
possible to change the ApartmentState once the COM object has been created. So
it makes sense to set the thread's ApartmentState as early as possible in your
code.
// Set the client thread ApartmentState to enter an STA Thread.CurrentThread.ApartmentState = ApartmentSTate.STA; // Create our COM object through the Interop MySTA objSTA = new MySTA(); objSTA.MyMethod()
COM's position in the .NET world :
In this article, you took a look at how you can expose Classic COM components to .NET applications executing under the purview of the Common Language Runtime (CLR). You saw how the COM interop seamlessly allows you to reuse existing COM components from managed code. Then, you skimmed through ways to invoke your COM component using both early binding and late binding along with ways to do runtime type checking. Finally, you saw how managed threads decalare their Apartment affiliations when invoking COM components. As a COM developer, you might wonder if it makes sense to continue writing COM components or make the transition directly into the .NET world by writing all your components and business logic code wrapped up as managed components using one of the languages such as C#, VB.NET or any of your favorite languages that generates CLR compliant managed code. In my opinion, if you have tons of COM code out there that you just cannot port to managed code overnight, it makes sense to leverage the interop's ability to reuse existing COM components from .NET applications. But, if you are starting with writing new business logic code from scratch, then it's best to wrap your code as managed components using one of the languages that generate CLR managed code. That way, you can do away with the performance penalties that are incurred while transitioning between managed and unmanaged boundaries. So eventually, we COM developers do not have to despair. Our beloved COM components will continue to play well with .NET applications. The tools provided with the .NET framework and the COM interop mechanism make it seamless from a programming perspective as to whether your .NET application is accessing a Classic COM component or a managed component. So in essence, the marriage between COM & the brave new .NET world should be a happy one and the COM that we all know and love so much will still continue to be a quintessential part of our lives.
Comments
There are no comments yet. Be the first to comment!