Four days ago, I jumped in a time machine, dialled back two millennia, and emerged in the medieval period of programming history, sometime during the reign of King COM. Now, returning, I present to you my rough notes on how you, a citizen of the brave new .Net utopia can communicate with the denizens of the dark ages.
Brace yourself
You’ll want to start by understanding the basic principles on which COM is built. There is a series of articles on CodeProject which form a good introduction. Here are links to Part 1 and Part 2. A post on StackOverflow provides an index to the remainder. Reading those articles, I am, once again, amazed at the ingenuity of the ancients!
Consuming COM in .Net
Consuming COM in .Net - well “it’s trivial” (as my old Maths lecturer used to say, having written a partial differential equation on the blackboard, looking to us expectantly for a solution). And in .Net 4.0 it has just got trivialler thanks to dynamic types and No PIA. You just right click on your project, click Add Reference…, then select the appropriate library in the COM tab. All the types will then be available to you as if they were regular .Net types
Exporting .Net to COM
Where it gets interesting is when you want to make a .Net type available to COM. Here’s what I’ve learnt.
- Create yourself a new Class library project. In the AssemblyInfo file, make sure that the ComVisible attribute is given the value false – you can then be selective in what is visible. Also, make sure that a Guid has been created.
- Start by defining an interface defining the methods that you want your object to expose to COM. Decorate this interface with the attribute ComVisible(true).
- Implement the interface in your object, and decorate the class with [ComVisible(true)] and [ClassInterface(ClassInterfaceType.None)]
- Open the project properties, go to the Build tab, and tick Register for COM interop. This will ensure that the the necessary Type Library (.tlb) file gets created in the /bin directory
- To check how your type will appear to COM, you can browse the tlb file in the Visual Studio Object Browser – but note that this will lock the tlb file, so you won’t be able to rebuild your project whilst it remains open in the browser.
- I’ve noticed in VS2010 that the tlb file sometimes doesn’t seem to get updated: a Clean and a Rebuild soon fixed that.
An example:
using System; using System.Runtime.InteropServices; namespace ComLibrary { [ComVisible(true)] public interface IMainType { int GetInt(); void StartTiming(); int StopTiming(); } [ComVisible(true)] [ClassInterface(ClassInterfaceType.None)] public class MainType : IMainType { private Stopwatch _stopWatch; public int GetInt() { return 42; } public void StartTiming() { _stopWatch = new Stopwatch(); _stopWatch.Start(); } public int StopTiming() { return (int)_stopWatch.ElapsedMilliseconds; } } }
Using the exported COM object
I last touched C++ about 15 years ago. I’ve never programmed with COM in C++. With that valuation of my advice, here is how I went about calling my COM object from C++.
- Remember to call CoInitialize(NULL) on each thread from which you call COM objects. Before the thread exits you should also call CoUninitialize()
- Copy the tlb from the /bin directory of your .Net project into your C++ project. At the top of the file where you want to call your COM object put #import “<name of your tlb file>.tlb”.
- Define a smart pointer to your type: this takes care of all the AddRef/ReleaseRef stuff which you should have read about in the introductory article I pointed you to: for example
COMLibrary::IMainTypePtr myType;
- Create an instance of your object:
myType.CreateInstance(__uuidof(COMLibrary::MainType));
- Use it:
myType->GetInt();
- During development, you might find that, having updated your tlb file, new or changed members don’t appear on the C++ side. There are two things to try here:
- In the Debug folder of your C++ project look for the two files <your typelib name>.tlh and <your typelib name>.tli and delete them. Then rebuild your project.
- If the project compiles but you get intellisense errors, try closing down the solution and deleting the intellisense cache files. These are located next to the solution file, and have extension sdf (for VS 2010) or ncb (for VS 2008 and earlier)
Here’s a fuller sample for the C++ side
#include "stdafx.h" #include <iostream> #import "ComLibrary.tlb" int _tmain(int argc, _TCHAR* argv[]) { CoInitialize(NULL); ComLibrary::IMainTypePtr myType; myType.CreateInstance(__uuidof(ComLibrary::MainType)); myType->StartTiming(); for (long long i=0; i < 1000000; i++) { myType->GetInt(); } long timeInMilliseconds = myType->StopTiming(); printf("%d", timeInMilliseconds); std::cin.get(); CoUninitialize(); return 0; }
Other notes
- If you try using your exported .Net type in VBA or VB6 and you get the error “Function or interface marked as restricted, or the function uses an Automation type not supported in Visual Basic”, then check the parameter and return types on the interfaces you are exposing. I got this error when trying to return a .Net long – a 64-bit integer. VBA can’t count that far: it’s longs are only 32-bit.
- Primitive types appear to translate fairly straight-forwardly between .Net and COM. Things start to get hairier when arrays become involved. On the COM side these become SAFEARRAYS, and look like being a right pain to deal with, somewhat mitigated by the CComSafeArray wrapper class.