Tuesday, January 13, 2009

Building .NET classes as COM component

From the time Microsoft engineers started working on the ideas behind COM in 1998, COM went through quite an evolution. Once .NET was released everything was about the CLR. Those business systems made lot of investments on those COM developments and they may not be willing to invest more money to build their components into .NET. Also this will make a severe impact in productivity.
Fortunately, switching from COM to .NET involves no such radical loss of productivity. The concept of providing bridge between .NET and COM components is .NET-COM interoperability. Microsoft .NET Framework provides system, tools, and strategies that enable strong integration with past technologies and allow legacy code to be integrated with new .NET components. It provides a bridge between the .NET and COM and vice versa.
There are two key concepts that make it much easier to move from COM development to .NET development, without any loss of code base or productivity.
1. Interaction with COM components from .NET
2. Interaction with .NET components from COM

COM is a binary reusable object which exposes its functionality to other components. When a client object asks for instances of server object, the server instantiates those objects and handout references to the client. So, a COM component can act as a binary contract between caller and callee. This binary contract is defined in a document known as Type library. The Type library describes to a potential client the services available from a particular server. Each COM components will expose a set of interfaces through which the communication between COM components will occurs. It will clear by diagram.


In the above figure the IUnknown and IDispatch are the interfaces and QueryInterface, AddRef, Release, etc., are the methods exposed by those interfaces.
The communication between the .NET objects occurs through Objects, there are no such interfaces for communication. So, in .NET component, there is no type libraries, instead they deal with assemblies. Assembly is a collection of types and resources that are built to work together and form a logical unit of functionality. All the information related to the assembly will be held in assembly metadata. Unlike the communication between COM components, the communication between .NET components is Object based.


Calling COM components from .NET Client
Generally COM components will expose interfaces to communicate with other objects. A .NET client cannot directly communicate with a COM component because the interfaces exposed by a COM component may not be read by the .NET application. So, to communicate with a COM component, the COM component should be wrapped in such a way that the.NET client application can understand the COM component. This wrapper is known as Runtime Callable Wrapper (RCW).
The .NET SDK provides Runtime Callable Wrapper (RCW) which wraps the COM components and exposes it into to the .NET client application.




Fig.2 calling a COM component from .NET client
To communicate with a COM component, there should be Runtime Callable Wrapper (RCW). RCW can be generated by using VS.NET or by the use of TlbImp.exe utility. Both the ways will read the type library and uses System.Runtime.InteropServices.TypeLibConverter class to generate the RCW. This class reads the type library and converts those descriptions into a wrapper (RCW). After generating the RCW, the .NET client should import its namespace. Now the client application can call the RCW object as native calls.
When a client calls a function, the call is transferred to the RCW. The RCW internally calls the native COM function coCreateInstance there by creating the COM object that it wraps. The RCW converts each call to the COM calling convention. Once the object has been created successfully, the .NET client application can access the COM objects as like native object calls.


Calling .NET components from COM Client
When a COM client requests a server, first it searches in the registry entry and then the communication starts. Calling a .NET component from a COM component is not a trivial exercise. The .NET objects communicate through Objects. But the Object based communication may not be recognized by the COM clients. So, to communicate with the .NET component from the COM component, the .NET component should be wrapped in such a way that the COM client can identify this .NET component. This wrapper is known as COM Callable Wrapper (CCW). The COM Callable Wrapper (CCW) will be used to wrap the .NET components and used to interact with the COM clients. CCW will be created by the .NET utility RegAsm.exe. This reads metadata of the .NET component and generates the CCW. This tool will make a registry entry for the .NET components.



Fig.3 calling a .NET component from COM client
Generally COM client instantiates objects through its native method coCreateInstance. While interacting with .NET objects, the COM client creates .NET objects by coCreateInstance through CCW.
Internally, when coCreateInstance is called, the call will redirect to the registry entry and the registry will redirect the call to the registered server, mscoree.dll. This mscoree.dll will inspect the requested CLSID and reads the registry to find the .NET class and the assembly that contains the class and rolls a CCW on that .NET class.
When a client makes a call to the .NET object, first the call will go to CCW. The CCW converts all the native COM types to their .NET equivalents and also converts the results back from the .NET to COM.


Now the problem is, suppose that I have written a nice library, set of utility functions, etc.. running under the .NET Framework, however I want to use this under pre .NET development environments. For Raman, he would like to use VB6 specifically. Enter the COM Callable Wrapper (CCW) to create a proxy that will provide access to our functions through interface pointers. This can be accomplished through the use of those fun little attribute tags (I keep finding them more and more useful everyday) and with an interface of course.
To begin, you will need to include the System.Runtime.InteropServices; namespace. The first thing we will do is create an interface with the same name as the class prefixed with an _ (underscore). Within this interface we will need to include all functions we want to "export" from within our .NET assembly. It is important that we apply the InterfaceType attribute to our interface we declare; this will expose our interface to COM.






COM Objects are of the type ClassLibrary. The COM Object generates a DLL file.
To exposing the VC# objects to the COM world requires the following ...
1. The class must be public
2. Properties, methods, and events must be public.
3. Properties and methods must be declared on the class interface.
4. Events must be declared in the event interface.


Other public members in the class that are not declared in these interfaces will not be visible to COM, but they will be visible to other .NET Framework objects. To expose properties and methods to COM, you must declare them on the class interface and mark them with a DispId attribute, and implement them in the class. The order the members are declared in the interface is the order used for the COM vtable.
To expose events from your class, you must declare them on the events interface and mark them with a DispId attribute. The class should not implement this interface. The class implements the class interface (it can implement more than one interface, but the first implementation will be the default class interface).

Every Interface needs a GUID property set before the interface name. To generate the unique Guid , use the guidgen.exe utility and select the Registry Format.

Here is how the interface class looks like ...

[Guid("694C1820-04B6-4988-928F-FD858B95C880")]
public interface DBCOM_Interface
{
[DispId(1)]
void Init(string userid , string password);
[DispId(2)]
bool ExecuteSelectCommand(string selCommand);
[DispId(3)]
bool NextRow();
[DispId(4)]
void ExecuteNonSelectCommand(string insCommand);
[DispId(5)]
string GetColumnData(int pos);
}


The actual class decleration
[Guid("9E5E5FB2-219D-4ee7-AB27-E4DBED8E123E"),
ClassInterface(ClassInterfaceType.None),
ComSourceInterfaces(typeof(DBCOM_Events))]
public class DBCOM_Class : DBCOM_Interface
{

......................................
}

Note the following property set before the class ClassInterface(ClassInterfaceType.None),
ComSourceInterfaces(typeof(DBCOM_Events))]
The ClassInterfaceType.None indicates ..
Indicates that no class interface is generated for the class. If no interfaces are implemented explicitly, the class will only provide late bound access through IDispatch. IDispatch interfaces are what Automation clients such as VB use to enable COM support.Users are expected to expose functionality through interfaces that are explicitly implemented by the class. This is the recommended setting for ClassInterfaceAttribute.
The ComSourceInterfaces(typeof(DBCOM_Events))]Identifies a list of interfaces that are exposed as COM event sources for the attributed class
.
Before you build the COM object we have to Register the object for COM Interop. To do this right click the project name in the Solution Explorer. Click Properties. Click Configuration ->Build. Expand the output section. Set the Register for COM Interop to true.
Indicates that your managed application will expose a COM object (a COM-callable wrapper) that allows a COM object to interact with your managed application.
In order for the COM object to be exposed, your class library assembly must also have a strong name.

Programming model comparison of .NET-COM interoperability

.NET

COM

Object based communication

Interface based communication

Garbage Collector to manage memory

Reference count will be used to manage memory

Type Standard objects

Binary Standard objects

Objects are created by normal new operator

Objects are created by coCreateInstance

Exceptions will be returned

HRESULT will be returned

Object info resides in assembly files

Object info resides in Type library



Before the application starts to communicate, there are some technical constraints associated with this. When an object is transmitted to a receiver which is in a separate machine/process (managed/unmanaged) space, the object may need to undergo a transformation according to the native type to make it suitable for use by the recipient. That is the object will be converted into a recipient readable form. This process of converting an object between types when sending it across contexts is known as marshaling. The next section of the paper will gives an overview of marshalling in .NET.

.NET Marshalling
Thus .NET runtime automatically generates code to translate calls between managed code and unmanaged code. While transferring calls between these two codes, .NET handles the data type conversion also. This technique of automatically binding with the server data type to the client data type is known as marshalling. Marshaling occurs between managed heap and unmanaged heap.

Thus the communication between .NET applications and COM applications occurs through RCW and CCW.
As you have seen, COM applications can implement .NET types to achieve type compatibility or a .NET type can implement COM interfaces to achieve binary compatibility with related coclasses.
Although the managed clients can interact with the unmanaged objects, the managed client expects that the unmanaged object should act exactly the same as managed object.
When developing against the unmanaged component through COM interoperability, managed code developers will not be able to use some features of .NET like parameterized constructors, static methods, inheritance, etc., migrating an existing component or writing a managed wrapper will make the component easier to use for managed code developers. In some cases, the developer wants to migrate parts of the application to .NET so that application can take advantage of the new features that the .NET Framework offers. For example, ASP .NET provides advanced data binding, browser-dependent user interface generation, and improved configuration and deployment. The designer should evaluate when the value of bringing these new features in to the application outweigh the cost of code migration.

No comments: