Create a .NET COM and use it in Unmanaged C++
[stextbox id=”info” caption=”Comment or Leave a Message”]
Please Comment or Leave a Message if this is what you are looking for or if you have any question/comment/suggestion/request.
[/stextbox]
Well, there is enough material here to write a book… or at least a bunch of chapters. I don’t like to go through pages and pages that explain the concept but don’t give a tangible example. So, because the matter here is quite boring and not exciting, I’ll go directly to the point.
The goal here is to create a component in .NET, register it as COM and use it in a nice(!) and not-so-painful way, from C++.
This said let’s start with the bullet points:
- Create a .NET project and tell it some way that the result of it will be a COM object
- Write a bunch of methods, just to have something to test
- Start a C++ project and import all that we need to use our newly created .NET COM component
- Initialise and use it in code
- Deploy it on a different machine (not the one we are using as development environment)
- Enjoy and Comment
When you first see this you could think: “Well… easy!”. Well… not exactly. There are a bunch of stuff to consider if you don’t want to bang your head on the monitor (and keyboard) because you cannot see/use your simple object.
First things first: The .NET COM project
The first things to do here is to choose you preferred Visual Studio environment and launch it. I am using VS2010 as an example.
Start a new project, select Class Library and give it a name (I named mine COMTest). Rename the default class to something sensible (such as “MyPippoClass”… or even more sensible :P).
Write some code
In your custom class (MyPippoClass) write something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
using System; namespace COMTest { public class MyPippoClass { public void DoThis() { Console.WriteLine("Done!"); } public bool DoThat(string what) { if (string.IsNullOrEmpty(what)) { Console.WriteLine("I cannot do that!"); return false; } Console.WriteLine("I did that: " + what); return true; } } } |
Now, in order to make it visible as COM we need to generate the tlb file. In order to do this, right-click on the project and select Properties. Select the “Build” tab and (down in that page) tick the checkbox “Register for COM interop”.
Now, when you build the project you will see that, beside the .dll, there is a file with the same name and extension .tlb (COMTest.tlb in my case). Not finished yet. If you want to use the classes defined in you COM object (and I bet that you want) than in the property windows of the project select the “Application” tab and click on “Assembly Information…”. In the form that will pop-up tick the “Make assembly COM-Visible” checkbox.
Once this is done, you are ready to rock.
The C++ project
Now open you VS environment and create a new C++ project. Create a simple console app and go where the main is.
Now the only (well… almost) thing to do is to import our COM object through its Type Library file (the .tlb that we generated with so much love).
Assuming that you will have the include folder settings configured properly (to point where the type library file is), add this just before the main:
1 |
#import "COMTest.tlb" named_guids raw_interfaces_only |
Finally some meat now. Go in the main and write:
1 2 |
COMTest::_MyPippoClassPtr myPippoClass; myPippoClass = new COMTest::_MyPippoClassPtr("COMTest.MyPippoClass"); |
Well, now you will be probably surpised (or frustrated) that myPippoClass does not show any method. Yes, this is the default behaviour of your COM creation. Why? Well, the reason behind this is to allow more robustness when the client will be deployed. Once this is done in fact, and you will go and change the COM component (e.g. adding more stuff), the existing client will continue to work. You are essentially forced to use late binding to interact with your COM object.
If you don’t believe the intellisense and you try to write (C++) this anyway:
1 |
myPippoClass->DoThis(); |
you will obtain an error similar to:
1 |
error C2039: 'DoThis' : is not a member of 'COMTest::_MyPippoClass' |
The only pain is that to make this work, you have to code in a way that is not very elegant and nice. You will have to query the method that you want to invoke on the instantiated COM object…
Something like this (don’t be scared):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
IUnknown *pUnk = NULL; IDispatch * pDisp = NULL; HRESULT hresult; hresult = CoCreateInstance(COMTest::CLSID_MyPippoClass, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void **)&pUnk); if (FAILED(hresult)) { printf("Error! Cannot create object: 0x%xn", hresult); return -1; } // Get the IDispatch interface hresult = pUnk->QueryInterface(IID_IDispatch, (void **)&pDisp); if (FAILED(hresult)) { printf("Error! Cannot obtain the IDispatch interface pointer: 0x%xn", hresult); pUnk->Release(); return -1; } pUnk->Release(); // Get the DISPID for the DoThat method OLECHAR* methodName = L"DoThat"; DISPID dispid; hresult = pDisp->GetIDsOfNames(IID_NULL, &methodName, 1, GetUserDefaultLCID(), &dispid); if (FAILED(hresult)) { printf("Error! GetIDsOfNames failed: 0x%xn", hresult); pDisp->Release(); return -1; } // Initialise the DoThat parameter VARIANT args[1]; VariantInit(&args[0]); args[0].vt = VT_BSTR; args[0].bstrVal = ::SysAllocString(L"Amaze me!"); DISPPARAMS params = {args, NULL, 1, 0}; // Invoke the DoThat method VARIANT result; VariantInit(&result); hresult = pDisp->Invoke(dispid, IID_NULL, GetUserDefaultLCID(), DISPATCH_METHOD, ¶ms, &result, NULL, NULL); if (FAILED(hresult)) { printf("Error! Invoke failed: 0x%xn", hresult); } else { if (result.boolVal == VARIANT_TRUE) { printf("The response was true. It did it!n"); } else { printf("The response was false. It didn't do it!n"); } } // Free the string and release the IDispatch pointer VariantClear(&args[0]); pDisp->Release(); |
Are you still there? Good!
If you don’t like it, I don’t like it. It is clear why, but what if we don’t care about this problem and we want to simplify all this?
Fortunately a solution exists, and it is pretty simple as well. Hurray!
Go back to your C# project, expand the “Properties” folder in your solution explorer and open the “AssemblyInfo.cs” file.
Just to put some order in what we will do, go where the line “[assembly: ComVisible(true)]” is (if you have “false” instead of “true” then you did something wrong in one of the initial steps. Simply double-check them or change the “false” to “true”).
Immediately underneath add this line:
1 |
[assembly: ClassInterface(ClassInterfaceType.AutoDual)] |
Note here: Types that use a dual interface allow clients to bind to a specific interface layout. As already mentioned (and this is why it is generally discouraged, despite the ugly code): any changes in a future version to the layout of the type or any base types will break COM clients that bind to the interface. But… But now our C++ side is nicer to work with.
Go to the C++ code and try to use your COM instance. Be sure that the dll (and tlb) are updated, and you will see that your instance will expose (thanks to the intellisense) all the methods and properties that we wrote.
Note that if the intellisense still doesn’t work, it could be due to intellisense itself that is somehow slow and dumb sometime (especially in C++). But if you write the following code the compiler should build without any problem.
1 2 3 4 5 6 7 8 |
VARIANT_BOOL res; COMTest::_MyPippoClassPtr myPippoClass; myPippoClass = new COMTest::_MyPippoClassPtr("COMTest.MyPippoClass"); myPippoClass->DoThis(); myPippoClass->DoThat(_bstr_t("Behave!"), &res); |
Of course this will build but will not work. We still need some extra code to allow our program to interact with COMs.
As first line in our main function add this:
1 2 3 4 5 6 7 |
HRESULT init = CoInitialize( NULL ); if (init != S_OK) { printf("Error Initialising COMn"); return 1; } printf("COM Initialised"); |
Then at the end don’t forget to add:
1 |
CoUninitialize(); |
Now it should be happy!
If you did everything correctly and you run this on your dev machine you should see this output:
1 2 3 |
COM Initialised Done! I did that: Behave! |
Isn’t it cool? Yes! Well… somehow.
Deploying it on another pc
If you try to copy this exe to another machine and the COM dll as well and you try to launch the exe you will obtain a not-so-nice (assuming that we are doing all in debug mode): “This application has requested the Runtime to terminate it in an unusual way. Please contact the application’s support team for more information.”
What’s now?! Well, you need to tell the system how to find your COM DLL. You can simply do it registering the dll (in the target machine) using the RegAsm tool provided with the .NET framework.
You can find it in:
1 |
C:WINDOWSMicrosoft.NETFrameworkv4.0.30319RegAsm.exe |
or (if you are not using the .NET 4.0):
1 |
C:WINDOWSMicrosoft.NETFrameworkv2.0.50727RegAsm.exe |
Simply run (assuming RegAsm.exe in your path and the dll in your current folder):
1 |
RegAsm.exe COMTest.dll |
The output of the registration operation should be similar to this:
1 2 3 4 |
Microsoft (R) .NET Framework Assembly Registration Utility 4.0.30319.1 Copyright (C) Microsoft Corporation 1998-2004. All rights reserved. Types registered successfully |
If you launch your C++ exe now everything should work fine :).
One or few more words. If you move your exe in a folder without your DLL you will have again the nasty crash. To solve this problem (in case you care about this) you need to run another tool (gacutil) that we will essentially use to register our DLL in the global assemblies cache.
After locating it just run it on your DLL with the /if option that will essentially force the installation of the DLL in the GAC.
Theoretically it should simply be:
1 |
gacutil.exe /if COMTest.dll |
Practically instead it depends on your framework version and what you have installed on your machine.
If you build your code using the .NET framework 4.0, then probably you will have to copy the gacutil tool from your dev pc into the other.
The fastest way (just to have it working without too many operations) is to copy the whole folder:
1 |
C:Program FilesMicrosoft SDKsWindowsv7.0AbinNETFX 4.0 Tools |
in your target machine and run the gacutil in there.
Once you do it you will see that it doesn’t work… again? Yes again. This is because, in order to do this, we need to give a strong name to our assembly.
The output of the gacutil registration will in fact be:
1 2 3 4 |
Microsoft (R) .NET Global Assembly Cache Utility. Version 4.0.30319.1 Copyright (c) Microsoft Corporation. All rights reserved. Failure adding assembly to the cache: Attempt to install an assembly without a strong name |
Well… let’s do it.
There are two ways of doing it, one semi-manual and the other more automated.
Just to explore both options let’s see both of them starting from the semi-manual one.
Open the command prompt (on your dev pc) and go where the generated COM DLL is. Then from here (just for simplicity) run the command:
1 |
sn -k test.snk |
if your sn.exe is not in the path, then use the one in the same folder of the gacutil writing something like this (if you don’t want to move folder and assuming that you have my folder structure):
1 |
"C:Program FilesMicrosoft SDKsWindowsv7.0AbinNETFX 4.0 Toolssn.exe" -k test.snk |
the result will be a file called (surprise) test.snk and the console output will be:
1 2 3 4 |
Microsoft (R) .NET Framework Strong Name Utility Version 4.0.30319.1 Copyright (c) Microsoft Corporation. All rights reserved. Key pair written to test.snk |
Well done.
Now open you C# project and go again in the property windows, “Signing” tab now. Tick the “Sign the assembly” option and from here you can select the file that we have just created (test.snk).
If you want to use the more automatic way directly instead, from here open the drop down box and select “<New…>”. In the dialog box that will pop-up remove the tick from “Protect my key file with a password” and enter a nice name (e.g. “MyComTestKeys”). Once you will press “OK” the system will automatically create a “MyComTestKeys.snk” file in your project and will give a strong name to the assembly.
If you build now and deploy the DLL and try to run the gacutil again you will succeed!
1 2 3 4 |
Microsoft (R) .NET Global Assembly Cache Utility. Version 4.0.30319.1 Copyright (c) Microsoft Corporation. All rights reserved. Assembly successfully added to the cache |
Note that you will need to unregister/register again the DLL with the RegAsm tool.
Now sit, relax, take a breath and launch again the C++ exe in its own isolated folder.
Well Done!
The journey ends… well… actually it starts here.
Please comment and leave any feedback/request about this or any other argument (it can be completely unrelated if you want). I will try to answer and explain more concepts in the future posts.
For now: Good Bye! :)
very interesting subject , great post.
I found your article very helpfull. Thanks.
could you tell me why after ticking the “Sign the assembly” and rebuilding C# project in C++ project I’m getting following error:
error C2039: ‘MyPippoClass’ : is not a member of ‘COMTest’
The problem doesn’t occur when “Sign the assembly” is unticked