Understanding COM Interop

.NET is a great platform for developing applications and many pieces of code that took lots of effort to write, is now made much simpler. As much as all of us would like to adopt .NET immediately, many organizations cannot. Lots of investment has been made developing applications using technologies like COM and these code bases cannot be junked overnight. Also, .NET may not be suitable for all types of projects. There are still some applications that are best left to low-level languages.
Given this need for organizations to preserve existing code, one of the important goals of .NET during its development was to promote interoperability with existing technologies. .NET interoperability comes into three flavors:
  • Interoperability of .NET code with COM components (called as COM interop)
  • Interoperability of COM components with .NET (called .NET interop)
  • Interoperability of .NET code with Win32 DLLs (called P/Invoke)

Each of these models have different requirements and best practices and this article we will discuss about COM interop. More specifically, we will see how to write a COM component and then invoke it from .NET. We will also see some best practices for developing well behaved COM interop solutions.

One important thing to understand is that components written in .NET are managed whereas components written using COM are unmanaged. Therefore, the first challenge to overcome is, to bridge these two models. Each model has its own way of memory allocation, object lifetime management and parameter passing convention that to bridge these two models require the usage of an intermediary that handles all these differences. Having an intermediary is important because we do not want applications writing this code for each application that wants to interop with COM components. The .NET developers were aware of this and thus gave an intermediary called as the Runtime Callable Wrapper (RCW). The CCW takes care of all the intricacies of communicating between the two platforms. The following figure shows the role of the CCW.
The main job of the RCW is to hide all the differences between the two worlds and as such is an object that is created from the managed heap. Only one instance of the RCW is ever created, irrespective of how many .NET clients access it. Once a .NET client object wants to instantiate a COM client, the RCW intervenes and creates the object on your behalf and then manages the lifetime of the COM object. COM objects are reference counted. This means that each client accessesing the COM client will increase its reference count by 1 and each release of the reference decreases its reference count by 1. When the reference count becomes 0, the COM client is released. This counting happens by calling the Add and Release methods of the IUnknown interface, an interface that all COM objects may implement. All these intricacies are taken over by the RCW. It is important to remember that .NET is a garbage collected environment. So, when will the RCW be collected?? The RCW will be collected when the last client holding a reference to the COM object releases the reference. At this point, the reference count on the COM object becomes 0 and will thus be collected by the operating system while the RCW will be garbage collected by the .NET runtime.
COM components also report errors using HRESULTs. The RCW maps the various HRESULTs into .NET exceptions that can then be captured in your application.
Creating an RCW
Having seen the basics, the next step is to understand how the RCW is itself created. There are two methods for creating the RCW.
  1. Using the references dialog box of Visual Studio .NET
  2. Using the TLBIMP command-line tool

In this article, we will see an example of both these methods. As an example to illustrate the creation of the RCW, let us consider a simple example of a COM component that just echos back a string that is provided as input. We will see the development of this COM component, followed by the generation of the RCW. We will be using Visual Basic 6.0 to create the COM components although you could use any language of your choice that can generate COM components. You will need to adapt the example suitably to suit the needs of your language.

Let us first create the COM component. To do this, open Visual Basic 6.0 and choose to create an ActiveX DLL project. Name the project as HelloWorld and the default class as CHelloWorld. In this class we will create a function called sayHello. Here is the code for the function:

Public Function sayHello(ByVal inputString As String) As String
    sayHello = "You said: " & inputString
End Function

After you have created the function, choose File > Make DLL to create the DLL file. Since we have named our project as HelloWorld, the DLL will be called HelloWorld.DLL. Once you have created the COM DLL, the next step is to create the .NET code that will call into this DLL. As mentioned before, a .NET code will access the COM component via an RCW and there are two ways you can create the RCW. The first method involves using the Visual Studio .NET references dialog box and is the most simplest of the methods. To use this method, open Visual Studio .NET and choose File > New > Project > Visual Basic Projects > Console Application. This will create a console application project in the location of your choice. Once the project has been created, in the Solution Explorer, right-click the references node and choose Add New Reference. In the Add Reference dialog box, choose the COM tab and locate the HelloWorld DLL that we created. The following figure shows the located DLL.
Once you have created a reference to the HelloWorld DLL, you will now be able to access the DLL. But before that, what did Visual Studio .NET do?? If you check out the bin folder of your .NET console application, there will be a file called Interop.HelloWorld.dll file that just appeared there!! This is the RCW and Visual Studio .NET automatically created it for you. You can open this assembly in ILDASM to see what it has. The following figure shows the contents of the assembly inside ILDASM.
What's all this?? COM programming is all about using interfaces and when you write a COM project in Visual Basic, an interface is silently created for you behind the scenes with the same name of the class prefixed with an underscore (_) character. Since we created a class called CHelloWorld, an interface for the same is created called as _CHelloWorld which you see above. You can also see that the CHelloWorld class itlself has been created as an interface that implements _CHelloWorld. Finally, a concrete class implementation CHelloWorldClass has been created that implements all the other interfaces. You can now interact with the COM object using either the CHelloWorld interface or using the concrete class itself. Here is the code for the .NET class.

Module Module1

    Sub Main()
        Dim oHello As HelloWorld.CHelloWorldClass

        oHello = New HelloWorld.CHelloWorldClass
        Console.WriteLine(oHello.sayHello("Hello, World"))
    End Sub

End Module

Note that we have programmed the .NET class against the concrete implementation. Do you see anything new being done?? No. Programming against an COM object is as similar as programming against another .NET object. To the developer there is no new thing that needs to be done, except adding the reference. The rest of the programming paradigm remains the same. Now, when you execute the project, you will the string being echoed back.
Another method is to use the TLBIMP utility. The Type Library Importer converts the type definitions found within a COM type library into equivalent definitions in a common language runtime assembly. The output of Tlbimp.exe is a binary file (an assembly) that contains runtime metadata for the types defined within the original type library. For our example DLL created earlier, here is the TLBIMP command.

tlbimp HelloWorld.dll /out:HelloWorldInterop.dll

The usage of TLBIMP is very simple. You just need to point it to the DLL and then specify the output file to create. The output then would be the assembly that you can then reference in your .NET code. In this, you will add a .NET reference (much like adding references for other .NET assemblies) and then use it. The usage of the assembly is very similar to the example shown earlier.
Well, that's all there is to it! In this article we saw the details of how to interop with COM. Working with COM is extermely simple (well, for most cases) and .NET pretty much hides all the complexity using the RCW. Some of the things that you should keep in mind when interoperating with COM components are:
  • Always provide a type library for the component. A type library will expose all the types in your DLL file and it is using this that the TLBIMP utility creates its interop assembly.
  • Since all .NET objects inherit from System.Object make sure that none of your COM DLL functions have name clashes with the System.Object functions.
  • Use compatable data types between the .NET assembly and the COM DLL. Types which are compatible between both worlds are called blittable. For example, the integer data type is a blittable data type, whereas the string data type is not, since strings are represented as pointers in COM.
  • Deploying an interop assembly is a straight forward process. The interop DLL can be deployed either as a shared assembly or a private assembly. A private assembly resides in the same folder as the application, while the shared assembly resides in the GAC.
  • Note that TLBIMP may not always produce the correct interop code especially if the COM DLL is IDL based. There are some issues with respect to these. When you are faced with such a situation, it is better to use a language like managed C++ to write the interop code. Managed C++ requires no interop code and is the only language that allows both managed and unmanaged code to be mixed in the same file. You can wrap the calls to the unmanaged DLL through C++ and then expose the C++ code to .NET through interop. This way, you avoid the issues related to IDL.
  • Using the default interop model is great for simple and flat APIs and where the COM DLL is not very complex and it is automation compatible.

Have fun!!