(Disclaimer: The information in this article and source code are published in accordance with the Beta 2 bits of the .NET framework SDK - Build 1.0.2914.16)
Ever wondered how all those COM components that you've written through the years play along with the .NET runtime. If you are a diehard COM developer interested in knowing how Classic COM Components are positioned in the .NET world or how COM aware clients could consume .NET components, read on.
Contents
After playing around with the .NET Beta 1 & Beta 2 bits, there's 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 those of us who program with COM for a living, and for those of us who live by the 'COM is love' mantra, there is good news. COM is here to stay and .NET 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 models 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. Now, let's take a look at the other side of the fence. What if you decide to code your components using a .NET friendly language of your choice targeted for the CLR and still want to be able to consume these .NET components from COM aware clients, say, like VB 6.0 or Classic ASP?. Despair not. COM aware clients will be more than happy to party around with .NET Components through the COM Interop. The tools provided with the .NET framework allow you to expose .NET components to COM aware clients as if they were plain-vanilla COM components. The COM Interop handles all the grunge work and plumbing under the covers. In the first part of the article, we will focus on how you can get COM components to work with .NET applications and then in the latter part, we'll take a look at how you can consume .NET Components from COM aware clients in the unmanaged world. Hopefully, at the end of the article, you'd have gained enough ground to understand how Classic COM and the .NET framework can peacefully co-exist and tango together. So if you're ready, let's take a journey through exploring how Classic COM fits into the grand scheme of things in the .NET world.
Part I : Using Classic COM Components from .NET Applications
We'll begin by taking a look at how you can expose a Classic COM Component to a .NET Application. Our first order of business is to write a simple COM component using ATL that gives us the arrival details for a specific airline. For simplicity, we only return details for 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 and 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 GetAirlineTiming
method:
.......
CAirlineInfo::GetAirlineTiming(BSTR bstrAirline, BSTR *pBstrDetails)
{
_bstr_t bstrQueryAirline(bstrAirline);
if(NULL == pBstrDetails) return E_POINTER;
if(_bstr_t("Air Scooby IC 5678") == bstrQueryAirline)
{
*pBstrDetails = _bstr_t(_T("16:45:00 - Will arrive at Terminal 3")).copy();
}
else
{
return Error(LPCTSTR(_T("Airline Timings not available for this Airline" )),
__uuidof(AirlineInfo), AIRLINE_NOT_FOUND);
}
return S_OK;
}
.......
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.
Consuming a Classic COM Component from a .NET application
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 glean 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 marshaling 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 v-table 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.
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 framework SDK and can be found under the Bin subfolder of your SDK installation. The Type library Importer utility reads a type library 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 type library importer to read your AirlineInfo
COM type library and generate a corresponding metadata wrapper called AirlineMetadata.dll. If everything went through fine, you should see a message indicating that the metadata proxy has been generated from the type library:
Type library imported to E:\COMInteropWithDOTNET\AirlineMetadata.dll
What kind of type information does this generated metadata contain and what does it look like? As COM folks, we've always loved our beloved OleView.exe, at times when we felt we needed to take a peek at a type library'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 take a peek at that IL code and metadata. Go ahead and open AirlineMetadata.dll using ILDASM. Take a look at the metadata generated and you will see that the GetAirlineTiming
method is listed as a public method of the IAirlineInfo
interface that is implemented by the AirlineInfo
class. There is also a constructor that gets generated for the AirlineInfo
class. The datatypes for the method parameters and return values have also been substituted to take their equivalent .NET counterparts. In our example, the GetAirlineTiming
method's parameter with the BSTR
datatype has been replaced by the string
(an alias for System.String
) datatype. Also notice that the parameter that was marked [out,retval]
in the GetAirlineTiming
method has been converted to the actual return value of the method (returned as a string
). Any failure HRESULT
values that are returned back from the COM object (in case of an error or failed run-of-the-mill business logic) are thrown back as .NET exceptions.
IL Disassembler - a great tool for viewing metadata and MSIL for managed assemblies
Now that we have generated the metadata that is required by a .NET client, let's try invoking the GetAirlineTiming
method in our COM object from the .NET Client. Here's a simple C# client application that creates the COM object using the metadata that we generated earlier and invokes the GetAirlineTiming
method. The first method call invocation should go through fine and we get back the details of airline "Air Scooby IC 5678". Then, we'll pass in an unknown airline, "Air Jughead TX 1234", so that the COM object throws us the AIRLINE_NOT_FOUND
error that we defined.
.......
String strAirline = "Air Scooby IC 5678";
String strFoodJunkieAirline = "Air Jughead TX 1234";
try
{
AirlineInfo objAirlineInfo;
objAirlineInfo = new AirlineInfo();
System.Console.WriteLine("Details for Airline {0} --> {1}",
strAirline,
objAirlineInfo.GetAirlineTiming(strAirline));
System.Console.WriteLine("Details for Airline {0} --> {1}",
strFoodJunkieAirline,
objAirlineInfo.GetAirlineTiming(
strFoodJunkieAirline));
}
catch(COMException e)
{
System.Console.WriteLine("Oops an error occured !. Error Code is : {0}.
Error message is : {1}",e.ErrorCode,e.Message);
}
.......
Here's how the output would look like:
Details for Airline Air Scooby IC 5678 --> 16:45:00 - Will arrive at Terminal 3
Oops an error occured !. Error Code is : -2147221502.
Error message is : Airline Timings not available for this Airline
Under the hood, the runtime fabricates an RCW and this maps the metadata proxy's 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 class is defined under a namespace called AirlineMetadata
. This class implements the IAirlineInfo
interface. All you 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. The RCW also handles all the marshaling & object lifetime issues. To the .NET client, it looks nothing more than it's actually creating a managed object and calling one of its 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
and IErrorInfo
interfaces for this error propagation to work, so that the RCW knows that the object provides extended error information. The error is caught by the .NET client using 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. You could also return standard HRESULT
s back and the RCW will take care of mapping them to the corresponding .NET exceptions to throw back to the client. For example, if you were to return a HRESULT
of E_NOTIMPL
from your COM method, then the RCW will map this to the .NET NotImplementedException
exception and throw an exception of that type.
Please refer to the Exception Handling section in this article, to learn more about how .NET exceptions are mapped to COM HRESULT
s.
How does the classic QueryInterface
scenario work from the perspective of the .NET client when it needs to check if the COM Object implements a specific interface? To QI for another interface, all you need to do is cast the object to the interface that you are querying for, and if it succeeds, voil�, your QI has succeeded as well. In case you attempt to cast the object to some arbitrary interface that the object does not support, a System.InvalidCastException
exception is thrown, indicating that the QI has failed. It's that simple. Again, the RCW does 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.
An alternate way to check if the object instance that you are currently holding supports or implements a specific interface type is to use C#'s 'is
' operator. The 'is
' operator does runtime type checking to see if the object can be cast safely to a specific type. If it returns true, then you can safely perform a cast to get the QI done for you. 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. You can also use C#'s 'as
' operator to cast from one type to another compatible type as shown in the example below. These simple constructs are all what you need to keep swinging between interfaces that the COM object supports in a type safe manner.
.......
try
{
AirlineInfo objAirlineInfo = null;
IAirportFacilitiesInfo objFacilitiesInfo = null;
objAirlineInfo = new AirlineInfo();
String strDetails = objAirlineInfo.GetAirlineTiming(strAirline);
if(objAirlineInfo is IAirportFacilitiesInfo)
{
objFacilitiesInfo = (IAirportFacilitiesInfo)objAirlineInfo;
objFacilitiesInfo = objAirlineInfo as IAirportFacilitiesInfo;
System.Console.WriteLine("{0}",
objFacilitiesInfo.GetInternetCafeLocations());
}
if(objAirlineInfo is IJunkInterface)
{
System.Console.WriteLine("We should never get here ");
}
else
{
System.Console.WriteLine("I'm sorry I don't implement" +
" the IJunkInterface interface ");
}
IJunkInterface objJunk = null;
objJunk = (IJunkInterface)objAirlineInfo;
}
catch(InvalidCastException eCast)
{
System.Console.WriteLine("Here comes trouble" +
" ... Error Message : {0}",
eCast.Message);
}
.......
Here's how the output would look like:
Your nearest Internet Cafe is at Pavilion 3 in Terminal 2 -
John Doe's Sip 'N' Browse Cafe
I'm sorry I don't implement the IJunkInterface interface
Here comes trouble ... Error Message :
An exception of type System.InvalidCastException was thrown.
All the examples that you saw above used the metadata proxy 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 late bind 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 and loaded using Reflection. Also, if your object implements a pure dispinterface only, then you are pretty much limited to only using Reflection to activate your object and invoke methods on its interface. For late binding to a COM object, you need to know the object's ProgID or CLSID. 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 COM ProgID and COM CLSID for our COM object and not true .NET Type Information. So we need to get the Type information from the ProgID or CLSID using the GetTypeFromProgID
or GetTypeFromCLSID
static methods of the System.Type
class. The System.Type
class is one of the core enablers for Reflection. After creating an instance of the object using Activator.CreateInstance
, you can invoke any of the methods/properties supported by the object using the System.Type.InvokeMember
method of the Type object that you got back from Type.GetTypeFromProgID
or Type.GetTypeFromCLSID
. All you 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 into 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" };
objTypeAirline =
Type.GetTypeFromProgID("AirlineInformation.AirlineInfo");
"{F29EAEEE-D445-403B-B89E-C8C502B115D8}"));
objAirlineLateBound = Activator.CreateInstance(objTypeAirline);
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);
String strTime = (String)objTypeAirline.InvokeMember("LocalTimeAtOrlando",
BindingFlags.Default |
BindingFlags.GetProperty,
null,
objAirlineLateBound,
new object[]{});
Console.WriteLine ("Late Bound Call - Local" +
" Time at Orlando,Florida is: {0}",
strTime);
}
catch(COMException e)
{
System.Console.WriteLine("Error code : {0}, Error message : {1}",
e.ErrorCode, e.Message);
}
.......
Take a look at the output that you'd get:
Late Bound Call - Air Scooby Arrives at 16:45:00 - Will arrive at Terminal 3
Late Bound Call - Local Time at Orlando,Florida is: Sun Jul 15 16:50:01 2001
The Connection Points event handling mechanism, as you know, is one of the primary enablers for bi-directional communication between your COM components and the consumers of your components. Just to jog your memory, I'll brief you a little bit on the event handling mechanism in Classic COM Components. Typically, COM components that support event notifications have what is called, an outgoing interface. The outgoing interface is used by the component to call into the client when a specific event has occurred. Outgoing interfaces are marked with the [source] attribute in the coclass section of the component's IDL file. The [source]
attribute in the IDL allows development tools and IDEs to parse the typelibrary to check to see if the object supports an outgoing interface. Consumers or clients of these components usually set up a sink object, which implements this outgoing interface. An interface pointer to this sink object is passed by the client to the component. The component stashes away this interface pointer in typically, something like a map that contains a list of outgoing interface pointers to sink objects that are interested in receiving notifications from the component. Whenever a component needs to raise an event, it uses the map to get a list of interface pointers to the sink objects that have subscribed for notifications. It then notifies them by calling the respective method on the outgoing interface implemented by the sink object.
Connection Points in Classic COM
Essentially, a COM object that supports outgoing interfaces, implements the IConnectionPointContainer interface. A client that wants to receive event notifications does a QI on the COM object for the IConnectionPointContainer interface to see if it supports outgoing interfaces. If the QI fails, then the object does not support events. If the QI succeeds, the client calls the FindConnectionPoint method (could also call EnumConnectionPoints ) on the IConnectionPointContainer interface by passing it the IID of the outgoing interface. If such an interface is supported, the client receives back an IConnectionPoint interface pointer corresponding to the outgoing interface. It then calls the IConnectionPoint::Advise method and passes to it, the sink object's IUnknown pointer. The COM object adds this IUnknown pointer to its map to keep a list of the sink objects that have subscribed for notifications. The client gets back a cookie from the COM object that it can subsequently use to revoke event notifications. When the COM object needs to raise events, it iterates through the map, gets a list of all the sink object interface pointers and calls the corresponding event methods on the outgoing interface (implemented by the sink object). When a client no longer desires to receive notifications, it removes itself from the object's map by calling the IConnectionPoint::Unadvise method by passing it the cookie that it received earlier on the IConnectionPoint::Advise call.
Connection Points in Classic COM
In simple terms, that's how the event handling mechanism & bi-directional communication works in Classic COM components. Most books that teach COM programming usually have a full chapter dedicated to explain this architecture and you might want to look them up to further your understanding in this topic. |
Creating an ATL COM Component that sources events
Let's take a look at how the Connection Points event handling mechanism in COM translates to the delegate event handling mechanism in the .NET world. We'll take a look at how you can use .NET managed event sinks to catch event notifications sent from COM objects. To get started, let's take a look at the COM object that's going to source events to your .NET application. Let's put together a simple COM object that will page your .NET application, whenever an airline arrives at a fictitious airport called John Doe International Airport. We will subscribe to this paging service from our .NET application so that we get paged whenever an airplane taxies down on John Doe's runway.
We'll create an ATL EXE project that hosts an object called AirlineArrivalPager
. The AirlineArrivalPager
object supports an incoming interface called IAirlineArrivalPager
and an outgoing interface called _IAirlineArrivalPagerEvents
. Here's the interface definition of the _IAirlineArrivalPagerEvents
outgoing interface. This interface is marked with the [source] attribute in the coclass definition.
.....
interface IAirlineArrivalPager : IDispatch
{
[id(1), helpstring("method AddArrivalDetails")]
HRESULT AddArrivalDetails([in] BSTR bstrAirlineName,
[in] BSTR bstrArrivalTerminal);
};
....
dispinterface _IAirlineArrivalPagerEvents
{
properties:
methods:
[id(1), helpstring("method OnAirlineArrivedEvent")]
HRESULT OnAirlineArrivedEvent([in] BSTR bstrAirlineName,
[in] BSTR bstrArrivalTerminal);
};
....
coclass AirlineArrivalPager
{
[default] interface IAirlineArrivalPager;
[default, source] dispinterface _IAirlineArrivalPagerEvents;
};
.......
Take a look at the implementation of the incoming IAirlineArrivalPager
interface's AddArrivalDetails
method:
.......
STDMETHODIMP CAirlineArrivalPager::AddArrivalDetails(
BSTR bstrAirlineName,BSTR bstrArrivalTerminal)
{
Fire_OnAirlineArrivedEvent(bstrAirlineName,bstrArrivalTerminal);
return S_OK;
}
.......
The implementation of this method uses the Fire_OnAirlineArrivedEvent
helper method to notify all sink objects implementing _IAirlineArrivalPagerEvents
that have subscribed for event notifications. The Fire_OnAirlineArrivedEvent
is a method of the helper proxy class derived from IConnectionPointImpl
that is generated automatically by the ATL Implement Connection point wizard. Essentially, it iterates through the map where it stored the interface pointers to the sink objects when IConnectionPoint::Advise
was called, and uses these interface pointers to call the event notification method (OnAirlineArrivedEvent
) implemented by the client's sink object.
If you were a C++ programmer coding a COM aware client application to receive notifications, you would set up a sink object in the client application that implements the _IAirlineArrivalPagerEvents
interface. You would then create the AirlineArrivalPager
object and pass it the IUnknown
interface pointer of the sink object through a call to IConnectionPoint::Advise
or use a helper method such as AtlAdvise
to wire your sink to the object that raises events, so that you can receive event notifications. With VB 6.0, it's as simple as using the WithEvents
keyword in your declaration and defining a handler function for receiving notifications. VB will do all the hard work under the covers to hook up the notifications made on the outgoing interface to the appropriate handler function.
Event handling using Delegates
If you are already familiar with how delegates are used in .NET, you might want to skip this section and swing by to the next section. Event handling in .NET is primarily based on the Delegate Event model. A delegate is something akin to function pointers that we use in C/C++. The delegate based Event model was popularized by the simplicity of its use, right from the WFC (Windows Foundation Classes in Visual J++) days. Delegates allow an event raised by any component to be connected to a handler function or method of any other component as long as the function signatures of the handler function or method matches the exact signature of that of the delegate. Take a look at this simple example below that shows you how you can put delegates to action.
delegate string SayGoodMorning();
public class HelloWorld
{
public string SpeakEnglish() {
return "Good Morning";
}
public string SpeakFrench() {
return "Bonjour";
}
public static void Main(String[] args) {
HelloWorld obj = new HelloWorld();
SayGoodMorning english = new SayGoodMorning(obj.SpeakEnglish);
SayGoodMorning french = new SayGoodMorning(obj.SpeakFrench);
System.Console.WriteLine(english());
System.Console.WriteLine(french());
}
}
Here's the output that you get back:
Good Morning
Bonjour
In the example above, we declare a delegate called SayGoodMorning
. Then we wire the delegate to reference the SpeakEnglish
and SpeakFrench
methods of the HelloWorld
object. All that is required is that the SpeakEnglish
and SpeakFrench
methods have the same signature as that of the SayGoodMorning
delegate. The reference is typically made by instantiating the delegate as if it were an object and passing in the referenced method as its parameter. The referenced method could either be an instance method or a static method of a class. The delegate maintains the reference it needs to call the right handler for the event. This makes delegates first class object-oriented citizens and they are also type-safe and secure to deal with. The .NET event-handling model is based primarily on the delegate event model. Take a look at the following example:
......
private System.Windows.Forms.Button AngryButton = new Button();
....
AngryButton.Click += new System.EventHandler(AngryButton_Click);
.....
protected void AngryButton_Click(object sender,EventArgs e)
{
MessageBox.Show("Please Stop clicking me !!");
}
.......
When your application deals with controls and wants to receive specific notifications, it creates a new instance of an EventHandler delegate that contains a reference to the actual handler function that will handle the events raised by the control. In the example shown above, the EventHandler delegate contains a reference to the AngryButton_Click
method. The AngryButton_Click
method needs to have the same method signature as that of the EventHandler delegate. Here's how the signature of the System.EventHandler
delegate looks like:
public delegate void EventHandler(object sender, EventArgs e);
The EventHandler
delegate instance will then have to be added to the Click
event's list of delegate instances. Delegates that extend the System.MulticastDelegate
allow you to add multiple handler functions to the delegate's invocation list using C# operators such as +=
and -=
which are wrappers for the Delegate.Combine
and Delegate.Remove
methods. Using an event provides users with a foolproof scheme to only add or remove delegate instances to the event using the +=
and -=
operator and not accidentally overwrite the invocation list. When the control raises an event, each of the delegates that have been added to the button's Click
event will be invoked and the delegate will route it to the correct handler function that it references.
In our example, whenever the Click
Event occurs in the button the call will be routed to the AngryButton_Click
method. I guess this gives you a fairly good idea on the role played by delegates and events in the event-handling mechanism in the .NET framework. The reason I explained to you how delegates work is because it's one of the primary enablers of the .NET event handling model and it's important to understand this to appreciate how .NET applications use delegates to subscribe to event notifications from Classic COM Components.
Sinking Unmanaged COM Events in a .NET Application
Here's a simple VB Client application that assumes the role of the Control Tower at John Doe International Airport and calls the AddArrivalDetails
method in the incoming interface. The implementation of this method in turn triggers the event notifications that are subsequently caught by the handler functions in the .NET application that have subscribed for OnAirlineArrivedEvent
event notifications. The AirlineArrivalPager
COM object is itself a singleton object hosted in an out-of-proc COM server. So, the same instance of the object services both the VB based Control tower application (that triggers events) and the .NET pager applications that have subscribed for OnAirlineArrivedEvent
event notifications.
Dim AirlinePager As New AIRLINENOTIFYLib.AirlineArrivalPager
Private Sub AirlineArrived_Click()
AirlinePager.AddArrivalDetails Me.AirlineName, Me.ArrivalTerminal
End Sub
With that said, let's see how a .NET managed application receives event notifications generated by the AirlineArrivalPager
COM object. Firstly, you need to generate a .NET metadata proxy from the COM object's typelibrary, so that it can be consumed by a .NET application. Let's use the Type Library Importer (TLBIMP) to generate the metadata proxy assembly for us.
tlbimp AirlineNotify.tlb /out:AirlineNotifyMetadata.dll
This metadata proxy will be referenced in your .NET application. Here's a simple .NET Windows Forms application that subscribes to event notifications from the AirlineArrivalPager
COM component using delegates.
......
using AirlineNotifyMetadata;
public class AirlineNotifyForm : System.WinForms.Form
{
private System.Windows.Forms.CheckBox checkBoxPaging;
private System.Windows.Forms.ListBox listPager;
private AirlineArrivalPager m_pager = null;
......
public AirlineNotifyForm() {
.....
subscribePaging();
}
......
void subscribePaging() {
m_pager = new AirlineArrivalPager();
m_pager.OnAirlineArrivedEvent +=
new _IAirlineArrivalPagerEvents_OnAirlineArrivedEventEventHandler(
OnMyPagerNotify);
}
protected void checkBoxPaging_CheckedChanged (object sender,
System.EventArgs e) {
if(checkBoxPaging.Checked) {
m_pager.OnAirlineArrivedEvent +=
new _IAirlineArrivalPagerEvents_OnAirlineArrivedEventEventHandler(
OnMyPagerNotify);
}
else {
m_pager.OnAirlineArrivedEvent -= new
_IAirlineArrivalPagerEvents_OnAirlineArrivedEventEventHandler(
OnMyPagerNotify);
}
}
public int OnMyPagerNotify(String strAirline, String strTerminal) {
StringBuilder strDetails = new StringBuilder("Airline ");
strDetails.Append(strAirline);
strDetails.Append(" has arrived in ");
strDetails.Append(strTerminal);
listPager.Items.Insert(0,strDetails);
return 0;
}
}
The line of code that is most important here is the line:
m_pager.OnAirlineArrivedEvent += new
_IAirlineArrivalPagerEvents_OnAirlineArrivedEventEventHandler(
OnMyPagerNotify);
If you understand the semantics of how delegates work, you should have absolutely no problem comprehending what's going on here. What you're doing is adding the _IAirlineArrivalPagerEvents_OnAirlineArrivedEventEventHandler
delegate instance that references the OnMyPagerNotify
method to the OnAirlineArrivedEvent
event list. Usually the name of the event (OnAirlineArrivedEvent
) is the same as the method name in the outgoing interface. The delegate name (_IAirlineArrivalPagerEvents_OnAirlineArrivedEventEventHandler
) usually follows the pattern InterfaceName_EventNameEventHandler
. That's all there is to receiving event notifications from COM components. All you need to do is create an instance of the component and then add a delegate referencing your handler function to the event list. Effectively, what you are doing here is something that's analogous to the IConnectionPoint::Advise
in the COM world. Whenever the OnAirlineArrivedEvent
event is raised by the COM component, the OnMyPagerNotify
method will be called to handle the event notification. It's that simple in .NET, to wire a handler sink to receive event notifications from a COM object that sources events.
How the Connection Point Event Handling mechanism in Classic COM maps to the Delegate event handling mechanism in .NET
When you no longer want to receive notifications, you can remove the delegate from the event list by calling:
m_pager.OnAirlineArrivedEvent -= new
_IAirlineArrivalPagerEvents_OnAirlineArrivedEventEventHandler(OnMyPagerNotify);
This is analogous to the IConnectionPoint::Unadvise
method call that revokes further notifications by removing your sink object's interface pointer from the map using the cookie that you received in the Advise call. But, who handles the mapping between the Connection point event handling model in Classic COM and delegate event model in .NET? The metadata proxy generated by the Typelibrary importer (TLBIMP) contains classes that act as adaptors to wire the Connection point Event Model in the unmanaged world to the Delegate based event model in the .NET world via the RCW stub that is created at runtime. If you are interested in examining what happens under the hood, I encourage you to open up the metadata proxy (AirlineNotifyMetadata.dll) using the IL Diassembler (ILDASM) and examine the MSIL code for the various methods in the helper classes.
Using COM Collections from .NET Applications
Using COM based Collections allows you to categorize objects together as a part of the group that exhibit similar behavior. For example, a BookCollection
could model all the books in a library. Each Book object stored in this collection could represent the details of the book such as the Author, ISBN etc. Iterating through the collection is extremely simple and allows you to get back objects on demand that have been added to the collection. There are other methods to model collections too, such as using SAFEARRAYs. The problem with a SAFEARRAY
is that, if the collection is large, it involves moving entire data chunks that the SAFEARRAY
represents across to the client. Using collections allows you to get data on demand. Also, it's much more elegant to iterate through a collection from clients such as VB, using the For Each..Next
syntax. If you have existing COM objects that represent COM based collections, they would continue to work well with .NET applications. These collections can be enumerated just as easily by .NET clients. We'll soon see how. If you are already familiar with how collections work in Classic COM, you might want to skip this section and go to the next section.
Creating a COM collection Component using ATL
For the VB folks, who feel more at home with churning out COM components with VB, you might want to skip this section and go to the Creating a COM collection Component using VB section. First, let's put together a simple COM Component using ATL that models an ice cream collection. The Collection class represents the menu at an ice cream parlor that contains a variety of ice cream flavors. Take a look at the IDL file for this component.
[
....
]
interface IIceCreamMenu : IDispatch
{
[propget, id(1), helpstring("property Count")]
HRESULT Count([out, retval] long *pVal);
[propget, id(DISPID_NEWENUM),
helpstring("property _NewEnum"), restricted]
HRESULT _NewEnum([out, retval] LPUNKNOWN *pVal);
[propget, id(DISPID_VALUE), helpstring("property Item")]
HRESULT Item([in] long lIndex,
[out, retval] VARIANT *pVal);
[id(2), helpstring("method AddFlavortoMenu")]
HRESULT AddFlavortoMenu([in] BSTR bstrNewFlavor);
};
The Count
, _NewEnum
, and Item
property are standard properties that every COM Collection supports. The _NewEnum
property allows you to enumerate through the collection using constructs like For Each .. Next
in VB and is always assigned a DISPID of DISPID_NEWENUM
(-4) to indicate that it's the enumerator for the collection. This property usually returns an IUnknown
interface pointer to an object that implements the IEnumVariant
interface. The IEnumVariant
interface provides all the methods that you need (such a Next
, Skip
, Reset
, Clone
) for enumerating a collection containing VARIANTs. The Item
property is assigned a DISPID of DISPID_VALUE
(0) to indicate that this is the default property to be used, if the property name is omitted. The Item
property allows you to locate an item in the collection using an index. The index itself can be any type based on your business model. For example, a Book Collection could provide an ISBN Number as a string for its Item Index that could act as a Key Value in an STL map to locate the corresponding book. In the ice cream menu example, we use an Index of type long
to locate an ice cream at a specified index. Also, since we have modeled our collection based on an STL vector, an index of type long is convenient to access a specific element in the vector. The base class ICollectionOnSTLImpl<>
that our IceCreamMenu
collection component derives from provides us with a default implementation of Item
based on an index of type long
. The Count
property returns the number of elements in the collection. All 3 of them are read-only properties. Other than these properties, you could add any number of helper methods that allow you to add, remove and update elements in your collection. Take a look at the code for the IceCreamMenu
collection component below, coded using ATL.
.......
class _CopyPolicyIceCream;
typedef vector<_bstr_t> ICECREAM_MENU_VECTOR;
typedef CComEnumOnSTL< IEnumVARIANT, &IID_IEnumVARIANT, VARIANT,
_CopyPolicyIceCream, ICECREAM_MENU_VECTOR > VarEnum;
typedef ICollectionOnSTLImpl< IIceCreamMenu,
ICECREAM_MENU_VECTOR, VARIANT,
_CopyPolicyIceCream,
VarEnum > IceCreamCollectionImpl;
class _CopyPolicyIceCream
{
public:
static HRESULT copy(VARIANT* pVarDest,_bstr_t* bstrIceCreamFlavor)
{
CComVariant varFlavor((TCHAR *)(*bstrIceCreamFlavor));
return ::VariantCopy(pVarDest,&varFlavor);
}
static void init(VARIANT* pVar)
{
pVar->vt = VT_EMPTY;
}
static void destroy(VARIANT* pVar)
{
VariantClear(pVar);
}
};
class ATL_NO_VTABLE CIceCreamMenu :
public CComObjectRootEx< CComSingleThreadModel >,
public CComCoClass< CIceCreamMenu, &CLSID_IceCreamMenu >,
public ISupportErrorInfo,
public IDispatchImpl< IceCreamCollectionImpl,
&IID_IIceCreamMenu,
&LIBID_ICECREAMPARLORLib, 1, 0 >
{
public:
...........
public:
STDMETHOD(AddFlavortoMenu)( BSTR bstrNewFlavor);
};
The ICollectionOnSTLImpl<>
class that the CIceCreamMenu
class extends provides the default implementation for the Item
, Count
, and _NewEnum
collection properties. The underlying collection type that it represents (in our case, a vector containing _bstr_t
strings) is denoted by the m_coll
instance. To add items to the collection, you just need to populate the m_coll
with the elements in your collection. This is what the FinalConstruct
attempts to do by adding some ice cream flavors into the vector< _bstr_t >
represented by m_coll
.
.......
HRESULT CIceCreamMenu::FinalConstruct()
{
m_coll.push_back(_bstr_t(_T("Chocolate Almond Fudge")));
m_coll.push_back(_bstr_t(_T("Peach Melba")));
m_coll.push_back(_bstr_t(_T("Black Currant")));
m_coll.push_back(_bstr_t(_T("Strawberry")));
m_coll.push_back(_bstr_t(_T("Butterscotch")));
m_coll.push_back(_bstr_t(_T("Mint Chocolate Chip")));
return S_OK;
}
STDMETHODIMP CIceCreamMenu::AddFlavortoMenu(BSTR bstrNewFlavor)
{
m_coll.push_back(_bstr_t(bstrNewFlavor));
return S_OK;
}
Creating a COM collection Component using VB
For the benefit of the VB folks, here's an equivalent implementation of the IceCreamMenu
COM collection class in VB. Be sure to tag the NewEnum
function with a DISPID of -4 (DISPID_NEWENUM
). You can do this by using the Tools->Procedure Attributes dialog box in the VB IDE. You need to set the Procedure ID for NewEnum to -4 and also make sure that you turn on the Hide this member attribute check box.
Option Explicit
Private mIceCreamFlavors As Collection
Private Sub Class_Initialize()
Set mIceCreamFlavors = New Collection
mIceCreamFlavors.Add "Chocolate Almond Fudge"
mIceCreamFlavors.Add "Peach Melba"
mIceCreamFlavors.Add "Black Currant"
mIceCreamFlavors.Add "Strawberry"
mIceCreamFlavors.Add "Butterscotch"
mIceCreamFlavors.Add "Mint Chocolate Chip"
End Sub
Public Function Count() As Integer
Count = mIceCreamFlavors.Count
End Function
Public Function Item(varIndex As Variant) As String
Item = mIceCreamFlavors(varIndex)
End Function
Public Function NewEnum() As IEnumVARIANT
Set NewEnum = mIceCreamFlavors.[_NewEnum]
End Function
Public Function AddFlavortoMenu(strNewFlavor As String)
mIceCreamFlavors.Add strNewFlavor
End Function
Consuming COM Collections in a .NET Application
To get a .NET application to consume the collection COM component that we just coded in the last section, we will need to generate the .NET metadata proxy from the component's typelibrary. You can do this using the following command from the command-line:
tlbimp IceCreamParlor.tlb /out:IceCreamMenuMetadata.dll
Now open the IceCreamMenuMetadata.dll using the IL Disassembler (ILDASM) and take a look at the methods generated for the IceCreamMenu
class.
Metadata proxy generated by TLBIMP for the IceCreamMenu collection component
The IceCreamMenu
class implements two interfaces: the IIceCreamMenu
interface and the System.Collections.IEnumerable
interface. Implementing the IEnumerable
interface tells consumers that the class allows you to enumerate through elements in its collection. The IIceCreamMenu
interface in the metadata proxy object has the Count
and Item
property preserved from the COM component's IIceCreamMenu
interface. But what has TLBIMP done to the _NewEnum
property that represents our collection's enumerator?. It has replaced that with the GetEnumerator()
method that returns the IEnumerator
interface of the object that handles the actual enumeration.
Consuming COM collections in .NET Applications
Since the IceCreamMenu
class implements the IEnumerable
interface, you could use extremely simple constructs such as C#'s foreach
statement to enumerate through elements in such a collection. Here's how you could consume the IceCreamMenu
collection COM component from your .NET application.
using System;
using IceCreamMenuMetadata;
public class IceCreamMenuClient
{
public static void Main(String[] args)
{
IceCreamMenu menu = new IceCreamMenu();
menu.AddFlavortoMenu("Blueberry");
menu.AddFlavortoMenu("Chocolate Chip");
foreach(Object objFlavor in menu)
{
System.Console.WriteLine("{0}",objFlavor);
}
}
}
You can compile the above code using the following command-line:
csc /target:exe /r:IceCreamMenuMetadata.dll
/out:IceCreamMenuClient.exe IceCreamMenuClient.cs
Here's the output that you get:
Chocolate Almond Fudge
Peach Melba
Black Currant
Strawberry
Butterscotch
Mint Chocolate Chip
Blueberry
Chocolate Chip
Take a look at how easy it is to use C#'s foreach
construct to iterate through the elements in the collection. Again, the RCW powers the enumeration under the covers by translating IEnumVARIANT
based COM Collection semantics into a representation that can be serviced by IEnumerator
based methods and frees us from all the marshaling rigmarole.
Enumerating elements in a .NET Collection
The IEnumerable and the IEnumerator interfaces are the primary enablers for enumerating collections in the .NET world. As mentioned earlier, implementing the IEnumerable interface tells consumers that the object allows you to enumerate through elements in its collection The IEnumerator interface consists of two methods: MoveNext and Reset , and one property: Current , that needs to be implemented by the object that provides the Enumerator for the collection. If your class is based on a simple collection such as an array, you need to just make an index move back and forth across the array for the MoveNext implementation. You would need to reset the index to point to the start of the array in the implementation of Reset and would need to return the array element at the current index position for the Current property's implementation. Take a look at this example below so that things become a little clearer as to what these two interfaces are required to do if you were to implement a .NET class that modeled a collection and allowed enumeration. using System;
using System.Collections;
public class SevenDwarfs : IEnumerable , IEnumerator
{
private int nCurrentPos = -1;
private string[] strArrayDwarfs =
new String[7] {"Doc", "Dopey", "Happy", "Grumpy",
"Sleepy", "Sneezy" , "Bashful"};
SevenDwarfs() {}
public IEnumerator GetEnumerator()
{
return (IEnumerator)this;
}
public bool MoveNext()
{
if(nCurrentPos < strArrayDwarfs.Length - 1)
{
nCurrentPos++;
return true;
}
else
{
return false;
}
}
public void Reset()
{
nCurrentPos = -1;
}
public object Current
{
get
{
return strArrayDwarfs[nCurrentPos];
}
}
public static void Main(String[] args)
{
SevenDwarfs SnowWhitesDwarfs = new SevenDwarfs();
foreach(string dwarf in SnowWhitesDwarfs)
{
System.Console.WriteLine("{0}",dwarf);
}
}
}
You can compile the above code using the following command-line: csc /target:exe /out:SevenDwarfs.exe SevenDwarfs.cs
Here's the output that you get back: Doc
Dopey
Happy
Grumpy
Sleepy
Sneezy
Bashful |
Mapping method parameter keywords in C# to IDL's Directional attributes
There are certain rules that the Interop uses when mapping method parameter keywords in C# such as out, ref to their corresponding directional attributes such as [in]
, [out]
, [in,out]
, [out,retval]
and vice versa.
- When the method parameter is not qualified by a keyword in C#, it usually gets mapped to the [in] attribute in IDL assuming pass-by-value semantics.
- The return value from the C# method is always mapped to the
[out, retval]
directional attribute in IDL.
- The
ref
method parameter keyword gets mapped to the [in,out]
directional attribute in IDL.
- The
out
method parameter keyword gets mapped to an [out]
directional attribute in IDL.
- Errors that occur in the .NET world are not returned using the return value of the method. But are instead thrown as exceptions.
Read more about error handling here. Here are a few examples of how C# parameter types map to the directional attributes in IDL:
C# Method |
IDL Equivalent |
Calling semantics in C# |
public void Method(String strInput); |
HRESULT Method([in] BSTR strInput); |
obj.Method("Hello There"); |
public String Method(); |
HRESULT Method([out, retval] BSTR* pRetVal); |
String strOutput = obj.Method(); |
public String Method(ref String strPassAndModify); |
HRESULT Method([in, out] BSTR* strPassAndModify, [out, retval] BSTR* pRetVal); |
String strHello = "Hello There"; String strOutput = obj.Method(ref strHello); |
public String Method(out String strReturn); |
HRESULT Method([out] BSTR* strReturn, [out, retval] BSTR* pRetVal); |
|
public String Method(String strFirst, out String strSecond, ref String strThird); |
HRESULT Method([in] BSTR bstrFirst, [out] BSTR* strSecond, [in, out] BSTR* strThird, [out, retval] BSTR* pRetVal); |
String strFirst = "Hi There"; String strSecond; String strThird = "Hello World"; String strOutput = obj.Method(strFirst,out strSecond, ref strThird); |
One of the nice features of the interop is that you can have your managed .NET class use the inheritance or containment models to reuse functionality provided by an existing COM component. The beauty of this that a .NET application consuming a managed .NET component never gets to know that the managed component is internally leveraging unmanaged code implementation from Classic COM components. We'll take a look at some of the ways that a managed .NET component can reuse an existing COM Component:
We call this mixed mode because we have managed classes reusing code and functional logic already available in unmanaged COM components.
Reuse Mechanisms in Classic COM
Classic COM has never subscribed to the idea of implementation inheritance but only played by interface inheritance. The traditional reuse mechanisms in COM have been to use containment and aggregation.
Component reuse through Containment in Classic COM
Just to refresh your memory, Containment allows you to expose an outer component that totally subsumes the inner component within itself. Only the outer component's interface is ever visible to clients. The methods exposed by the outer component's interface usually handle the implementation themselves and/or delegate the work to the inner component when needed. The outer component creates an instance of the inner component and forwards the calls to the inner component when it needs to leverage some of the functionality exposed by the inner component. From the client's perspective, it never knows that there is an inner component shielded by the outer component that does work for the outer component.
Component reuse through Aggregation in Classic COM
In the case of Aggregation, the outer object no longer forwards calls to the inner component. Instead, it allows the client to party directly with the inner component by handing over the inner component's interface pointer to it. The outer component no longer gets to intercept method calls on the inner component's interface because the client directly interacts with the inner component. The inner component uses a default IUnknown (Non-delegating unknown) implementation if it is not being aggregated and uses the Outer Component's IUnknown implementation (Controlling/Delegating unknown) if it's being aggregated. This ensures that the client always gets the IUnknown interface pointer of the outer component and never the non-delegating IUnknown interface pointer of the inner component. Again, from the client's perspective, it does not know that there's an inner component. The client thinks that the inner component's interface is just another interface exposed by the outer component. |
Let's take a look at the various ways in which you could reuse unmanaged code in existing COM components from .NET classes:
In this reuse model, you can have your managed .NET class extend/inherit from an unmanaged COM coclass. In addition to that, the managed class has the option of overriding methods in the COM coclass' interface or accept the base COM coclass' implementation. This is a very powerful model where you get to mix both managed and unmanaged implementations within the same class.
Inheriting unmanaged code from COM Components in managed classes
Let's take a look at the code snippet below to understand this a little better. We'll create a COM Object called Flyer
using ATL that supports an interface called IFlyer
with two methods, TakeOff()
and Fly()
. Here's the IDL declaration for the component:
[
....
]
interface IFlyer : IDispatch
{
[id(1), helpstring("method TakeOff")]
HRESULT TakeOff([out,retval] BSTR* bstrTakeOffStatus);
[id(2), helpstring("method Fly")]
HRESULT Fly([out,retval] BSTR* bstrFlightStatus);
};
[
.....
]
coclass Flyer
{
[default] interface IFlyer;
};
Here's the implementation of the two methods:
STDMETHODIMP CFlyer::TakeOff(BSTR *bstrTakeOffStatus)
{
*bstrTakeOffStatus =
_bstr_t(_T("CFlyer::TakeOff - This is COM taking off")).copy();
return S_OK;
}
STDMETHODIMP CFlyer::Fly(BSTR *bstrFlyStatus)
{
*bstrFlyStatus =
_bstr_t(_T("CFlyer::Fly - This is COM in the skies")).copy();
return S_OK;
}
Before this component can be consumed by managed code, you'll have to generate the metadata proxy for this component from its typelibrary. To do that, you need to issue the following command from the command-line:
tlbimp MyFlyer.tlb /out:MyFlyerMetadata.dll
We'll now create managed classes that inherit from this component using the usual inheritance semantics, so that the component's functionality can be reused. One of the managed classes, Bird
, inherits from the metadata type corresponding to the Flyer
COM object. This means that it would inherit all the methods of the Flyer COM component. The other managed class, Airplane
, overrides the TakeOff
and Fly
methods with its own implementation. One caveat here is that you cannot selectively override only specific methods from the COM coclass in your managed code. If you decide to override a single method in the COM coclass in your managed code, you'd have to override the rest of the methods as well. In other words, you cannot provide an overridden implementation just for the TakeOff
method and implicitly have the managed class use the Fly
implementation from the COM object. You would need to override the Fly method as well in the managed class and provide an implementation for it. If you need to reuse the COM coclass' implementation, you could call base.Fly
from the managed class' Fly
implementation. You might get away with selectively overriding specific methods during compile time. But at runtime, you'd end up running into a System.TypeLoadException
exception with an error message that reads something like: 'Types extending from COM objects should override all methods of an interface implemented by the base COM class'.
using System;
using MyFlyerMetadata;
public class Bird : Flyer
{
}
public class Airplane : Flyer
{
public override String TakeOff() {
return "Airplane::TakeOff - This is .NET taking off";
}
public override String Fly() {
System.Console.WriteLine(base.Fly());
return "Airplane::Fly - This is .NET in the skies";
}
}
public class FlightController
{
public static void Main(String[] args)
{
Bird falcon = new Bird();
System.Console.WriteLine("BIRD: CLEARED TO TAKE OFF");
System.Console.WriteLine(falcon.TakeOff());
System.Console.WriteLine(falcon.Fly());
Airplane skyliner = new Airplane();
System.Console.WriteLine("AIRPLANE: CLEARED TO TAKE OFF");
System.Console.WriteLine(skyliner.TakeOff());
System.Console.WriteLine(skyliner.Fly());
}
}
You can compile the above program using the following command in the DOS command-line:
csc /target:exe /out:FlightClient.exe /r:MyFlyerMetadata.dll FlightClient.cs
Running the program above, gives you the following output:
BIRD: CLEARED TO TAKE OFF
CFlyer::TakeOff - This is COM taking off
CFlyer::Fly - This is COM in the skies
AIRPLANE: CLEARED TO TAKE OFF
Airplane::TakeOff - This is .NET taking off
CFlyer::Fly - This is COM in the skies
Airplane::Fly - This is .NET in the skies
Consumers of the Bird
and Airplane
managed classes are shielded from having to know that these classes are actually reusing existing COM components via inheritance. Whenever necessary, the managed class overrides all the methods in the COM component with its own implementation. This reuse model, where managed code inherits from unmanaged code is called the Mixed-mode inheritance reuse model.
In this model, the managed class uses the same principles of containment in Classic COM. All it does is, stash away an instance of metadata proxy class representing the unmanaged COM component as a member. Whenever it requires the services of the COM component, it forwards a request to the component's methods.
Reusing unmanaged COM code through Containment/Composition
The managed class has the ability to inject its own code before and after the call to the contained COM component. Here's an example:
using System;
using MyFlyerMetadata;
.....
public class HangGlider
{
private Flyer flyer = new Flyer();
public String TakeOff()
{
return flyer.TakeOff();
}
public String Fly()
{
System.Console.WriteLine("In HangGlider::Fly - " +
"Before delegating to flyer.Fly");
return flyer.Fly();
}
}
public class FlightController
{
public static void Main(String[] args)
{
....
HangGlider glider = new HangGlider();
System.Console.WriteLine("HANGGLIDER: CLEARED TO TAKEOFF");
System.Console.WriteLine(glider.TakeOff());
System.Console.WriteLine(glider.Fly());
}
}
Here's the output of the above program:
HANGGLIDER: CLEARED TO TAKEOFF
CFlyer::TakeOff - This is COM taking off
In HangGlider::Fly - Before delegating to flyer.Fly
CFlyer::Fly - This is COM in the skies
In the example above, the HangGlider
class creates an instance of the Flyer
COM component and stores it away as a private member. Whenever a method call arrives that requires the Flyer
component's services, it calls into the component using the private instance that it had stashed away earlier. Also, the HangGlider
class has the liberty to inject code before and after a call is delegated to the Flyer
component's methods. This would not be possible with Mixed-mode inheritance reuse model unless you override all the base class COM methods in your managed class.
Understanding COM Threading models & Apartments from a .NET application's perspective
I remember that when I first started programming with COM, I had not yet ventured 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 COM Threading model waters and 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 its 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 its 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 would 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 and 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. Therefore, it makes sense to set the thread's ApartmentState
as early as possible in your code.
Thread.CurrentThread.ApartmentState = ApartmentSTate.STA;
MySTA objSTA = new MySTA();
objSTA.MyMethod()
As an alternative method, you can tag your managed client's Main entry point method with the STAThreadAttribute
or the MTAThreadAttribute
to start it up with the desired threading affiliation for consuming COM components. For example, take a look at the code snippet below:
public class HelloThreadingModelApp {
.....
[STAThread]
static public void Main(String[] args) {
System.Console.WriteLine("The apartment state is: {0}",
Thread.CurrentThread.ApartmentState.ToString());
}
}
The output that you'd get would be something like:
The apartment state is: STA
If the MTAThread
attribute is set, then the ApartmentState
would be set to MTA. If no thread state attribute is specified in the client's Main entry point or if the ApartmentState
property is not set for the thread from which the COM component is created, then the ApartmentState
would be Unknown, which defaults to MTA behavior.
Part II: Consuming .NET Components from COM aware clients
In this section, we'll see how we can consume managed .NET components from unmanaged COM aware clients. Limiting clients for .NET Components to only managed clients would've been a tough pill to swallow for most developers who have spent a great deal of time over the years developing applications that can't be ported overnight to managed code. The .NET framework allows disparate applications in different platforms to talk to managed applications using wire protocols like SOAP. Unmanaged COM aware clients still get easier ways to talk to managed components. The .NET runtime allows unmanaged COM aware clients to seamlessly access .NET Components through the COM Interop and through the tools provided by the framework. This ensures that COM aware clients can talk to .NET components, as if they were talking to plain-vanilla Classic COM Components.
To begin with, let's put together a simple .NET Component that allows you to look up the temperature at your city. Only classes that are public are added to the typelibrary and exposed to COM aware clients. Also, if the class needs to be creatable from a COM aware client, it needs to have a public default constructor. A public class that does not have a public default constructor still appears in the typelibrary, although it cannot be directly co-creatable from COM. The Temperature Component has two methods DisplayCurrentTemperature
and GetWeatherIndications
. It has a public read-write property called Temperature defined with the corresponding get/set methods.
using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;
public enum WeatherIndications
{
Sunny = 0,
Cloudy,
Rainy,
Snowy
}
[ClassInterface(ClassInterfaceType.AutoDual)]
public class TemperatureComponent
{
private float m_fTemperature = 0;
public TemperatureComponent()
{
m_fTemperature = 30.0f;
}
public float Temperature
{
get { return m_fTemperature; }
set { m_fTemperature = value;}
}
public void DisplayCurrentTemperature()
{
String strTemp = String.Format("The current " +
"temperature at Marlinspike is : " +
"{0:####} degrees fahrenheit",
m_fTemperature);
MessageBox.Show(strTemp,"Today's Temperature");
}
public WeatherIndications GetWeatherIndications()
{
if(m_fTemperature > 70) {
return WeatherIndications.Sunny;
}
else {
return WeatherIndications.Cloudy;
}
}
}
You will also notice that there is an attribute called ClassInterface
that is tagged to the Temperature class with its value set to ClassInterfaceType.AutoDual
. We'll see the significance of applying this attribute in the section, Snooping in on the generated Typelibrary. For now, think of this as a way to tell typelibrary generation tools like REGASM.EXE and TLBEXP.EXE to export the public members of the .NET Component's class into a default Class Interface in the generated typelibrary. Also remember that using a Class Interface to expose the public methods of a .NET class is not generally recommended because it's creedless to COM versioning. We'll take a look at how we can use interfaces explicitly to achieve the same thing. Defining an interface explicitly, deriving your .NET Component class from this interface and then implementing the interface's methods in your .NET Component is the recommended way of doing things if you are going to expose your .NET Component to COM aware clients. We will compare and constrast these two approaches in detail and also see why the former approach is not recommended, in the section, Snooping in on the generated Typelibrary.
If you are using Visual Studio.NET, you can create a Visual C# project and use the Class Library template to code the above component. If you are a command-line jockey, then here's the command to build the component. This creates an assembly called Temperature.dll.
csc /target:library /r:System.Windows.Forms.dll
/out:Temperature.dll TemperatureComponent.cs
What you just generated now is a .NET assembly that a COM aware client like Visual Basic 6.0 is clueless about. You need to get some COM friendly type information from it so that our VB client will be happy to party around with it. Earlier, you used a tool called TLBIMP (Type Library Importer) to create a .NET metadata proxy from a COM typelibrary. You need to do the reverse of that here. You need to take in a .NET assembly and generate a typelibrary out of it so that it's usable from a COM aware client. The .NET framework provides a couple of tools for this. You can use the Type Library Exporter utility (TLBEXP.exe) or the Assembly Registration Utility (Regasm.exe), both of which you'll find in the Bin directory of your .NET SDK installation. REGASM is a superset of the TLBEXP utility in that it also does much more than generating a typelibrary. It's also used to register the assembly, so that the appropriate registry entries are made to facilitate the COM runtime and the .NET runtime to hook up the COM aware client to the .NET component. We'll use REGASM.EXE here because we can get both the assembly registration and the typelibrary generation done in one go. But you could use TLBEXP as well to generate the typelibrary, and then use REGASM to register the assembly.
regasm Temperature.dll /tlb:Temperature.tlb
The above call to REGASM.EXE makes the appropriate registry entries and also generates a typelibrary (Temperature.tlb) from the .NET assembly so that the typelibrary can be referenced from our VB 6 client application.
Let's quickly put together a VB Form based application that creates and invokes the .NET Component whose assembly we registered and generated a typelibrary from. Creation of the component is the same as how you would create a COM object. You could either reference the typelibrary and early-bind to the Component or perform a CreateObject
call using the component's ProgID to late-bind to the component. Usually, the ProgID generated is the fully qualified name of the class. In our case, the ProgID generated would be TemperatureComponent
. But you could use the ProgIDAttribute to specify an user-defined ProgID to override the default ProgID that is generated.
Private Sub MyButton_Click()
On Error GoTo ErrHandler
Dim objTemperature As New TemperatureComponent
objTemperature.DisplayCurrentTemperature
objTemperature.Temperature = 52.7
objTemperature.DisplayCurrentTemperature
If (objTemperature.GetWeatherIndications() = _
WeatherIndications_Sunny) Then
MsgBox "Off to the beach"
Else
MsgBox "Stay at home and watch Godzilla on TV"
End If
Exit Sub
ErrHandler:
MsgBox "Error Message : " & Err.Description, _
vbOKOnly, "Error Code " & CStr(Err.Number)
End Sub
To enable the .NET Assembly Resolver to find the assembly housing your component, you will either need to place the component in the same directory as the application that's consuming it, or deploy the assembly as a Shared Assembly in the Global Assembly Cache (GAC). For now, just copy the Temperature.dll to the same directory as your VB Client Application executable. If VB can use the usual Classic COM based invocation mechanism and still get away with invoking and consuming the .NET component, there's got to be a good Samaritan sitting in between the VB6 Client and the .NET component and wiring the COM invocation requests to the actual .NET component. We'll soon see, what happens under the covers.
Let's take a peek at the registry entries that Regasm.exe made when we registered our assembly.
Registry Entries made during Assembly registration by REGASM
You can check your Component's CLSID in OLEVIEW.EXE by opening the typelibrary generated by REGASM. Check for the uuid attribute under the coclass section. If you navigate to your HKCR\CLSID\{...Component's CLSID...} key in the registry, you can see REGASM has made the relevant registry entries required by COM to activate an object hosted by an Inproc server. In addition, it has created a few other registry entries such as Class, Assembly, and RuntimeVersion that are used by the .NET runtime. The Inproc Server handler (indicated by the InProcServer32 key's default value) is set to mscoree.dll, which is the core CLR runtime execution engine. The COM runtime calls the DllGetClassObject entry point in MSCOREE.dll (The CLR runtime). The runtime then uses the Class ID (CLSID) passed to DllGetClassObject
to look up the Assembly and Class keys under the InProcServer32 key to load and resolve the .NET assembly that will service this request. The runtime also dynamically creates a COM Callable Wrapper (CCW) proxy (a mirror image of the RCW) to handle the interaction between unmanaged code and the managed components. This makes COM aware clients think that they are interacting with Classic COM components and makes .NET Components think that they are receiving requests from a managed application. There is one CCW created per .NET component instance.
Under the hood: Accessing .NET Components from COM aware clients
As the saying goes, 'A picture is worth a thousand words', so let the illustration here, do most of the talking as to what is happening under the covers when a COM aware client interacts with a .NET component. The primary players here are the CLR runtime and the COM Callable Wrapper (CCW) that gets fabricated by the .NET runtime. From then on, the CCW takes over and handles most of the spadework to get the two to work together. The CCW handles the lifetime management issues here. COM clients in the unmanaged realm maintain reference counts on the CCW proxy rather than on the actual .NET component. The CCW only holds a reference to the .NET component. The .NET Component lives by the rules of the CLR garbage collector as any other managed type would. The CCW lives in the unmanaged heap and is torn down when the COM aware clients no longer have any outstanding references to the objects. Just like the RCW, the CCW is also responsible for marshaling the method call parameters that move back and forth between the unmanaged client and managed .NET components. It's also responsible for synthesizing v-tables on demand. V-tables for specific interfaces are generated dynamically. They are built lazily only when the COM aware client actually requests a specific interface via a QueryInterface
call. Calls on the CCW proxy are eventually routed away to a stub that actually makes the call into the managed object.
Let's take a quick look at the kind of information that was put into the typelibrary generated by the Assembly Registration utility (REGASM). Open the typelibrary through OLEVIEW's Type Library Viewer so that you can take a look at the IDL file that was reverse engineered from the typelibrary.
[
uuid(A9F20157-FDFE-36D6-90C3-BFCD3C8C8442),
version(1.0)
]
library Temperature
{
importlib("mscorlib.tlb");
importlib("STDOLE2.TLB");
interface _TemperatureComponent;
typedef [uuid(0820402E-B8B6-330F-8D56-FF079E5B4659), version(1.0),
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "WeatherIndications")]
enum {
WeatherIndications_Sunny = 0,
WeatherIndications_Cloudy = 1,
WeatherIndications_Rainy = 2,
WeatherIndications_Snowy = 3
} WeatherIndications;
[
uuid(01FAD74C-3DC4-3DE0-86A9-8490FAEE8964),
version(1.0),
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},
"TemperatureComponent")
]
coclass TemperatureComponent {
[default] interface _TemperatureComponent;
interface _Object;
};
[
odl,
uuid(C51D54FA-7C81-35A5-9998-3963EAB4AA12),
hidden,
dual,
nonextensible,
oleautomation,
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},
"TemperatureComponent")
]
interface _TemperatureComponent : IDispatch {
[id(00000000), propget]
HRESULT ToString([out, retval] BSTR* pRetVal);
[id(0x60020001)]
HRESULT Equals([in] VARIANT obj,
[out, retval] VARIANT_BOOL* pRetVal);
[id(0x60020002)]
HRESULT GetHashCode([out, retval] long* pRetVal);
[id(0x60020003)]
HRESULT GetType([out, retval] _Type** pRetVal);
[id(0x60020004), propget]
HRESULT Temperature([out, retval] single* pRetVal);
[id(0x60020004), propput]
HRESULT Temperature([in] single pRetVal);
[id(0x60020006)]
HRESULT DisplayCurrentTemperature();
[id(0x60020007)]
HRESULT GetWeatherIndications([out, retval]
WeatherIndications* pRetVal);
};
};
If you take a look at the coclass
section, it specifies the default interface as the Class Name prefixed by an _ (underscore) character. This interface is called the Class Interface and its methods comprise all the non-static public methods, fields, and properties of the class. The Class Interface gets generated because you tagged your .NET class with the ClassInterface
attribute. This attribute tells typelibrary generation tools such as RegAsm.exe and TlbExp.exe to generate a default interface known as the Class Interface and add all the public methods, fields, and properties of the class into it, so that it could be exposed to COM aware clients.
- If you do not tag the
ClassInterface attribute to a .NET Component's class, a default Class Interface is still generated. But in this case, it's an IDispatch based Class Interface that does not include any type information for the methods exposed nor their DISPIDs. This type of Class Interface is only available to late binding clients. This effect is the same as that of applying the ClassInterfaceType.AutoDispatch value to the ClassInterface attribute. The advantage of the AutoDispatch option is that, since the DISPIDs are not cached and not available as a part of the type information, they don't break existing clients when a new version of the component is released since the DISPIDs are obtained at runtime by clients using something like IDispatch::GetIDsOfNames . |
Only public methods are visible in the typelibrary and can be used by COM clients. The private members don't make it into the typelibrary and are hidden from COM Clients. The public properties and fields of the class are transformed into IDL propget
/propput
types. The Temperature
property in our example has both set and get accessor defined and so, both the propset
and propget
are emitted for this property. There is also another interface called _Object
that gets added to the coclass
. The Class interface also contains 4 other methods. They are:
ToString
Equals
GetHashCode
GetType
These methods are added to the default Class Interface because it implicitly inherits from the System.Object
class. Each one of the methods and properties that's added to the interface gets a DISPID that is generated automatically. You can override this DISPID with a user defined one by using the DispId attribute. You'll notice that the ToString
method has been assigned a DISPID of 0 to indicate that it is the default method in the class Interface. This means that if you leave out the method name, the ToString
method will be invoked.
Dim objTemperature As New TemperatureComponent
MsgBox objTemperature
Let's examine the various ways in which we can facilitate the generation of the implicit Class Interface. We'll start by taking a look at the effect of applying the ClassInterfaceType.AutoDual
value to the ClassInterface
attribute.
[ClassInterface(ClassInterfaceType.AutoDual)]
public class TemperatureComponent
{
....
}
Notice that the value (positional parameter value) assigned to the ClassInterface
attribute is ClassInterfaceType.AutoDual.
This option tells typelibrary generation tools (like RegAsm.exe) to generate the Class Interface as a dual interface and export all the type information (for the methods, properties etc. and their corresponding Dispatch IDs) into the typelibrary. Imagine what would happen if you decided that you want to add another public method to the class. This mutates the Class Interface that gets generated and breaks the fundamental interface immutabilty law in COM because the v-table's structure changes now. Late Bound clients also have their share of woes when they try to consume the component. The Dispatch IDs (DISPIDs) get regenerated because of the addition of the new method and this breaks late bound clients too. As a general rule, using ClassInterfaceType.AutoDual
is evil since it is totally agnostic about COM Versioning. Let's take a look at the next possible value that you can set for your ClassInterface
attribute. Tagging your ClassInterface
attribute with a value of ClassInterfaceType.AutoDispatch
forces typelibrary generation tools to avoid generating type information in the typelibrary. So, if you had your TemperatureComponent
class tagged with the ClassInterface
attribute as shown below:
[ClassInterface(ClassInterfaceType.AutoDispatch)]
public class TemperatureComponent
{
.....
}
then, the corresponding typelibrary generated by RegAsm.exe would have an IDL structure such as this:
[
uuid(A9F20157-FDFE-36D6-90C3-BFCD3C8C8442),
version(1.0)
]
library Temperature
{
......
[
uuid(01FAD74C-3DC4-3DE0-86A9-8490FAEE8964),
version(1.0),
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},
"TemperatureComponent")
]
coclass TemperatureComponent {
[default] interface IDispatch;
interface _Object;
};
};
You will notice that the default interface is an IDispatch
interface and neither the DISPIDs nor the method type information is present in the Typelibrary. This leaves the COM aware client to consume .NET Components using only late-binding. Also, since the DISPID details are not stored as a part of the type information in the typelibrary, the clients obtain these DISPIDs on demand using something like IDispatch::GetIDsOfNames
. This allows clients to use newer versions of the components without breaking existing code. Using ClassInterfaceType.AutoDispatch
is much safer than using ClassInterfaceType.AutoDual
because it does not break existing client code when newer versions of the component are released, albeit the former allows only late binding. The recommended way of modeling your .NET component to be exposed to COM aware clients is to do away with the Class Interface itself and instead, explicitly factor out the methods you are exposing into a separate interface, and have the .NET component implement that interface. Using a Class Interface is a quick and easy way to get your .NET component exposed to COM aware clients. But it's not the recommended way. Let's try rewriting our TemperatureComponent
by factoring out the methods explicitly into an interface and see how the typelibrary generation differs:
public interface ITemperature {
float Temperature { get; set; }
void DisplayCurrentTemperature();
WeatherIndications GetWeatherIndications();
}
[ClassInterface(ClassInterfaceType.None)]
public class TemperatureComponent : ITemperature
{
......
public float Temperature
{
get { return m_fTemperature; }
set { m_fTemperature = value;}
}
public void DisplayCurrentTemperature() {
.....
}
public WeatherIndications GetWeatherIndications() {
....
}
}
Here's how the corresponding IDL file looks like for the generated typelibrary. Notice that the TemperatureComponent
class' default interface is now the ITemperature
interface that was implemented by the class.
[
uuid(A9F20157-FDFE-36D6-90C3-BFCD3C8C8442),
version(1.0)
]
library Temperature
{
......
[
odl,
uuid(72AA177B-C6B2-3694-B083-4FF535B40AD2),
version(1.0),
dual,
oleautomation,
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "ITemperature")
]
interface ITemperature : IDispatch {
[id(0x60020000), propget]
HRESULT Temperature([out, retval] single* pRetVal);
[id(0x60020000), propput]
HRESULT Temperature([in] single pRetVal);
[id(0x60020002)]
HRESULT DisplayCurrentTemperature();
[id(0x60020003)]
HRESULT GetWeatherIndications([out, retval]
WeatherIndications* pRetVal);
};
[
uuid(01FAD74C-3DC4-3DE0-86A9-8490FAEE8964),
version(1.0),
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},
"TemperatureComponent")
]
coclass TemperatureComponent {
interface _Object;
[default] interface ITemperature;
};
};
This approach of factoring out the methods of the .NET class explicitly into an interface and having the class derive from the interface and implement it, is the recommended way for exposing your .NET Components to COM aware clients. The ClassInterfaceType.None
option tells the typelibrary generation tools that you do not require a Class Interface. This ensures that the ITemperature interface is the default interface. Were you to not specify the ClassInterfaceType.None
value for the Class Interface attribute, then the Class Interface would have been made the default interface. Here's the gist of what we learnt in this section:
- Class Interfaces with
ClassInterfaceType.AutoDual are COM version agnostic. Try to avoid using them.
- Class Interfaces with
ClassInterfaceType.AutoDispatch do not export type information and DISPIDs to the typelibrary. They are COM versioning friendly. They can be accessed from COM aware clients only through late-binding.
- Class Interfaces are a hack. Try to avoid them if possible. Use explicit interfaces instead.
- Use
ClassInterfaceType.None to make your explicit interface the default interface. |
Another interesting observation is the way REGASM and TLBEXP generate a mangled method name in the IDL by appending a '_' followed by a sequence number when you have overloaded methods in your .NET class or interface that you are exporting to a typelibrary. For example, if you had exposed the following interface in your .NET Component:
public interface MyInterface
{
String HelloWorld();
String HelloWorld(int nInput);
}
Then, the IDL corresponding to the typelibrary that REGASM/TLBEXP generated would look like this.
[
.....
]
interface MyInterface : IDispatch {
[id(0x60020000)]
HRESULT HelloWorld([out, retval] BSTR* pRetVal);
[id(0x60020001)]
HRESULT HelloWorld_2([in] long nInput,
[out,retval] BSTR* pRetVal);
};
Notice the '_2' appended to the second HelloWorld
method to distinguish it from the first one in the IDL file.
Knowing the rules that RegAsm.exe or TlbExp.exe utilities use, to generate the IDL and subsequently, the typelibrary by introspecting a .NET assembly allows you to mould the typelibrary generation to your requirements. You could inject .NET attributes into your assembly that give these utilities the hints they need to alter metadata in the IDL to affect the typelibrary that gets created. For example, you could change the interface type from dual to an IUnknown
only based custom interface or a pure dispinterface using the InterfaceTypeAttribute
. Here are some of the ways you can inject attributes to qualify the types in your .NET component and modify the generated typelibrary to suit your requirements.
By default, the interfaces used by a .NET Class are transformed to dual interfaces in the IDL. This allows the client to get the best of both early binding and late binding. However, there may be occasions when you want the interface to be a pure-dispinterface or a custom IUnknown
only based interface. You can override the default type of the interface using the InterfaceTypeAttribute
. Take a look at the example below.
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface ISnowStorm
{
....
}
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IHurricane
{
....
}
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ITyphoon
{
....
}
public interface IWeatherStatistics
{
.....
}
As seen in the above example, the InterfaceType
attribute is used to emit metadata information into the interface so that utilities like RegAsm.exe & TlbExp.exe can use this information to generate the appropriate type of interface in the typelibrary. Here's how the resulting IDL would look like:
[
odl,
uuid(1423FBFA-BE13-3766-9729-9C1AAF5DB08A),
version(1.0),
dual,
oleautomation,
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "ISnowStorm")
]
interface ISnowStorm : IDispatch {
....
};
[
odl,
uuid(D95E54B8-FABC-3BDA-AA45-AC4EFF49AF92),
version(1.0),
oleautomation,
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "IHurricane")
]
interface IHurricane : IUnknown {
....
};
[
uuid(676B3B85-7DB8-306D-A1E9-B6AA1008EDF2),
version(1.0),
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "ITyphoon")
]
dispinterface ITyphoon {
properties:
methods:
};
[
odl,
uuid(A1A37136-341A-3631-9275-FC7B0F0DB695),
version(1.0),
dual,
oleautomation,
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},
"IWeatherStatistics")
]
interface IWeatherStatistics : IDispatch {
.....
}
As seen above, setting the InterfaceType
attribute to ComInterfaceType.InterfaceIsIUnknown
results in the generation of an IUnknown
only based custom interface. Setting the InterfaceType
attribute to ComInterfaceType.InterfaceIsIDispatch
results in the generation of a pure dispatch only dispinterface. Setting the InterfaceType
attribute to ComInterfaceType.InterfaceIsDual
or ignoring the InterfaceType
attribute altogether (such as the IWeatherStatistics
interface in the example) results in the emission of a dual interface.
GUIDs for types such as classes and interfaces are generated automatically by the REGASM/TLBEXP utilities, when these types are exported into a typelibrary. But, you still have your final say if you need to assign a specific GUID to the interface or class. You can use the Guid Attribute to specify an user defined GUID. You can also affect the way that a ProgID gets generated for a specific class. By default, the RegAsm.exe utility assigns the fully qualified type name of the class for the Progid
. You can use the ProgId
attribute to assign a user specified ProgID for your coclass.
[
GuidAttribute("AD4760A9-6F5C-4435-8844-D0BA7C66AC50"),
ProgId("WeatherStation.TornadoTracker")
]
public class TornadoTracker {
.....
}
Here's how the IDL file looks like after the Guid
attribute has been used on the class:
[
uuid(AD4760A9-6F5C-4435-8844-D0BA7C66AC50),
version(1.0),
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "TornadoTracker")
]
coclass TornadoTracker {
....
};
Notice that the uuid
attribute (representing the CLSID of the COM class) in the coclass
section contains the value of the GUID that we specified. If you check the HCKR\CLSID\{AD4760A9-6F5C-4435-8844-D0BA7C66AC50}\ProgID Registry Key, you should see the value WeatherStation.TornadoTracker
representing the class' ProgID.
You saw earlier, how all the public classes and interfaces of the class were automatically added to the typelibrary so that they can be referenced and used by COM aware clients. But there may be circumstances when you need to prevent certain public interfaces and public classes from being available to COM. You can do this using the ComVisible
attribute. Setting this attribute with a value of false prevents the type to which it is applied from appearing in the typelibrary.
[ComVisible(false)]
public interface IWeatherStatistics
{
float GetLowestTemperatureThisMonth();
float GetHighestTemperatureThisMonth();
}
In the above example, the IWeatherStatistics
interface is not exported as type-information into the typelibrary, when the assembly is exported to the typelibrary. If the IWeatherStatistics
type is used by any other type that's exposed to COM, then it's replaced by an IUnknown
interface in the typelibrary. For example:
HRESULT SetWeatherStatistics([in] IWeatherStatistics* pWeatherIndications);
becomes:
HRESULT SetWeatherStatistics([in] IUnknown* pUnkWeatherIndications);
There's a caveat here, though. It does not make sense to apply a [ComVisible(true)]
to turn on the visibility of private or protected members in the class because they can never be exposed to COM.
The ComVisible
attribute can be applied not only to classes and interfaces, but to a whole slew of other types such as assemblies, methods, fields, properties, delegates, structs etc. too. Take a look at this example below, where we selectively hide specific methods in an interface from being exposed to COM.
public interface IHurricaneWatch {
void AlertWeatherStations(String strDetails);
[ComVisible(false)]
void PlotCoordinates();
void IssueEvacuationOrders();
}
The above snippet of code indicates to the typelibrary generator (RegAsm.exe/TlbExp.exe) that we intend to hide the PlotCoordinates
method of the IHurricaneWatch
interface from appearing in the generated type library. Here's how the IDL looks like, for the typelibrary that gets generated:
[
....
]
interface IHurricaneWatch : IDispatch {
[id(0x60020000)] HRESULT AlertWeatherStations([in] BSTR strDetails);
[id(0x60020002)] HRESULT IssueEvacuationOrders();
};
When the runtime marshals managed types into the unmanaged world, it follows certain data type conversion rules. For example, a managed type such as a String
is always converted to a BSTR
. Types such as String
s could have more than one possible representation in the unmanaged world such as array of ANSI characters (LPSTR
), an array of UNICODE characters (LPWSTR
), or a BSTR
. When the target data type in the unmanaged realm presents more than one possible representation of the underlying managed type, then we call such types, Non-isomorphic types. The COM Interop converts non-isomorphic types to a specific default target type (such as a String
always converted to a BSTR
). Consider the following piece of code in a C# class:
public void SetProductName(String strProductName) { ... }
When the above code is run through the Typelibrary exporter such as TLBEXP or REGASM, what we get back in the IDL corresponding to the generated typelibrary looks like this:
HRESULT SetProductName([in] BSTR strProductName);
However, there may be occasions when you need to provide alternate representations for the types. For example, you might want to convert a managed type such as a String
to a null-terminated array of ANSI characters (LPSTR
), instead of a BSTR
. This is where the MarshalAs
attribute helps. You could use the MarshalAs
attribute to control how the conversion happens between managed and unmanaged types. So if you'd like to have the managed String
converted to a null-terminated ANSI character array as opposed to a BSTR
, you would need to apply the MarshalAs
attribute to the method parameter as shown below:
public void SetProductName( [MarshalAs(UnmanagedType.LPStr)]
String strProductName) { .... }
The resulting conversion would look something like this in the IDL representation:
HRESULT SetProductName([in] LPSTR strProductName);
Thus, the MarshalAs
attribute can be very useful when you need to tweak data representation mappings between types in the managed and unmanaged worlds.
Let's take a look at how the exceptions raised by .NET components get mapped to COM based HRESULT
s. You'll notice that the return types from .NET component methods are converted into an [out,retval]
IDL type when run through a typelibrary exporter. The actual return type for the method in the IDL is a HRESULT
that indicates a success or failure of a method call. A failed HRESULT
is usually the result of a system exception or a user-defined exception raised because of failed business logic. If the method call goes through fine with no exception thrown from the .NET component, then the CCW fills in the returned HRESULT
with 0 (S_OK
). If the method call fails or business logic validation fails, the .NET component is expected to raise an exception. This exception usually has a failure HRESULT
assigned to it and an Error description associated with it. The CCW gleans out details such as the Error Code, Error message etc. from the .NET Exception and provides these details in a form that can be consumed by the COM client. It does this by implementing the ISupportErrorInfo
interface (to indicate that it supports rich error information) and the IErrorInfo
interface (through which it provides all the error information details) or through the EXCEPINFO
structure that is passed through an IDispatch::Invoke
call (in case the COM aware client was late binding through a dispinterface). The error propagation happens seamlessly such that .NET exceptions are mapped to their equivalent COM counterparts and delivered to the COM Client by the CCW. Let's modify the Temperature
property's set
method to raise a user defined error if the temperature specified is not within acceptable limits. We'll soon see how the VB Client traps this error using the usual COM error handling mechanisms.
public class TemperatureComponent
{
private float m_fTemperature = 0;
public float Temperature
{
get
{
return m_fTemperature;
}
set
{
if((value < -30) || (value > 150))
{
TemperatureException excep = new TemperatureException(
"Marlinspike has never experienced" +
" such extreme temperatures. " +
"Please recalibrate your thermometer");
throw excep;
}
m_fTemperature = value;
}
}
}
class TemperatureException : ApplicationException
{
public TemperatureException(String message) : base(message)
{
}
}
Here's a VB client that tries to set the temperature in Marlinspike to a value that's out-of-bounds of the temperature readings accepted by the method and this triggers an exception from the .NET Component.
Private Sub MyButton_Click()
On Error GoTo ErrHandler
Dim objTemperature As New TemperatureComponent
objTemperature.Temperature = 212
Exit Sub
ErrHandler:
MsgBox "Error Message : " & Err.Description, _
vbOKOnly, "Error Code: " & CStr(Err.Number)
End Sub
In the example above, the VB6 client's ErrorHandler block set through the On Error Goto
statement uses VB's global intrinsic Err
object to get all the error details that the CCW has mapped from the .NET Exception into a COM specific error object implementing the IErrorInfo
interface.
The .NET Component here raises a TemperatureException
which extends the ApplicationException
class. An ApplicationException
is usually thrown to indicate errors that relate to usual run-of-the-mill failure in application business logic. The TemperatureException
calls the base class to initialize the error message. Since no explicit HRESULT
was specified, a failure HRESULT
is generated by the ApplicationException
class and is returned back. If you want to take control of the value of the HRESULT
s instead of accepting the auto generated HRESULT
values by the base class, then you can use the HResult
protected member in the ApplicationException
class (that is accessible from the TemperatureException
class) to specify a specific HRESULT
value to be returned. Another way to throw exceptions from your .NET component is to use the ThrowExceptionForHR
method of the System.Runtime.InteropServices.Marshal
class. This method takes an integer that represents a standard HRESULT
parameter. Most of these standard HRESULT
s map to .NET exception types and the corresponding .NET exception is thrown. For example, if you were to use:
System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(COR_E_OUTOFMEMORY);
then, an OutOfMemoryException
would be thrown.
We saw earlier how COM objects in the unmanaged world raise events asynchronously using Connection Points and how these events could be consumed by .NET applications. We'll now take a look at the other way round. We'll get a .NET Component to raise events and then have an unmanaged sink consume these events. A .NET Component should declare events representing delegate instances for each of the methods in its outgoing event interface. When an event is raised, all the delegates in the event's list will be invoked. These delegates reference the notification target's handler and can therefore call into the correct handler function provided by the subscriber. The unmanaged sink goes about subscribing to events as if it were interacting with a COM object that supports outgoing interfaces using connection points. The CCW takes care of mapping both these event handling models so that a COM Client's unmanaged handler could still receive notifications when a managed .NET event occurs.
Let's create a .NET Component that notifies subscribers on inclement weather conditions. The component allows the weather station master to set wind speeds that have been recorded. If the wind speeds exceed a certain limit (300 mph), it senses an impending Tornado and notifies subscribers by firing off the OnTornadoWarning
event.
using System;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using System.Reflection;
using System.Diagnostics;
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ITornadoWatchEvents
{
void OnTornadoWarning(int nWindSpeed);
}
public interface IWeatherNotify {
int WindSpeed { get; set; }
}
public delegate void TornadoWarningDelegate(int nWindSpeed);
[
ComSourceInterfaces("ITornadoWatchEvents"),
ClassInterface(ClassInterfaceType.None)
]
public class WeatherNotify : IWeatherNotify
{
private int m_nWindSpeed = 20;
public event TornadoWarningDelegate OnTornadoWarning;
public WeatherNotify() {
}
public int WindSpeed
{
get {
return m_nWindSpeed;
}
set {
m_nWindSpeed = value;
if(value >= 300) {
try {
if(null != OnTornadoWarning) {
OnTornadoWarning(m_nWindSpeed);
}
}
catch(Exception ex) {
Trace.WriteLine(ex.Message);
}
}
}
}
}
Let's dissect the code a bit and try to understand what's going on. The ITornadoWatchEvents
interface is the outgoing interface that unmanaged sinks will need to implement in order to receive event notifications. This interface consists of a single method, OnTornadoWarning
that notifies clients of the possibility of an approaching Tornado along with the current wind speed. You'll notice that the outgoing interface ITornadoWatchEvents
is tagged with an InterfaceType attribute with the positional parameter ComInterfaceType.InterfaceIsIDispatch
. This is because by default, the interface would be exported into the IDL and subsequently into the typelibrary as a dual interface. Scripting clients generally crack up when they try to sink in a dual interface and they're only pure dispinterface friendly. So we inject an InterfaceType
attribute with a ComInterfaceType.InterfaceIsIDispatch
value to force typelibrary generation tools to generate a pure dispinterface for the outgoing interface. You would need to define a delegate (TornadoWarningDelegate
) that matches the exact signature of the OnTornadoWarning
method in the outgoing interface. If you had more than one method in the outgoing interface, you need to define matching delegates for each method. We are now done with the definition of the outgoing interface and matching delegates for each of the methods in the outgoing interface. Now comes the most important part. You would need to define events representing each of the delegates that you defined for the methods in the outgoing interface.
public event TornadoWarningDelegate OnTornadoWarning;
The delegate instance that the event represents needs to have the same name as the corresponding method in the outgoing interface. This means that the event representing the TornadoWarningDelegate
delegate instance will need to be named OnTornadoWarning
. That's all there is to it. You are now ready to send out event notifications. In our example, an event notification is sent out when the WindSpeed
is set to a value greater than or equal to 300. You first need to check if the delegate instance for the event exists and then fire-off the OnTornadoWarning
event. Since the delegate represented by the event is a multicast delegate, all the COM sinks and subscribers in the delegates invocation list will be notified that there's a twister looming on the horizon. You can build the above .NET component using the following command:
csc /target:library /r:System.dll /out:WeatherNotify.dll WeatherNotify.cs
You can now run the assembly through RegAsm.exe to register it and generate a typelibrary out of it. This typelibrary can then be referenced in your VB6 Client that will sink the events.
regasm WeatherNotify.dll /tlb:WeatherNotify.tlb
Here's a VB 6.0 Client that subscribes to OnTornadoWarning
event notifications from the WeatherNotify
Component. It's a simple Form
based application that subscribes to event notifications using the WithEvents
keyword. The objWeatherNotify_OnTornadoWarning
subroutine receives event notifications from the .NET component when the WindSpeed
is set to a value greater than or equal to 300.
Dim WithEvents objWeatherNotify As WeatherNotify.WeatherNotify
Private Sub Form_Load()
Set objWeatherNotify = New WeatherNotify.WeatherNotify
End Sub
Private Sub SetWindSpeedButton_Click()
Me.LabelWarning = ""
objWeatherNotify.WindSpeed = Me.WindSpeed
End Sub
Private Sub objWeatherNotify_OnTornadoWarning(ByVal nWindSpeed As Long)
Me.LabelWarning = "Tornado Warning: Current Wind Speeds : " & _
nWindSpeed & " mph"
End Sub
Earlier, you saw how the RCW and the metadata helper classes translated the Connection point event handling to Delegate based event handling semantics so that a .NET application could sink events from COM Components. On a similar relation, the CCW here does most of the plumbing to allow unmanaged code to subscribe to event notifications from .NET Components and to deliver these events to the respective handlers in the unmanaged COM land.
In the examples that you saw earlier, you placed the .NET Components and the COM Metadata Proxy assemblies in the same directory as the application that was consuming it, so that the runtime's Assembly resolver would locate it when it probes for the assembly. Assemblies deployed this way in the same directory of the application referencing it are called Private Assemblies. Yet another way of locating referenced assemblies is by using configuration files to tell the Assembly resolver where to look for the referenced assembly. When the .NET runtime resolves and binds to such private assemblies, it has no concerns for versioning. If your assembly is often used by a large number of applications, then it makes sense to deploy your assembly in a global repository so that they can be shared and used by other applications. This shared assembly repository is called the Global assembly cache (GAC). Assemblies deployed in the GAC are called Shared Name Assemblies or Strong Name Assemblies. This is because these assemblies have a unique "Shared Name" or "Strong Name" associated with them and their identity is uniquely qualified with a textual name, version number, public key token, culture information, and a digital signature. So how do we go about generating a Strong name for an assembly? The first thing you need to do is to generate a Public-Private Key Pair using the Strong Name utility (SN.exe). You can run SN.exe from the command-line to create a new random key pair.
sn -k MyKeyPair.snk
If everything went off well, you should see the following message:
Key pair written to MyKeyPair.snk
Once the key pair is generated, you need to associate this key pair with the Assembly for which you need to generate a shared name. You can do this using the System.Reflection.AssemblyKeyFileAttribute
. The AssemblyKeyFile
attribute associates the key pair file that was generated from SN.exe with the assembly so that the public key and the digital signature (generated using the private key and the information in the assembly's manifest) can be used to generate the assembly's shared name. The key pair file is generally required during compile time to generate the fully qualified shared-name signature. But for security reasons, most organizations are a little apprehensive about passing their private key over to the developer of the assembly. For this reason, there is a Delay signing option available. The System.Reflection.AssemblyDelaySignAttribute
allows you to sign the assembly with the private key at a later time.
Let's make the Temperature.dll .NET assembly that we used earlier as a Strong named assembly. To do that, you need to add the AssemblyKeyFile
attribute to your component. If you are using Visual Studio.NET, there is a placeholder for this attribute and other similar global attributes in AssemblyInfo.cs file in your Project Solution. You can edit the file and specify MyKeyPair.snk as the file containing your key pair.
[assembly:AssemblyKeyFile("MyKeyPair.snk")]
You can specify the versioning information for the assembly using the System.Reflection.AssemblyVersionAttribute
. This attribute allows you to specify the version of the assembly. The version usually follows the pattern major.minor.build.revision. There is usually a placeholder for this attribute too in AssemblyInfo.cs in your C# Class Library solution in Visual Studio.NET. You can go in there and edit this attribute.
[assembly: AssemblyVersion("1.0.0.0")]
Rebuild the Temparature Component after editing the two attributes specified above in the AssemblyInfo.cs file. Your Assembly now has a strong name associated with it. To verify this, issue the following command using the Strong Name utility to list the assembly's public Key and token.
sn -Tp Temperature.dll
An output similar to this is returned back:
Public key is
0024000004800000940000000602000000240000525341310004000001000100ed87f0432cbf37
fc70eec5d0e59d7e47327729cd99e257a2790c690957691f20c01b47d46a72b20b4f37a829f6ad
82e6594221bbd0193b5499ca0a83db7fc9b78bcb07177f02ef9c827688246f6073f34405e9a441
37017cf6ed52c5001272b0b820926f078bbe8705fa9d411a18d692c94be9541bb3fde38b1b1f79
5a06dde8
Public key token is f80b1601a4d8a9dd
If you use the same command on a private assembly that does not have a strong name associated with it, this is what you get:
sn -Tp WeatherNotify.dll
WeatherNotify.dll does not represent a strongly named assembly
You are now ready to deploy the Temperature.dll in the Global Assembly Cache (GAC), so that the assembly resolver can locate this assembly in the GAC when applications attempt to load or use this assembly. You can list out all the assemblies in the GAC using the following command:
gacutil -l
Alternatively, you can use a Windows shell extension in shfusion.dll that allows you to view, add and remove assemblies in the GAC. If you navigate to the Assembly folder under your Windows Directory, this extension will provide you with a view of the assemblies deployed in the GAC along with properties such as Name, Type, Version, Culture and Public key token. To deploy the Temperature.dll assembly, just type in the following command in the command-line and it should be deployed in the GAC.
gacutil -i Temperature.dll
If everything went off well, you would get a message such as:
Assembly successfully added to the cache
You could also drag and drop your strong name assemblies into the view provided by the shell extension and it should install it for you. Once your assemblies are deployed in the GAC, you are relieved from the rigmarole of having to place your assembly in the application's folder or mess with the configuration files to tell the resolver where to find the assembly. The assembly resolver will now locate the assembly in the GAC.
Deploying the assembly in the GAC has advantages like side-by-side execution of assemblies with different version numbers, single-instancing of assemblies (also called Code Page Sharing) that allows the runtime to load fewer copies of the assembly when used by multiple applications, decreased load times, and facilitates Quick fix Engineering (QFE) based hot-deployment of assemblies.
Thread Affinity in .NET Components
In the Understanding COM Threading Models and Apartments from a .NET Application's perspective section, you saw how .NET applications could declare the calling thread's apartment affiliation before creating a classic COM Component. Now, let's take a look at the other side of the equation. Particularly, the kind of threading behavior that .NET Components exhibit when they are created from unmanaged COM aware applications. The thread affinity of a .NET Component is defined by the context that the object lives in. A Context is essentially an environment hosted by the AppDomain (a light weight process) in which the object gets created. Each context in turn hosts objects that share common usage requirements such as Thread Affinity, Object pooling, Transaction, JIT Activation, Synchronization etc. These contexts are created as and when required by the runtime depending on the attributes, and the interception services required by the object. If there is an existing context that matches the usage rules that govern the object, then the runtime offers it accommodation in that context. If it does not find a matching context, a new context is created for the object to live in.
With that said, every AppDomain also hosts a Default context. The Default Context in turn hosts Context Agnostic (Context Agile) objects. These objects are not bound to any context. Context Agile objects do not require any attributes, special usage policies and interception services. Take a look at the following table that summarizes how .NET components behave in cross-context access scenarios based on their context agility:
|
.NET classes that extend MarshalByRefObject |
.NET classes that extend ContextBoundObject |
.NET classes that niether extend from MarshalByRefObject nor ContextBoundObject |
Cross-Context calls within the same AppDomain (Intra AppDomain calls) |
Context-Agile. Direct Access. (Emulates a Classic COM object that aggregates the Free-threaded Marshaler) |
Context-Bound. Object is accessed from any other context only through a proxy. |
Context-Agile. Direct Access. (Emulates a classic COM object that aggregates the Free-threaded Marshaler) |
Cross-Context calls across AppDomains (Inter AppDomain calls) |
Exhibit Marshal-By-Reference semantics. Object is accessed from any other context, only through a proxy. |
Exhibit Marshal-By-Reference semantics. Object is accessed from any other context, only through a proxy. |
Exhibit Marshal-By-Value semantics. When tagged with the [serializable] attribute, a copy of the object is recreated in the caller AppDomain's context. |
Thread neutral behavior when accessed by unmanaged COM aware clients
Take a look at how a .NET component advertises its threading model to COM when an assembly is run through REGASM.EXE in order to make the appropriate registry entries for COM aware clients to look up.
The ThreadingModel
key under InprocServer32
has a value 'Both'. In Classic COM, objects that advertise their ThreadingModel
as 'Both' are willing to move into their caller's apartment, be it STA or MTA. In addition, 'Both' threaded objects that also aggregate the Free-threaded marshaler, give other apartments to which they are marshaled, direct interface pointer references as opposed to proxies. Context Agile .NET components (those that do not extend from ContextBoundObject
) are analogous to thread-neutral COM objects that aggregate the free-threaded marshaler. Let's check to see how the .NET component behaves as we pass interface references to the .NET component across COM apartments in the unmanaged client. Take a look at this simple C# class that we'll be exposing to the unmanaged COM client:
using System;
using System.Runtime.InteropServices;
public interface IHelloDotNet {
String GetThreadID();
}
[ClassInterface(ClassInterfaceType.None)]
public class HelloDotNet : IHelloDotNet
{
public HelloDotNet() {
}
public String GetThreadID() {
return AppDomain.GetCurrentThreadId().ToString();
}
}
The above class implements the GetThreadID
method from the IHelloDotNet
interface. This method returns the ID of the current thread that's executing in the AppDomain into which this object is loaded. To build the above class into an assembly and to make the appropriate registry entries for COM, issue the following commands from the command-line:
csc /target:library /out:HelloDotNet.dll HelloDotNet.cs
regasm HelloDotNet.dll /tlb:HelloDotNet.tlb
We'll now go on and consume the .NET component from a COM aware client. We'll use a C++ console application that will create the .NET component in its main thread (an STA) and then pass it across to two other apartments (an STA apartment and an MTA apartment) by spawning two worker threads. We'll take a look at what happens when a raw interface pointer to the object is passed across apartments. Then, we'll take a look at what happens when a marshaled reference is passed across apartments by using explicit inter-thread marshaling calls that use the CoMarshalInterface
/CoUnmarshalInterface
API family. Take a look at the code below (Error checking in the code is omitted for brevity):
.......
#import "mscorlib.tlb"
#import "HelloDotNet.tlb" no_namespace
long WINAPI MySTAThreadFunction(long lParam);
long WINAPI MyMTAThreadFunction(long lParam);
IHelloDotNetPtr spHelloNET = NULL;
IStream* g_pStream1 = NULL;
IStream* g_pStream2 = NULL;
int main(int argc, char* argv[])
{
.......
::CoInitialize(NULL);
cout << "The Thread ID of the primary STA thread is : "
<< ::GetCurrentThreadId() << endl;
hr = spHelloNET.CreateInstance(__uuidof(HelloDotNet));
cout << "From .NET when called from the primary STA Thread : "
<< spHelloNET->GetThreadID() << endl;
.......
hr = CoMarshalInterThreadInterfaceInStream(_uuidof(IHelloDotNet),
spHelloNET,
&g_pStream1);
hr = CoMarshalInterThreadInterfaceInStream(_uuidof(IHelloDotNet),
spHelloNET,
&g_pStream2);
hThreadSTA = CreateThread(NULL,0,
(LPTHREAD_START_ROUTINE)MySTAThreadFunction,
NULL,0 ,&dwThreadIDSTA);
cout << "The Thread ID of the STA based Worker thread is : "
<< dwThreadIDSTA << endl;
hThreadMTA = CreateThread(NULL,0,
(LPTHREAD_START_ROUTINE)MyMTAThreadFunction,
NULL,0,&dwThreadIDMTA);
cout << "The Thread ID of the MTA based Worker thread is : "
<< dwThreadIDMTA << endl;
::WaitForSingleObject(hThreadSTA,INFINITE);
::WaitForSingleObject(hThreadMTA,INFINITE);
return 0;
}
long WINAPI MySTAThreadFunction(long lParam)
{
::CoInitializeEx(NULL,COINIT_APARTMENTTHREADED);
cout << "From .NET when called from the STA Worker Thread (Direct Access) : "
<< spHelloNET->GetThreadID() << endl;
IHelloDotNetPtr spHello = NULL;
HRESULT hr = CoGetInterfaceAndReleaseStream(g_pStream1,
__uuidof(IHelloDotNet),
(void **)&spHello);
if(S_OK == hr)
{
cout << "From .NET when called from the STA Worker Thread (Marshaled) : "
<< spHello->GetThreadID() << endl;
}
return 0;
}
long WINAPI MyMTAThreadFunction(long lParam)
{
::CoInitializeEx(NULL,COINIT_MULTITHREADED);
cout << "From .NET when called from the MTA Worker Thread (Direct Access) : "
<< spHelloNET->GetThreadID() << endl;
IHelloDotNetPtr spHello = NULL;
HRESULT hr = CoGetInterfaceAndReleaseStream(g_pStream2,
__uuidof(IHelloDotNet),
(void **)&spHello);
if(S_OK == hr)
{
cout << "From .NET when called from the MTA Worker Thread (Marshaled) : "
<< spHello->GetThreadID() << endl;
}
return 0;
}
Here's the output that you get back when the console application is run.
The Thread ID of the primary STA thread is : 2220
From .NET when called from the primary STA Thread : 2220
The Thread ID of the STA based Worker thread is : 2292
The Thread ID of the MTA based Worker thread is : 2296
From .NET when called from the STA Worker Thread (Direct Access) : 2292
From .NET when called from the STA Worker Thread (Marshalled) : 2292
From .NET when called from the MTA Worker Thread (Direct Access) : 2296
From .NET when called from the MTA Worker Thread (Marshalled) : 2296
Notice that for all these calls, no thread switch occurs between the thread making the call in the client and the thread invoking the actual method in the .NET component. It other words, the .NET component is Context agile and always executes in the caller's thread. From the code snippet above, observe that the effect of marshaling an object reference (using inter-thread marshaling APIs such as CoMarshalInterThreadInterfaceInStream
/CoGetInterfaceAndReleaseStream
) is the same as that of passing a direct object reference across apartments. Eventually, the receiving apartment gets an apartment-neutral interface pointer that it can use to call into the .NET component. The .NET component exhibits all the behavior that is reminiscent of Both threaded Classic COM Components that aggregate the free-threaded marshaler.
In the first part of this article, we took a look at how you could expose Classic COM components to .NET applications executing under the purview of the Common Language Runtime (CLR). We saw how the COM interop seamlessly allows you to reuse existing COM components from managed code. Then, we skimmed through ways to invoke your COM component using both early binding and late binding along with ways to do runtime type checking and dynamic type discovery. We took a journey through understanding how delegates work in .NET, the role they play in the .NET event-handling model, and how the COM Interop acts as an adaptor to wire the connection points event handling model in classic COM to the delegate based Event handling model in .NET. We discussed about how to expose COM collections to .NET applications and use C#'s foreach
syntax to easily iterate through the elements of the collection. Then, we looked at how directional attributes in IDL files get mapped to the corresponding directional parameter types in C#. We also learnt about some of the reuse options available for Classic COM components from .NET applications using inheritance and containment. Finally, we saw how managed threads declare their apartment affiliations when invoking COM components.
In the latter half of this article, we took a dip into exploring how COM aware clients from the pre-.NET era could consume .NET components as if they were classic COM components. We saw how the CCW and the CLR facilitate this to happen seamlessly from a programming perspective. We briefly explored the possibilities of using attributes to emit metadata into .NET types so that the typelibrary generated could be tailored and fine-tuned to your requirements. We looked at how the exception handling mechanisms in the two worlds are correlative. We also discussed about how to go about receiving asynchronous event notifications from .NET components in unmanaged event sinks. Then, we turned our attention to the deployment options available and how to deploy .NET components as Shared assemblies. Lastly, we discussed the thread-neutral behavior of .NET components and saw how Context-agile .NET components are analogous to Classic COM 'Both' threaded components that aggregate the free-threaded marshaler (FTM).
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. Eventually, we COM developers don't 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 provided by the runtime 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 and the brave new �ber powerful .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.
"Virtually all aspects of the COM programming model have survived (interfaces, classes, attributes, context, and so on). Some may consider COM dead simply because CLR objects don't rely on IUnknown
-compliant vptrs/vtbls. I look at the CLR as breathing new life into the programming model that I've spent the last seven years of my life working with, and I know there are other programmers out there who share this sentiment."
- Don Box, in his 'House of COM' column in the MSDN Magazine(Dec, 2000)
"COM lives on in spirit, if not in body !"
- Peter Foreman, in the Developmentor DOTNET mailing list
I'd like to express my sincerest gratitude to Tom Archer for encouraging me to write and for including a part of this tutorial in his book Inside C#. I'd also like to thank the wonderful folks at the Developmentor DOTNET mailing list for giving .NET developers, food for thought, everyday.