Introduction
As one of the early steps with the latest project at my workplace, I had to get a webservice client running from a native coded C++ application where there really was no option to migrate the whole application that's been under development and on the market for years in native C++. So I had to expose both objects and code from the application to the webservice client and had to be able to use the service client from the C++ code. At the time I needed a simple and small code example to test and study the basic anatomy of how all that could be done just like this little calculator I am presenting here.
What I aim to achieve with this article, is to allow a first approach at the use of COM interoperability between Managed and Native coding contexts without going into much of the details and architecture surrounding it. I hope this will be a starting point for many that seek to start writing code without much knowledge about it and also for those who are in a tight deadline to get this kind of interoperability working for their first time. The detailed knowledge and need to go deeper into the entrails of these techniques should come from hands-on experience and natural instinct to learn about something. I'm one who thinks that you're not supposed to buy and read a whole book about a subject without having been able to take a first glance and toy around with it.
Using the Code
For this simple example I used only an interface and a class associated with it in each COM Component. This applied both for the Native COM Component and the C# one. So making a simple analysis we'll have on both C++ and C# components something like this:
class SimpleCalc : ISimpleCalc
{
double Add( double a, double b );
double Subtract( double a, double b );
double Multiply( double a, double b );
double Divide( double a, double b );
}
Keep in mind that only the C++ component has a real implementation for its class by performing the desired calculation, and that the C# works as a Managed Code Wrapper for this. One way to image this is the following:
Fig. 1: A diagram for the architecture developed.
To get the whole thing running and test it there are some steps to take for each piece of the image. Note that both SimpleCalc.dll and SimpleCalcClient.exe are projects created with Visual Studio 6. The SimpleCalc.dll:
- Build the C++ COM Component;
- Register it using the "regsvr32" tool that comes with windows using the command line:
regsvr32 SimpleCalc.dll;
The SimpleCalcInterop.dll:
- Generate a key file to sign the assembly SampleKeys.snk by using a .NET Framework tool with the command line:
sn �k SampleKeys.snk;
- Build the C# COM Component;
- Register the component in 2 steps with some .NET Framework tools on the command line:
The SimpleCalcClient.exe:
- Build the C++ COM Client;
There are some notes to consider on some good practices and also some missing steps already taken with the source code provided in order to get things working with these few simple steps:
tlbexp SimpleCalcInterop.dll /out:SimpleCalcInterop.tlb
and as you can see in the code, from there on is just copying it into the project directory and use it.
Going Inside the Code
This is where the things go further than just getting a first glance at something like this working.
There are several good articles about how all this works and the basics behind it all, not to mention the all the minor details related to COM interoperability and using COM components, especially here in CodeProject, and a couple of those are the references at the end of this article. For now, the idea is to create a first mental model on how things must be done to get this up and running as fast as possible when you have little time, when in truth, the density of the content one can say about this truly fills up books.
Starting with the C++ COM component again, things couldn't be simpler. You start by running Visual Studio 6 and create a new "ATL COM Appwizard" which will generate a DLL, and also allow merging of proxy/stub code.
Then add a "Simple ATL Object" and give it a short name. That's it, now you have a dual interface (usually associated to the IDispatch
interface) COM component ready to be compiled and instantiated in a COM client. Something happened here that is not visible unless you look for it. Inside the IDL file you'll find now an interface declaration, along with a class declaration inside a library declaration. The library is basically a namespace and the class declaration will stay the same even as you add methods and members to the interface, though the class and interface have one particular property in common, the "uuid." This is essentially a unique identifier string within the operating system, and it's generated in a method that Microsoft relies on to make sure that is truly one amazing coincidence (the one in a billion type of coincidence) for two software suppliers to be able to generate and install two distinct components on the same client machine with the same unique identifier, or even for a developer to generate a component using an id that already is occupied in that machine. This id is basically a set of values in a string that looks like this one from the supplied source code:
C79D6B3B-79DC-4EA4-83DF-0AA8EF02023D
These ids are meant to be unique and stored on the system registry once the component is registered on the system. You can even use the "regedit" tool to look for them when you need to clean up by hand some component you wanted to uninstall, remove from the registry or really delete it all together.
The next step is adding methods and/or members to the generated interface and it'll appear very much like regular C++ except for some extra data types. You'll also notice that there's no way you can set the return for the methods, and so you'll have to use a pointer in one of the arguments. (Important: avoid using reference arguments here) When you add a method, a line is added to the IDL file (an intermediate interface and class description file) which implements it's own language and will be compiled ahead of the remaining source code to generate some source code itself. This following line is a good example:
[id(1), helpstring("method Add")] HRESULT Add([in]double a, [in]double b,
[out, retval]double *result);
Take note on what composes it, an "id", a "helpstring" and then a standard C++ method declaration with some extra keywords "in"/"out"/"retval". The "id" reveals that all the interface methods and members are numbered and only the members are allowed to use the same id for both get and put methods. The "helpstring" is just something to aid you to keep track of what the method is supposed to represent. And the keywords "in"/"out" represent exactly the type of argument, if it's an input or output argument, though see also the "retval" which is only allowed to be used on the last argument which also has to be an "out" argument and will translate into setting the return for that method being that last argument. This is best visible when you're using the code in Visual Studio 2005 or Visual C# 2005 express and add the DLL as reference and try using it. In the end, that line will translate into a method prototype which would be written like this:
double Add( double a, double b );
The code itself for this method, respects the line in the idl file, so you'll end up with a method like this:
STDMETHODIMP CSimpleCalculator::Add(double a, double b, double *result)
{
*result=a+b;
return S_OK;
}
The next step becomes implementing all the source code you want to make in your component. Then build the DLL and it's time to generate the interop assembly to use with .NET managed code.
If you take a look at the example supplied with the source code, the relevant project is in the "SimpleCalc" folder and then the relevant files to peek into are the SimpleCalc.idl, SimpleCalculator.h, and SimpleCalculator.cpp. All the rest is either part of the project definition or generated by the build step or even by the Visual Studio wizards (though it can be just as important).
Finally, register the generated dll using the regsvr32
command as in
regsvr32 SimpleCalc.dll
This is where "tlbimp" comes in, as well as "sn." In order to use these, you'll need to run the Visual Studio 2005/Express command line console with the .NET Framework environment loaded or find them in your Visual Studio 2005/Express SDK folder. The "sn" is used to generate a file with a pair of keys using the command mentioned earlier
sn �k SampleKeys.snk
The keys file is something you can use over and over to sign your own components, though you'll want to centralize the keys you use when developing a larger application with multiple assemblies both to sign them and to make them strongly named, which is quite important for the .NET Framework when using the assemblies as references among different projects.
Now it's time to start taking a look at the wrapper itself, the C# COM component. The procedure is quite simple, open the Visual Studio 2005/Express and start a new project, a Class Library and open the Class file in it.
There are two methods to instantiate objects from a COM Component, one is Early Binding and the other is Late Binding. The major difference between these is like choosing between code complexity and the dependency of the interop assembly or not. With Early Binding you need to add the interop assembly as a reference in order to be able to build the application or DLL, though it will have very simple code, take a look at the Add method in the "SimpleCalcInterop" project:
public double Add(double a, double b)
{
double result=0;
try
{
if (mCalc!=null)
result=mCalc.Add(a, b);
}
catch (COMException ex)
{
System.Diagnostics.Debug.WriteLine("COM Exception : " + ex.Message);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("Unknown Exception : " +
ex.Message);
}
return result;
}
In turn, the mCalc
instance is instantiated and destroyed by this snippet:
public SimpleCalc()
{
try
{
mCalc = new Interop.SimpleCalc.SimpleCalculator();
GC.KeepAlive(mCalc);
}
catch(COMException ex)
{
System.Diagnostics.Debug.WriteLine("COM Exception : " + ex.Message);
}
catch(Exception ex)
{
System.Diagnostics.Debug.WriteLine("Unknown Exception : " +
ex.Message);
}
}
~SimpleCalc()
{
if(mCalc!=null)
Marshal.ReleaseComObject(mCalc);
mCalc = null;
}
As you can see, the code required to perform each action, instantiate, call a method and destroy the instance if all goes right is literally a couple of lines in most cases.
Instantiate:
ISimpleCalculator mCalc = new SimpleCalculator();
GC.KeepAlive(mCalc);
Call the Add
method:
if (mCalc!=null)
return mCalc.Add(a, b);
Destroy the instance:
if(mCalc!=null)
Marshal.ReleaseComObject(mCalc);
mCalc = null;
With the late binding there's no need to add the reference, it's assumed the component is correctly registered in the system registry, but the code really grows in size and complexity. The keywords here become "reflection" and "meta-information". Since the application or DLL does not include or import any interface or reference to the COM component itself, all it does is use information on the registry and the information exposed by the COM Component DLL itself. As a result, to perform the Add
operation with late binding you have the following code:
public double Add(double a, double b)
{
double result = 0;
result = doCalc(a, b, "Add");
return result;
}
public double doCalc(double a, double b, String oper)
{
double result = 0;
try
{
object simpleCalcInterop = null;
Type simpleCalcType;
simpleCalcType = Type.GetTypeFromProgID(
"SimpleCalc.SimpleCalculator");
simpleCalcInterop = Activator.CreateInstance(simpleCalcType);
object[] simpleCalcParams=new object[2];
simpleCalcParams[0]=a;
simpleCalcParams[1]=b;
result = (double)simpleCalcType.InvokeMember(
oper,
BindingFlags.Default | BindingFlags.InvokeMethod,
null,
simpleCalcInterop,
simpleCalcParams
);
}
catch (COMException ex)
{
System.Diagnostics.Debug.WriteLine("COM Exception : " + ex.Message);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("Unknow Exception : " +
ex.Message);
}
return result;
}
Similarly to any code there's the instantiation and method call, though in this case there's no especially crafted code for COM interoperability such as the direct calls to the garbage collector (GC
) or the system Marshal
object. In this case I let the garbage collector do its job, though there's also a lot of code related to the abstract concept of object and even reflection which is base on meta-information about both the interface and the exposed information on the DLL.
Instantiation:
object simpleCalcInterop = null;
Type simpleCalcType;
simpleCalcType = Type.GetTypeFromProgID("SimpleCalc.SimpleCalculator");
simpleCalcInterop = Activator.CreateInstance(simpleCalcType);
Call the Add
method:
object[] simpleCalcParams=new object[2];
simpleCalcParams[0]=a;
simpleCalcParams[1]=b;
result = (double)simpleCalcType.InvokeMember(
oper,
BindingFlags.Default | BindingFlags.InvokeMethod,
null,
simpleCalcInterop,
simpleCalcParams
);
The major visible difference is the way things are done, such as using the object
and Type
types, which use static methods to instantiate the desired object from its name. Then the object
type is used again in an array to send the parameters to the member invoked by its name again, and supplying all the details required such as the instance to call it from.
This is visible on the SimpleCalcInteropLateBinding
project also supplied in the source code.
The next step after filling in the code for each method you want visible in the wrapper or in the C# COM component, you have to make sure it'll have the desired COM behavior so there are attributes to be set and an interface to be declared, and the final result is as you can see on the supplied "SimpleCalcInterop" project:
[ComVisible(true),
InterfaceType(ComInterfaceType.InterfaceIsDual),
Guid("03C69B60-2F4F-4022-855A-F9702E40BF3A")]
public interface ISimpleCalc
{
double Add(double a, double b);
double Subtract(double a, double b);
double Multiply(double a, double b);
double Divide(double a, double b);
}
[ComVisible(true),
ClassInterface(ClassInterfaceType.None),
Guid("A8861345-3750-49df-BFBC-E16FE2DFC87B")]
public class SimpleCalc : ISimpleCalc
{
�
}
After all this is done, the .NET Framework needs things done in a slightly different manner to register a COM component. Aside from getting an entry in the registry, it also needs to place a copy registered on the global assembly cache (GAC). To register the assembly in the registry it's as simple as
regasm SimpleCalcInterop.dll
and to get it in the GAC you run the
gacutil /i SimpleCalcInterop.dll
using the Visual Studio 2005/Express command prompt.
At this point it's finally time to take a look at the client application for the COM component created in C#. Here the same techniques apply, but the code gets different, given it's also a completely different coding language so the client code supplied uses both Early and Late Binding techniques. So what you do for this final step resumes to running Visual Studio 6 and create an application using the MFC AppWizard and make it a dialog application to keep things simple.
In order to use early binding you need to use the "tlbexp" tool in the Visual Studio 2005/Express command prompt again, with the command
tlbexp SimpleCalcInterop.dll /out: SimpleCalcInterop.tlb
Then copy the TLB file to the client application project folder.
A sample of early binding to perform the Add method in C++ is:
#import "SimpleCalcInterop.tlb" named_guids
using namespace SimpleCalcInterop;
void CSimpleCalcClientDlg::OnBtAddEb()
{
ISimpleCalc *calculator=NULL;
HRESULT hr = CoCreateInstance(
CLSID_SimpleCalc,
NULL,
CLSCTX_INPROC_SERVER,
IID_ISimpleCalc,
(void**)&calculator
);
if(!SUCCEEDED(hr))
{
AfxMessageBox("An error occurred while trying to create the" +
"instance for the .NET COM object.");
return;
}
double oper1, oper2;
CString str;
m_Oper1.GetWindowText(str);
oper1=(double)atof(str.GetBuffer(str.GetLength()-1));
m_Oper2.GetWindowText(str);
oper2=(double)atof(str.GetBuffer(str.GetLength()-1));
double result=calculator->Add(oper1, oper2);
str.Format("%lf", result);
m_Result.SetWindowText(str);
calculator->Release();
}
This example does it all from beginning, instantiate, calls the method and destroys the instance, and even imports the type library. The named_guids
is a keyword that makes sure the unique identifiers get qualified names such as CLSID_SimpleCalc
instead of having to declare an instance of a unique identifier and using the unique id string. The last piece of COM interoperability code I'd like to show you is for Late Binding in C++ to perform the Add
method again:
void CSimpleCalcClientDlg::OnBtAddLb()
{
HRESULT hr = 0;
CLSID cls;
OLECHAR progid[255];
IDispatch* calculator=NULL;
wcscpy( progid, L"SimpleCalcInterop.SimpleCalc");
hr = CLSIDFromProgID (progid, &cls);
hr = CoCreateInstance (
cls,
NULL,
CLSCTX_INPROC_SERVER,
__uuidof(IDispatch),
(void**)&calculator
);
if (!SUCCEEDED(hr))
{
AfxMessageBox("An error occurred while trying to create the" +
"instance for the .NET COM object.");
return;
}
double oper1, oper2;
CString str;
m_Oper1.GetWindowText(str);
oper1=(double)atof(str.GetBuffer(str.GetLength()-1));
m_Oper2.GetWindowText(str);
oper2=(double)atof(str.GetBuffer(str.GetLength()-1));
VARIANT VarResult = {0};
VARIANTARG vargs[2];
VARIANT arg1,arg2;
DISPID dispid;
DISPPARAMS dispparamsArgs = {NULL, NULL, 0, 0};
arg1.vt= VT_R8;
arg1.dblVal = oper1;
arg2.vt= VT_R8;
arg2.dblVal = oper2;
vargs[0]=arg2;
vargs[1]=arg1;
dispparamsArgs.rgvarg=vargs;
dispparamsArgs.cArgs = 2;
LPOLESTR szMember = OLESTR("Add");
hr = calculator->GetIDsOfNames(IID_NULL, &szMember, 1,
LOCALE_USER_DEFAULT, &dispid);
hr = calculator->Invoke(
dispid,
IID_NULL,
LOCALE_USER_DEFAULT,
DISPATCH_METHOD,
&dispparamsArgs,
&VarResult,
NULL,
NULL
);
double result=VarResult.dblVal;
str.Format("%lf", result);
m_Result.SetWindowText(str);
calculator->Release();
}
This last snippet has some assumptions, from what was shown earlier, the use of IDispatch
for a dual interface declaration (visible on the interface attributes in C#). Other than that you can see some similarity to what happened in C# by using the VARIANT
named types instead of object
and finally the use of OLE
associated types and constants for the remaining details and method names. Notice also some special types to compensate for the absence of reflection in native C++.
Finally you need to keep in mind that there's a need to initialize and uninitialize the COM context for things to work in native C++. This should be done once for each CoInitialize
and CoUninitialize
calls in the whole application.
In the Initialization:
CSimpleCalcClientDlg::CSimpleCalcClientDlg(CWnd* pParent )
: CDialog(CSimpleCalcClientDlg::IDD, pParent)
{
CoInitialize(NULL);
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}
And the Finalization:
CSimpleCalcClientDlg::~CSimpleCalcClientDlg()
{
CoUninitialize();
}
Conclusion
As you can see, this article in comparison to the ones I show as references is but a small sample. Even so, it serves what I desired it to be, a small introduction where you don't have to read something the size of small book and you can just grab the code to test it for yourself and study it. Although the example is as simple as a small calculator there's a handful of source code generated automatically, especially when using C++ projects.
You'll also notice that I don't clear up much about many of the terms utilized, and this is done on purpose because there really are a lot of them and I needed to avoid letting the article grow to the size of a small book, even though you can easily check them out and even learn more on MSDN interoperability tutorials or also on some of the references here on codeproject (which get to be as in depth as a small book).
I hope this article will serve the goal I aimed for, which is a first mental image, a first glance for the less experienced with the code and minor architecture choices you need to be aware off when using COM interoperability.
Points of Interest
Always keep in mind the version you stamp on the DLLs when you build them, because version change implies altering all the client components and applications that use your COM component.
Strings in COM interop contexts are one of the major sources of memory leaks.
Especially important when writing your Managed COM components in Managed C++, keep in mind the need for side by side assemblies which usually end up in a folder such as %windows%\WinSxS.
Despite you've registered a Managed COM component and placed in on the GAC, you might need to place a copy of the dll itself with the client application. (so the application will know where to find it)
Make the final versions of your Managed COM components Primary Interop Assemblies (PIAs).
References
COM in plain C (The series of articles)[^]
Writing COM Clients with Late and Early Binding[^]
Understanding Classic COM Interoperability With .NET Applications[^]
Building COM Servers in .NET[^]
COM Interop Part 2: C# Server Tutorial[^]
History
November 10th, 2007 � Original Article