Environment: .NET Framework SDK Beta 1, Visual C++ 6, SP4
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());
}//ifISomeInterface 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()
CO
M’s positionin 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.