The important goals of .NET during its development was to promote interoperability with existing technologies. .NET interoperability comes in three types:
- 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)
.NET runtime allows us to use legacy COM code from .NET components. We can call it backward compatibility. In the same way, .NET runtime also provides us forward compatibility, means accessing .NET components from COM components.
Differences Between .NET Framework and COM Framework :
The .NET framework object model and its workings are different from Component Object Model (COM) and its workings. For example, clients of .NET components don't have to worry about the lifetime of the object. Common Language Runtime (CLR) manages things for them. In contrast, clients of COM objects must take care of the lifetime of the object. Similarly, .NET objects live in the memory space that is managed by CLR. CLR can move objects around in the memory for performance reasons and update the references of objects accordingly, but COM object clients have the actual address of the object and depend on the object to stay on the same memory location.
Similarly, .NET runtime provides many new features and constructs to managed components. For example, .NET components can have parameterized constructors, functions of the components can have accessibility attributes (like public, protected, internal, and others) associated with them, and components can also have static methods. Apart from these features, there are many others. These include ones that are not accessible to COM clients because standard implementation of COM does not recognize these features. Therefore .NET runtime must put something in between the two, .NET server and COM client, to act as mediator.
Interoperability of .NET code with COM components (called as COM interop)
A .NET component in C# named CManagedServer. This component is in the ManagedServer assembly. Client side, I have created a Visual Basic (VB) 6.0-based client that uses the services of our managed server.
COM Callable Wrapper:
Step 1
First, we create the managed server. The following code for CManagedServer is very simple and contains a single method named "SayHello."
using System;
namespace ManagedServer
{
public class CManagedServer
{
public CManagedServer()
{}
public string SayHello(string r_strName)
{
string str ;
str = "Hello " + r_strName ;
return str ;
}
}
}
}
For compilation and creation of ManagedServer assembly, use the following command:
csc /out:ManagedServer.dll /target:library ManagedServer.cs
This command will create the ManagedServer.dll file. This is our managed server.
Step 2
Standard COM implementation relies on the Windows registry for looking up the information related to COM components, like CLSID, Interface IDs, the path of the component's housing (DLL/exe), the component threading model, etc. .NET framework does not depend on the registry and uses metadata for this information. Therefore, we have to generate the COM-compatible registry entries for our managed server so that the COM runtime could instantiate our server. Like tlbimp.exe, there is a tool named regasm.exe. This tool reads the metadata information within an assembly and adds the corresponding COM-compatible registry entries. Classes in the assembly are not COM creatable until they are actually registered in the Windows registry.
Regasm.exe tool can also generate the COM type library for our manager server. We will reference this type library in VB 6.0 client.
The following command can be used for creating the registry entries and type library for our managed server:
regasm ManagedServer.dll /tlb
At this point, standard COM registry entries will have been created.
Step 3
The following is the code that creates the object and calls its SayHello Method.
Private Sub
mdAcces()
ManagedComponent_Click()
Dim objServer As New ManagedServer.CManagedServer
MsgBox(objServer.SayHello("15 Seconds reader"))
objServer = Nothing
The above code is very simple and COM Interoperability wrappers have abstracted all the complexities of accessing .NET components.
Running the UnmanagedClient.exe and pressing the "Access managed component" button, will produce the following output:
Interoperability of COM components with .NET (called .NET interoporability)
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 RCW takes care of all the intricacies of communicating between the two platforms. The following figure shows the role of the RCW.
Runtime Callable Wrapper(RCW):
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 accessing 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. 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.
Creating an RCW
Using the references dialog box of Visual Studio .NET
Using the TLBIMP command-line tool
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
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.
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.
You can now interact with the COM object . Here is the code for the .NET class.
using System;
class Sample
{
public static void Main()
{
private CHelloWorldClass oHello;
oHello = New CHelloWorldClass();
Console.WriteLine(oHello.sayHello("Hello, World"));
Console.ReadLine();
}
}
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 compatible 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.
.NET TYPES CALLING Win32 DLLs
Platform Invoke (PInvoke):
The platform invoke services offers a method to call functions that are exported from an unmanaged DLL. The most distinctive use of PInvoke is to allow .Net components to interact with the Win32 API. PInvoke is also used to access functions exports defined in custom DLLs.
To exemplify the use of PInvoke I create a C# class that makes a call to theWin32 Message Box () function.
Let us progress to how to call Win32 MessageBox() function from a C# class using PInvoke.
namespace APIExample
{
using System;
// Library need to be referred to use PInvoke types.
using System.Runtime.InteropServices;
public class PinvokeClient
{
[DllImport("user32")]
public static extern int MessageBox(int hWnd,
String pText ,
String pCaption ,
int uType);
public static int Main(string[] args)
{
String pText = "HELLO!";
String pCaption = "HelloWorld";
MessageBox(0,pText,pCaption,0);
return 0;
}
}
}
Explanation:
Before calling a Win32 Dll we have to declare the function to call using the static and extern C# keywords. After this you have to specify the name of the raw DLL that contain the function you are attempting to call,as shown here.
[DllImport("user32")] public static extern int MessageBox(.......);
After declare the DLL Pass the arguments such as pText,pCaption.It should be clear that it does not matter in which order you specify the values. In the above way one can use .Net types calling any type Win32 API.
Disadvantage:
PInvoke has an overhead of between 10 and 30 x86 instructions per call. In addition to this fixed cost, marshaling creates additional overhead. There is no marshaling cost between blittable types that have the same representation in managed and unmanaged code. For example, there is no cost to translate between int and Int32.
For higher performance, it may be necessary to have fewer PInvoke calls that marshal as much data as possible, rather than have more calls that marshal less data per call. Or somewhat more memorably: prefer a chunky over a chatty API.
Conclusion:
.NET runtime provides COM Interoperability wrappers for overcoming the differences between .NET and COM environments. For example, runtime creates an instance of COM Callable Wrapper (CCW) when a COM client accesses a .NET component. In the same way, an instance of Runtime Callable Wrapper (RCW) is created when a .NET client accesses a COM component. These wrappers abstract the differences and provide the seamless integration between the two environments. Along with these PInvoke facility can be utilized but of course it is a costly affair as far as memory is concerned.