An Introduction to Comet

by Paul Hollingsworth

Table of Contents

What is Comet?

Installing Comet

Hello Comet

Step 1: Create a new project

Step 2: Define an interface

Step 3: Define a CoClass implementing that interface

Step 4: Build a C++ test client

Step 5: Going further - exception handling

An excursion into the structure of Comet

Step 6: Adding Interfaces

Conclusion

Comet Tools

IDL2H.BAT

TLB2H.EXE

What is Comet?

Comet consists of

  1. a small utility (tlb2h.exe)
  2. a set of header files defining a template library

tlb2h.exe generates a header file from a type library, much like the .tlh/.tli files you get from using the #import directive. The header file contains "wrapper" classes that automatically wrap calls to COM interfaces making them much more "C++-friendly". The wrapper methods convert failed HRESULT calls to exceptions, and convert to and from C++ "wrapper" classes and the standard COM types. 

The template library contains C++ and STL-friendly wrapper classes for common COM data types such as BSTR and VARIANT. The header files generated by tlb2h.exe depend on the template library, however the converse is not true - you can use the Comet classes and objects with interfaces that are not implemented in a type library also.

Comet can be used as a replacement for ATL.

Installing Comet

  • Download the latest version of Comet from http://www.lambdasoft.dk/comet/download.htm.
  • Extract the .zip file using a utility such as WinZip into a directory of your choice. I am using "C:\" in this discussion. Be sure to check the box marked "Use folder names".

  • Close any instances of Microsoft Visual Studio that you have running. If you do not this, some of the changes made by the installation will be overwritten by Visual Studio when you do finally close it down.
  • Double click on the "install.bat" in the root directory of your Comet installation. (e.g. this is "C:\Comet\install.bat"). You can also run this from the command line if you want to see the output it generates:
C:\comet>install.bat
Copying wizard...

C:\comet>copy bin\comet.awx "C:\Program Files\Microsoft Visual Studio\Common\MSD
ev98\Template"
        1 file(s) copied.
Modifying Tools search path for Visual Studio IDE
Note: These changes will not have an effect if you have MSDEV.EXE
open. If you have the IDE currently running, close it and run this
batch file again.
BEFORE:X:\COM\Comet\Include;X:\COM\ComUtils;X:\COM\Interface\TLB;X:\COM\Interfac
e\IDL;X:\COM\Interface\Headers;X:\COM\ATLProjects\CBOEQuote0001;X:\COM\ATLProjec
ts\CBOESerialComm;C:\Program Files\Microsoft Platform SDK\include\ATL30;C:\Progr
am Files\Microsoft Platform SDK\include;C:\Program Files\Microsoft Visual Studio
\VC98\INCLUDE;C:\Program Files\Microsoft Visual Studio\VC98\MFC\INCLUDE;C:\Progr
am Files\Microsoft Visual Studio\VC98\ATL\INCLUDE;C:\Program Files\Microsoft Pla
tform SDK\src\WTL\Include;C:\Program Files\Stingray Objective Studio\Common\Comm
on 6.0\include;C:\Program Files\Stingray Objective Studio\OT60\include;C:\Progra
m Files\Stingray Objective Studio\OG70\include
AFTER:C:\comet\include;X:\COM\Comet\Include;X:\COM\ComUtils;X:\COM\Interface\TLB
;X:\COM\Interface\IDL;X:\COM\Interface\Headers;X:\COM\ATLProjects\CBOEQuote0001;
X:\COM\ATLProjects\CBOESerialComm;C:\Program Files\Microsoft Platform SDK\includ
e\ATL30;C:\Program Files\Microsoft Platform SDK\include;C:\Program Files\Microso
ft Visual Studio\VC98\INCLUDE;C:\Program Files\Microsoft Visual Studio\VC98\MFC\
INCLUDE;C:\Program Files\Microsoft Visual Studio\VC98\ATL\INCLUDE;C:\Program Fil
es\Microsoft Platform SDK\src\WTL\Include;C:\Program Files\Stingray Objective St
udio\Common\Common 6.0\include;C:\Program Files\Stingray Objective Studio\OT60\i
nclude;C:\Program Files\Stingray Objective Studio\OG70\include
BEFORE:C:\Utils;C:\Program Files\Microsoft Platform SDK\bin\winnt;C:\Program Fil
es\Microsoft Platform SDK\bin;C:\Program Files\Microsoft Visual Studio\Common\MS
Dev98\Bin;C:\Program Files\Microsoft Visual Studio\VC98\BIN;C:\Program Files\Mic
rosoft Visual Studio\Common\TOOLS;C:\Program Files\Microsoft Visual Studio\Commo
n\TOOLS\WINNT;C:\WINNT\system32;C:\WINNT;C:\WINNT\System32\Wbem;C:\Program Files
\Stingray Objective Studio\OT60\bin;C:\Program Files\Stingray Objective Studio\O
G70\bin
AFTER:C:\comet\bin;C:\Utils;C:\Program Files\Microsoft Platform SDK\bin\winnt;C:
\Program Files\Microsoft Platform SDK\bin;C:\Program Files\Microsoft Visual Stud
io\Common\MSDev98\Bin;C:\Program Files\Microsoft Visual Studio\VC98\BIN;C:\Progr
am Files\Microsoft Visual Studio\Common\TOOLS;C:\Program Files\Microsoft Visual
Studio\Common\TOOLS\WINNT;C:\WINNT\system32;C:\WINNT;C:\WINNT\System32\Wbem;C:\P
rogram Files\Stingray Objective Studio\OT60\bin;C:\Program Files\Stingray Object
ive Studio\OG70\bin
Finished install. Now open up the IDE and check the settings for
"Include Directories" and "Executable Files" under
"Tools->Options->Directories"

C:\comet>

Install.bat does two things.

  1. It installs the wizard - "comet.awx" into the appropriate directory so that Visual Studio will pick it up automatically in the "File->New->Project" dialog. This step makes use of the MSDEVDIR environment variable, which may not be defined if you did not enable command-line compilation when you installed Visual Studio. If this is the case, you will have to copy the "comet.awx" file manually.
  2. It adds "C:\Comet\Include" and "C:\Comet\Bin"  to the include and executable file paths respectively (assuming you unzipped into C:\Comet). You can see the effects of these changes when you load up Visual Studio and look under Tools->Options->Directories.

Assuming all has gone well, you will be able to use Comet as part of the IDE from now on.


Hello Comet

The rest of this article will take you through the steps to both implement a COM server using Comet, and to create and call COM servers on the client side using Comet. The sample program can also be downloaded here.

Most COM servers do not have a GUI. So instead, we'll write a COM server that pops up a message box that says "Hello" when the client asks it to. It will be a COM server housing one CoClass instance, called "CoPerson", that implements a single interface, "IPerson", with a single method "SayHello".

Step 1: Creating a new project

In order to just get started, several things need be set up as part of building your project:

  1. A DLL project needs to be created with a corresponding .DEF file that declares the exports for DllGetClassObject, DllCanUnloadNow,  DllRegisterServer and DllUnregisterServer.
  2. An IDL file has to be added to the project
  3. A custom build step has to be added for the IDL file which first runs midl.exe to produce a TLB file, and then runs tlb2h.exe on the resultant tlb file to produce an appropriate header file

  4. An often necessary step is to embed the type library as a resource in the DLL, so that the DLL can be shipped as a stand-alone unit. The type library has to be embedded in the .DLL as a resource with ID 1 and type "TYPELIBRARY".
  5. A custom build step at the end has to register the COM server

Doing this manually is somewhat tedious, so it's probably a lot more convenient, although by no means necessary, to use the wizard. Which is what I'll ask you to do in this example. The wizard should have been installed in the Templates directory of your Visual Studio installation. To activate the wizard, go to the File menu, select "New" and create a new Comet project. Call it "HelloComet":

Once the project wizard completes , you'll find it generated the following files:

HelloComet.idl: This is the .IDL file that is used to generate the type library for your DLL. If you right click on this file and select "settings", you'll notice a custom build step has been created by the wizard:

idl2h is a batch file in the "bin" directory of your Comet installation. It is used to convert an IDL file into a C++ header file - "HelloCometLib.h" - that is #included into your project. You can read more about IDL2H here.

HelloComet.def: This contains the .DEF file necessary to declare the four entry points in the DLL

HelloComet.rc: The resource script that adds the type library as a resource embedded in your DLL

std.cpp/std.h: These two files are analogous to the stdafx.cpp/stdafx.h produced by the ATL appwizard that generate a precompiled header file for faster re-compilation. The "afx" suffix is a legacy of the AFX - an unreleased predecessor to MFC. Since Comet has nothing to do with MFC the suffix was removed. std.h contains the include of HelloCometLib.h.

HelloComet.cpp: This is the initial source file generated for your DLL. Let's look at what code was automatically generated by the wizard:

#include "std.h"

using namespace comet;

typedef com_server< HELLOCOMETLib::type_library > SERVER;

COMET_DECLARE_DLL_FUNCTIONS(SERVER);

Almost everything in the Comet template library is defined in the "comet" namespace. Inside this namespace, all of the types from your IDL file are defined inside the "HELLOCOMETLib" namespace (the name just comes from whatever you called your library block). Using namespaces helps in avoiding name collision conflicts with other compiler tools such as #import and MIDL. However, it can be annoying to have to type "HELLOCOMETLib::" before everything, so generally, you'll want to add another line to your project:

using namespace comet::HELLOCOMETLib;

which saves you from having to put a HELLOCOMETLib:: prefix before all of your user defined types unless it is necessary to resolve a name conflict..

The typedef is not strictly necessary in the default circumstance (there is nothing special about the name "SERVER" either), you could remove the typedef and write instead:

COMET_DECLARE_DLL_FUNCTIONS(com_server<HELLOCOMETLib::type_library>)

However, com_server is a template class that can accept more than one argument. However, if you try to specify, for example:

COMET_DECLARE_DLL_FUNCTIONS(com_server<HELLOCOMETLib::type_library,
                                       com_server_traits<NO_EMBEDDED_TYPELIB> >)

you can run into trouble with the C pre-processor, which sees your code first. It will see this as a macro invocation with two arguments when only one is allowed, and give a compilation error. Hence the typedef is often necessary. This is also why this is the only macro you will have to deal with in Comet. We don't like macros - but sometimes they are a necessary evil.

What, exactly, does COMET_DECLARE_DLL_FUNCTIONS do? Here it is, in all its complexity: 

#define COMET_DECLARE_DLL_FUNCTIONS(SERVER) \
extern "C" BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID) \
{ \
return SERVER::DllMain(hInstance, dwReason, 0); \
} \
\
STDAPI DllCanUnloadNow() \
{ \
return SERVER::DllCanUnloadNow(); \
} \
\
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) \
{ \
return SERVER::DllGetClassObject(rclsid, riid, ppv); \
} \
\
STDAPI DllRegisterServer() \
{ \
return SERVER::DllRegisterServer(); \
} \
\
STDAPI DllUnregisterServer() \
{ \
return SERVER::DllUnregisterServer(); \
} 

As you can see, all it does is to declare the DLL entry points as wrappers on top of the static methods that are defined in the arguments you specified.

So in effect, the real entry points into the DLL, and the Comet framework, are the four static methods of the same name in whatever type you passed to the macro (in this case, com_server<HELLOCOMETLib::type_library>). The macro is merely an aid to save you typing in what is some very unsophisticated boilerplate code.

Step 2: Defining an interface

Any good COM programmer will tell you "define your interfaces before your implementations". So that's what we'll do here.

In the .IDL file, type in the definition for IPerson below:

import "oaidl.idl";
import "ocidl.idl";  

[
    uuid(4562F1E6-A609-11d4-9D47-009027133993),
    version(1.0)
]
library HELLOCOMETLib
{
    importlib("stdole2.tlb");

    [
        object,
        uuid(15E8C205-A60A-11d4-9D47-009027133993),
        oleautomation
    ]
    interface IPerson : IUnknown
    {
        HRESULT SayHello();
    };

};

Rebuild the whole project, just to check that you didn't make any errors.

Step 3: Define a CoClass implementing that interface

We've defined a Person interface, now we'll add a coclass declaration to the type library that implements that interface. We'll need to change two files. First, the IDL file:

import "oaidl.idl";
import "ocidl.idl";  

[
    uuid(4562F1E6-A609-11d4-9D47-009027133993),
    version(1.0)
]
library HELLOCOMETLib
{
    importlib("stdole2.tlb");

    [
        object,
        uuid(15E8C205-A60A-11d4-9D47-009027133993),
        oleautomation
    ]
    interface IPerson : IUnknown
    {
        HRESULT SayHello();
    };

    [
        uuid(15E8C206-A60A-11d4-9D47-009027133993)
    ]
    coclass CoPerson
    {
        [default] interface IPerson;
    };
};

Build the project again, just to make sure that there still are not any errors.

Now, you'll have to define the implementation of CoPerson. To do so, type the following into your project:

#include "std.h"

using namespace tlb2h;

typedef com_server< HELLOCOMETLib::type_library > SERVER;

COMET_DECLARE_DLL_FUNCTIONS(SERVER);

using namespace comet::HELLOCOMETLib;

class coclass_implementation<CoPerson> : public coclass<CoPerson>
{
};

Try compiling the project again and you'll come across another error:

error C2248: 'SayHello' : cannot access private member declared in class 'comet::HELLOCOMETLib::IPersonImpl<class coclass_implementation<struct comet::HELLOCOMETLib::CoPerson>,struct comet::HELLOCOMETLib::IPerson>'
c:\experiment\hellocomet\hellocometlib.h(165) : see declaration of 'SayHello'

If you double-click on that second line, it will tell you exactly what's wrong. Due to the information generated by tlb2h, it already knows that your CoClass should be implementing a method called "SayHello", which is declared "private" in a base class generated for you by tlb2h.exe, so that you don't forget to implement it! So, to keep the compiler happy, let's add an implementation:

#include "std.h"

using namespace tlb2h;

typedef com_server< HELLOCOMETLib > SERVER;

COMET_DECLARE_DLL_FUNCTIONS(SERVER);

class coclass_implementation<CoPerson> : public coclass<CoPerson>
{
public:
    void SayHello()
    {
        ::MessageBox(0, "Hello Comet!", "Comet!", MB_OK);
    }
};

That's it. It's seems like there should be more code doesn't it? Don't be worried about the lack of HRESULT, STDMETHOD or other macros or typedefs. The "grungy" part of COM is generated automatically for you by tlb2h.exe. Although it might not look like it, we're still doing COM. It just looks a lot more like C++! ;-)

At this point, give yourself a pat on the back - you've built your first COM server using Comet! But how do you know that it worked? You have lots of options. COM is somewhat language independent, so you could build a VB client, import the HELLOCOMETLib type library, and create and test your COM server with the following three lines:

Dim p As IPerson
Set p = New CoPerson
p.SayHello

However, we'll show you how to call your COM server from a C++ client.

Step 4 Build a C++ test client

For simplicity sake, we'll build a bare-bones win32 console application that also uses Comet. First, we'll need to set it up. Create a new win32 console application called "Client" belonging to the same workspace as the HelloComet workspace. Make sure there's a main .cpp file, called "main.cpp".

Here are the steps I went through to do this:

  • Right click on the "Workspace" icon and select "Add new project to workspace"
  • Select the "Win32 Console Application" icon, and type in the project name - "Client"
  • Ensure that the "Add to current workspace" radio button is checked. Do not check the "Dependency of" button
  • Click OK

  • Once the project has finished, select File->New.
  • Select the Files tab and make sure that "C++ source file" is selected
  • Type in the filename - "main.cpp"
  • Click OK
  • Go to the Project menu and select "Dependencies"
  • Make sure that the HelloComet project is listed as a dependency of Client

Once you're done, your File View tab in the workspace should look like this:

The Client project should be in a subdirectory of the HelloComet directory:

Now, in the main.cpp file, type in the following code:

#include "../HelloCometLib.h"
#include <iostream>

using namespace comet;
using namespace comet::HELLOCOMETLib;
using namespace std;

int main()
{
    try {
        auto_CoInitialize com;
        com_ptr<IPerson> person(uuidof<CoPerson>());
        person->SayHello();
    } catch(const std::exception &e) {
        cerr << "Error occurred: " << e.what() << endl;
        return 1;
    }
    return 0;
}

 Type it in and run it. It should pop up with a little message box:

Notice that we're using the same header file as was used to implement the COM server. If instead you were trying to write a client for a COM object that you did not implement using Comet, you could still generate the header file by adding the type library (or DLL) to your project and setting it up to run tlb2h.exe as a custom build step. For client use, Comet gives you the same convenience as #import.

Let's go through this example to see what it does. First, notice that everything is surrounded in a "try/catch" block for any exceptions. The client side code calls the underlying COM API and method calls, checks the return value for a failed HRESULT, and throws an exception if it fails. Here's what the implementation of SayHello looks like on the client side:

inline void IPerson::SayHello()
{
    HRESULT _hr_ = raw_SayHello();
    if (FAILED(_hr_)) throw_com_error(this, _hr_);
}

throw_com_error also checks to see if the object supports rich error information, via the ISupportErrorInfo interface if implemented. By default, all objects in Comet implement ISupportErrorInfo and convert exceptions into rich error exceptions by calling ::SetErrorInfo.

If you'd rather check the HRESULT yourself, this can be easily arranged by calling the corresponding method name with a "raw_" prefix:

HRESULT hr = person->raw_SayHello();
if(FAILED(hr))
    // handle the error

What's this next line about?

        com_ptr<IPerson> person(uuidof<CoPerson>());

 IPerson is the interface definition generated by tlb2h.exe when we built the HelloComet project. com_ptr<> is a smart pointer, like ATL's CComPtr - that automatically handles calling AddRef() and Release() for you.

Now we look at the argument passed in - uuidof<CoPerson>(). The syntax looks a bit odd, but we're actually calling a template function which is defined in <comet/interface.h>:

//! C++ replacement of VC's __uuidof()
/*!
    Use this function to an IID to an interface or coclass.
*/
template<typename Itf> inline const IID& uuidof(Itf * = 0) throw()
{
    return comtype<Itf>::uuid();
}

Because we're not supplying an argument, we explicitly specify which instantiation to call. To what purpose, then?

Essentially, Visual C++ version 5.0 and above have a compiler specific extension - the __uuidof() - operator. It allows you to arbitrarily associate UUIDs with C++ types - which is an extremely useful feature when you are trying to combine C++ programming with COM. You associate UUIDs with types by using the __declspec(uuid()) attribute, and at some other part of the source code, you can "retrieve" the UUID of a type by using the __uuidof() operator. The only problem with this is that it is not portable. Once you start using it, you won't be able to compile your COM program with a different compiler.

However, there's actually a perfectly portable way of associating information with types - template specialization. The Comet way to associate a UUID with a type is to declare a specialization of the comet::comtype<> type. It is the Comet alternative to __declspec(uuid()). This specialization is generated automatically for you by tlb2h.exe in "..\HelloCometLib.h":

template<> struct comtype<CoPerson> {
    static const IID& uuid() {
        static const IID iid = { 0x15E8C206, 0xA60A, 0x11D4, 
                                 { 0x9D, 0x47, 0x00, 0x90, 0x27, 0x13, 0x39, 0x93 } };
        return iid;
    }
    typedef nil base;
};

In this case, the "IID" is a bit of a misnomer - we're actually associating the CoPerson CLSID with the type "CoPerson".

The comet::uuidof() function is the Comet way (and the portable way) of retrieving the UUID of a type. If you have a pointer to the actual type (for example, if you have an interface pointer), you can supply that pointer as an argument:

    IID IID_IPerson = uuidof(person.in());

Otherwise, you can explicitly specify the type, and leave out the default parameter:

    CLSID CLSID_Person = uuidof<CoPerson>();

So by specializing comtype, and using the uuidof template, you can generally avoid the use of the __uuidof operator, and again write portable code.

So, we're initializing an instance of com_ptr<IPerson> with the UUID of the CoPerson type. The constructor of com_ptr interprets this UUID as a CLSID, which it passes to a call to the COM API function ::CoCreateInstance. It then assigns ownership of the resultant IPerson interface pointer to itself. So this one line creates an instance of CoPerson and assigns it to the "person" variable.

What about the auto_ComInitialize class? This is a helper class that calls ::CoInitialize in its constructor, throws an exception if it fails, and calls ::CoUninitialize in its destructor when it leaves the current scope. This is an example of using the "resource acquisition is initialization" technique to ensure that ::CoUninitialize is called irrespective of how the scope is exited. You can also optionally supply an apartment type (e.g. COINIT_MULITHREADED) that is passed into the call to ::CoInitializeEx, if you wish to initialize the current thread into something other than the main Single Threaded Apartment.

Step 5 - Going further - exception handling

The client code above surrounds everything in a try/catch block to catch exceptions - but how do we know it works? Let's change the HelloComet coclass so that the implementation of the SayHello method actually throws a runtime error. Change the HelloComet.cpp file so that it looks like this:

class coclass_implementation<CoPerson> : public coclass<CoPerson>
{
public:
    void SayHello()
    {
        ::MessageBox(0, "Hello Comet!", "Comet!", MB_OK);
        throw std::runtime_error("I'm an exception");
    }
};

When you run it again - you should see the client fail - and print out the error that occurred. Behind the scenes, you might want to step through this code in the debugger to see what's going on. Essentially, the wrapper method on the server side surrounds the call to SayHello with some try/catch blocks:

template<typename _B, typename _S>
STDMETHODIMP IPersonImpl<_B, _S>::raw_SayHello()
{
    try {
        static_cast<_B*>(this)->SayHello();
    } catch (com_error& err) {
        return impl::return_com_error(err);
    } catch (const std::exception& err) {
        return impl::return_com_error(err);
    } catch (...) {
        return E_FAIL;
    }
    return S_OK;
}

The impl::return_com_error function takes the object that was thrown (which must be derived off std::exception) and converts it into a call to ::SetErrorInfo - the COM mechanism for transferring exceptions.

Incidentally, you might be wondering how IPersonImpl ends up being tied to your class anyway. This would probably be a good point to digress and give an overview of what exactly it is that tlb2h.exe generates, and the overall structure of Comet.

An Excursion into the Structure of Comet

Before I get started, I should mention two things:

  1. I have to stop myself from giving a tutorial on the amazing miracles that can be done with C++ templates. Comet uses most of them. If you're not really familiar with C++ templates, or some of the things that can be done with them, you'd first better look at http://extreme.indiana.edu/~tveldhui/papers/Template-Metaprograms/meta-art.html.
  2. If you're not interested in how it all works just yet, feel free to skip to step 6 - this part is not crucial to "completing" the tutorial.

Remember that the implementation of a DLL starts with its entry points, which we defined using the COMET_DECLARE_DLL_FUNCTIONS macro:

COMET_DECLARE_DLL_FUNCTIONS(SERVER)

This simply implemented the five entry points into the DLL by forwarding them on to static methods of the same name on the SERVER type.

SERVER was just a typedef for com_server< HELLOCOMETLib::type_library >. The com_server template defines the static methods DllMain, DllRegisterServer, DllUnregisterServer, DllCanUnloadNow and DllRegisterClassObject.  How it implements these five DLL functions is further parameterized by the argument that is passed in.

As such, the com_server template implements a framework of common code that is parameterized by this type_library type.

Essentially, TLB2H.EXE generates a definition of type_library in the comet::HELLOCOMETLib that describes everything of importance in the type library, using C++ templates.

The com_server<> template uses this information to generate an implementation of the five Dll export functions mentioned earlier. With words like "iterates" and "generates", it sounds like I'm describing a running program - when in actual fact I'm describing the compilation of your program. 

Therefore, when following what is generated from tlb2h.exe, we look at HELLOCOMETLib::type_library first - the important stuff is reachable from there.

So let's look at the definition of "type_library" in the HELLOCOMETLib namespace. You'll find this in HelloCometLib.h, the file that was generated by tlb2h.exe:

// VC workaround. (sigh!)
struct HELLOCOMETLib_type_library;
typedef HELLOCOMETLib_type_library type_library;
struct HELLOCOMETLib_type_library {
    enum { major_version = 1, minor_version = 0 };
    typedef make_list<CoPerson> coclasses;
};

Almost immediately we've run into our first Visual C++ workaround! Even though ::comet::HELLOCOMETLib::type_library is a distinct and unambiguous type, there were certain situations where the Visual C++ compiler did not think so (this is just one of many bugs that were found in Visual C++ while developing Comet - but that's probably not news to most of you ;-) ). Hence, the "real" type that is defined is ::comet::HELLOCOMETLib::HELLOCOMETLib_type_library, and "type_library" is just present as a typedef. 

Easy stuff first - major_version and minor_version. This allows a generic template to access the major and minor version number declared in the type library. This information is used to provide an automatic implementation of IProvideClassInfo for any coclasses that you implement.

make_list is at the heart of how the Comet library works. In essence, it describes a "linked list of types". In C I might define a linked list using the following structure:

struct node
{
    void *head_;
    node *tail_;
};

I can iterate through the list with code like the following:

void for_all_nodes(node *list)
{
    if(list)
    {
        do_something(list->head_);
        for_all_nodes(list->tail_);
    }
}

make_list declares a list of types for you. make_list<int,float,double,char> generates a type for which

  • make_list<int,float,double,char>::head is a typedef for "int"
  • make_list<int,float,double,char>::tail::head is a typedef for "float"
  • make_list<int,float,double,char>::tail::tail::head is a typedef for "double"
  • make_list<int,float,double,char>::tail::tail::tail::head is a typedef for "char"
  • make_list<int,float,double,char>::tail::tail::tail::tail is a typedef for a special signature type - "comet::nil".

The utility of this is that I can declare a compile-time template meta-program that does something for each type in the list, in a similar fashion to the recursive for_all_nodes function mentioned above. For example, here is a template meta program that adds a "run" static method which prints out the sizes of each of its arguments:

// General case
template<typename LIST>
struct print_sizes
{
    static void run()
    {
        cout << sizeof(LIST::head) << endl;
        print_sizes<LIST::tail>::run();
    }
};

// Termination of recursion
template<>
struct print_sizes<comet::nil>
{
    static void run()
    {
    }
};

If I put the following line of code into a program:

print_sizes<make_list<int, float, double, char> >::run();

This will print out

4
4
8
1

But notice that it doesn't actually generate a "loop". Instead, the compiler itself goes into a loop, which generates the following code:

cout << sizeof(int) << endl;
cout << sizeof(float) << endl;
cout << sizeof(double) << endl;
cout << sizeof(char) << endl;

Note also that print_sizes<make_list<> >::run() would generate no code at all. In a sense, templates are a double-edged sword. Used properly, they generate highly efficient and smaller code than would otherwise be generated. Because you are explicitly telling the compiler information at compile time, it is able to better determine code that will never execute, and therefore exclude it from the resultant executable. On the other hand, because you are generating code, sometimes it is easy to accidentally generate lots of code where an simple loop would actually be smaller.

The advantage of using a type list above is that you can also write template-meta programs to manipulate and operate off of the list. For example, there's an "append" template which is defined such that

  • append<make_list<int> >::with<make_list<float> >::head is a typedef for "int"
  • append<make_list<int> >::with<make_list<float> >::tail::head is a typedef for "float"
  • append<make_list<int> >::with<make_list<float> >::tail::tail is a typedef for "comet::nil"

That is, you can use the append template to concatenate together two arbitrary lists. And of course, you could write others to manipulate them however you want. It has been shown that the compilation of a C++ program with templates is Turing complete. That is, you could write a template meta program which can, in theory, execute any algorithm that a Turing machine can (and therefore, any algorithm at all!). This also means that it is impossible to write a program which will be able to determine if any arbitrary C++ program will stop compiling.

One of the most important ways in which Comet uses the make_list template is the implement_qi<> temnplate. It takes as an argument a "template" list to specify the set of interfaces which should be implemented. For example, I can simultaneously derive off of a set of interfaces, and inherit an appropriate implementation of QueryInterface, with the following:

class dog : public implement_qi<make_list<IDog, IAnimal> >
{
public:
    // No QueryInterface method - implemented by implement_qi
    // Still need to implement reference counting though
    STDMETHOD_(ULONG, AddRef)();
    STDMETHOD_(ULONG, Release)();
    // IDog methods
    STDMETHOD(raw_Bark)();
    // IAnimal methods
    STDMETHOD(raw_Move)();
};

The implement_qi<> implementation of QueryInterface simply iterates through the "heads" and "tails" of the "list", comparing the IIDs of each interface by using the uuidof<> template discussed in Step 4. If it finds a match, it upcasts your object to that "type".

The make_list template here is used to declare a list of coclasses defined in the type library. In this case, there is only one (CoPerson):

    typedef make_list<CoPerson> coclasses;

::comet::HELLOCOMETLib::type_library::coclasses is a typedef for the list of all "CoClass" instances defined in this type library. The com_server<> template uses the list to provide the implementations of DllGetClassObject, DllRegisterServer and DllUnregisterServer. For example, the com_server<> template definition of DllGetClassObject iterates through each coclass in the coclasses list and compares the CLSID associated with that coclass, once again using the uuidof<> template discussed in Step 4. 

When it finds a match, it returns an implementation of IClassFactory that creates an instance of the appropriate coclass_implementation<> template. You specified how that coclass should be implemented by declaring a specialization of the coclass_implementation<> template:

class coclass_implementation<CoPerson> : public coclass<CoPerson>

And this, then, is how your implementation of the CoPerson coclass gets "slotted" into the Comet framework. 

So, we're getting close to the answer to my question. You can implement "coclass_implementation" however you like. All you have to declare is the programmatic ID, the threading model, and then implement the actual interfaces. For example, here's how you could start implementing CoPerson without using any further help from HELLOCOMETLib::type_library:

class coclass_implementation<CoPerson> : public implement_qi<make_list<IPerson> >
{
public:

    // Information required for registration
    enum { thread_model = thread_model::Apartment };
    static const char* get_progid() { return 0; } // No ProgId

    // Reference counting
    STDMETHOD_(ULONG, AddRef)();
    STDMETHOD_(ULONG, Release)();

    // IPerson
    STDMETHOD(raw_SayHello)();
};

Of course, implementing AddRef() and Release() can be a bit tricky - we have to handle deleting the object when the last reference disappears, as well as to tying it into com_server<>'s implementation of DllCanUnloadNow:

    HRESULT com_server<TYPELIB, TRAITS>::DllCanUnloadNow()
    {
        return comet::module::rc() == 0 ? S_OK : S_FALSE;
    }

We can solve all of our lifetime issues by deriving off of a class specifically designed for it - simple_object:

class coclass_implementation<CoPerson> : public simple_object<IPerson>
{
public:
    // Information required for registration
    enum { thread_model = thread_model::Apartment };
    static const char* get_progid() { return 0; } // No ProgId

    // IPerson
    STDMETHOD(raw_SayHello)();
};

This provides us with a complete implementation of IUnknown. simple_object implements QueryInterface by using implement_qi<> and make_list<>, and adds in an implementation of AddRef() and Release() that deletes the object when the last reference is released, as well as maintaining the server's lock count (stored in comet::module).

Incidentally, simple_object is actually just a wrapper for simple_object_ - the more general template (note the extra underscore at the end). Keeping things as "lists" of types allows you to do clever things like adding extra types to the end. For exampl, the simple_object_ template uses the append<> template discussed above to automatically add an "ISupportErrorInfo" wrapper to the list of interfaces that your object supports:

template<typename T> class ATL_NO_VTABLE simple_object_ : 
        public implement_qi< 
            append<T>::with<make_list<impl::interface_wrapper<ISupportErrorInfo> > >
            > 

You have to implement this "signature" interface so that clients will check for exceptions that you set using ::SetErrorInfo. Hang on a minute - how do I go about doing this? You'll recall that in Step 5, I was able to throw a descriptive error message just by using the "throw" statement:

        throw std::runtime_error("I'm an exception");

This would be illegal if I did this inside the raw_Hello() method - COM methods are not allowed to throw C++ exceptions. 

I can always call ::SetErrorInfo myself inside the raw_SayHello method - but here is where the information generated by tlb2h.exe becomes really useful - the xxxImpl classes generated automatically by tlb2h.exe. You derive off the IPersonImpl class to provide an implementation of raw_SayHello. It forwards the call to your own method which is allowed to throw exceptions:

class coclass_implementation<CoPerson> :
    public simple_object<IPersonImpl<coclass_implementation<CoPerson> > >
{
public:
    // Information required for registration
    enum { thread_model_ = thread_model::Apartment };
    static const char* get_progid() { return 0; } // No ProgId

    // IPerson
    void SayHello();
};

That whole "simple_object" phrase is a lot of repetitive typing. We can clean it up somewhat by first looking at what the simple_object template evaluates to. If I expand simple_object to the "list" version, it is equivalent to:

class coclass_implementation<CoPerson> :
    public simple_object_<make_list<IPersonImpl<coclass_implementation<CoPerson> > > >

It just so happens that tlb2h.exe generates a typedef for that long, convoluted "make_list<IPersonImpl<coclass_implementation<CoPerson> > >" expression - CoPerson::interface_impls:

struct DECLSPEC_UUID("15E8C206-A60A-11D4-9D47-009027133993") CoPerson {
    typedef make_list<IPerson> interfaces;
    typedef make_list<IPersonImpl< coclass_implementation<CoPerson> > > interface_impls;
    typedef HELLOCOMETLib type_library;
    static const char* name() { return "CoPerson"; }
    enum { major_version = 0, minor_version = 0 };
    static com_ptr<IPerson> create() {
        return com_ptr<IPerson>(uuidof<CoPerson>());
    };
};

So now I can write something a lot simpler:

class coclass_implementation<CoPerson> :
    public simple_object_<CoPerson::interface_impls>

If you look at the definition of the coclass<> template, that we derived off originally, you'll see that it's not very different from what we've arrived at above:

template<typename T, enum thread_model::thread_model_t TM = thread_model::Apartment> struct ATL_NO_VTABLE coclass :
    public simple_object_< append<T::interface_impls>::with<make_list<IProvideClassInfoImpl<T> > > > {
        enum { thread_model = TM };
        static const TCHAR* get_progid() { return 0; }
    };

 The additional features are:

  • It adds a default implementation of IProvideClassInfo that uses the coclass type information generated by tlb2h.exe
  • It declares default values for the threading_model and prog_id

Well, this was just scratching the surface. However, hopefully it went some way into how you can combine the different template classes in Comet to suit your needs, and how it is that you're able to write a complete COM server with error support, registration and other goodies in about 20 lines of code.

Step 6 - Adding interfaces

Of course, that SayHello method is very boring, don't you think? Let's allow the client to choose the "Greeting" message that is displayed when "SayHello" is called (OK - I admit it - it's a bit contrived and not that much more interesting ;-) ). The question is, how do we do this?

We could just add the "SetGreeting" method to the end of the IPerson interface, which would be acceptable if you were rapidly developing this thing. However, if we had already released IPerson implementations to users, you could end up with the situation of having a newer client calling into an older server and crashing as it tried to call a method off the end of the interface. This is not the COM way!

The COM way to solve this versioning problem is to utilize the ability of objects to support more than one interface. We'll add a new interface to the type library, and ensure that new CoPerson objects implement both interfaces. Newer clients can query the object to see if it supports the newer interface, older clients don't need to know anything - they'll just query for the IPerson interface like they always have.

Let's add an "IGreetingSettings" interface, that allows the client to change the greeting used. Edit HelloComet.idl as follows:

    [
        object,
        uuid(B8E81E53-AB79-11d4-9D47-009027133993),
        oleautomation
    ]
    interface IGreetingSettings : IUnknown
    {
        HRESULT SetGreeting([in] BSTR message);
    };

    [
        uuid(15E8C206-A60A-11d4-9D47-009027133993)
    ]
    coclass CoPerson
    {
        [default] interface IPerson;
        interface IGreetingSettings;
    };

We've changed the IDL file- try compiling it and you'll get the familiar C2248 error message:

c:\experiment\hellocomet\hellocometlib.h(224) : error C2248: 'SetGreeting' : cannot access private member declared in class 'comet::HELLOCOMETLib::IGreetingSettingsImpl<class coclass_implementation<struct comet::HELLOCOMETLib::CoPerson>,struct comet::HELLOCOMETLib::IGreetingSettings>'
c:\experiment\hellocomet\hellocometlib.h(199) : see declaration of 'SetGreeting'

which is telling you that you need to now provide an implementation of "SetGreeting". Here's what my implementation of the CoPerson coclass looks like now:

class coclass_implementation<CoPerson> : public coclass<CoPerson>
{
    std::string greeting_;
public:
    coclass_implementation<CoPerson>()
        : greeting_("Hello Comet!")
    {
    }

// IPerson
    void SayHello()
    {
        ::MessageBox(0, greeting_.c_str(), "Comet!", MB_OK);
    }
// IGreetingSettings
    void SetGreeting(const bstr_t &greeting)
    {
        greeting_ = greeting.s_str();
    }
};

First, allow me to introduce the bstr_t type. comet::bstr_t is a C++ wrapper class that encapsulates an oleautomation "BSTR" type.

One of the methods it provides is a conversion to the Standard C++ string type - just call the "s_str()" method. In my case, I want to call MessageBox, which expects a pointer to a C-style string. bstr_t can't give me this directly, but it will allow me to convert it to a std::string, which I can convert to a C-style string by calling the c_str() method.

Anyway, if you build and run the test client now, you'll find that everything works as before. This, of course, was the point - our new interface and functionality will not disrupt any old clients out there which haven't been upgraded.

Now, let's write a new client, that could work just as well with an older server. Add the following lines to main.cpp:

int main()
{
    try {
        auto_CoInitialize com;
        com_ptr<IPerson> person(uuidof<CoPerson>());
        if(com_ptr<IGreetingSettings> greeting = com_cast(person))
            greeting->SetGreeting("Whaazzuupp!");
        person->SayHello();
    } catch(const std::exception &e) {
        cerr << "Error occurred: " << e.what() << endl;
        return 1;
    }
    return 0;
}

Build and run the test client and you'll find it now displays the new greeting. If you'd just written

com_ptr<IGreetingSettings> greeting = person;

You'd get a compile time-error - 

c:\comet\include\comet\ptr.h(190) : error C2440: '=' : cannot convert from 'struct comet::HELLOCOMETLib::IPerson *' to 'struct comet::HELLOCOMETLib::IGreetingSettings *'
Types pointed to are unrelated; conversion requires reinterpret_cast, C-style cast or function-style cast

This is by design.

Generally, assigning pointers of different types is an operation that can fail. So you have to tell the compiler that "you know what you're doing" - you're deliberately Querying the person object to see if it implements "IGreetingSettings" - if it fails, the program still continues working correctly - it is just running against an older server.

The mechanism for doing this in Comet is the "com_cast" template function. It behaves very similarly to the C++ dynamic_cast<> operator. Under the covers, it's just calling IPerson::QueryInterface(IID_IGreetingSettings). In fact, readers of the first chapter of "Essential COM" will be familiar with this concept. You can read more about the inspiration for com_cast, and Comet in general, by checking out this article: http://www.microsoft.com/msj/defaultframe.asp?page=/msj/0798/com0798.htm

In our case, if we were running this client against an older server, the cast would fail. In order for the client to handle this gracefully, we always write code that plans for the cast not succeeding.

Let's simulate what would happen if we were running this against an older server. Comment out the support for the "IGreetingSettings" interface in the IDL file:

    [
        uuid(15E8C206-A60A-11d4-9D47-009027133993)
    ]
    coclass CoPerson
    {
        [default] interface IPerson;
        //interface IGreetingSettings;
    };

Now run the client again. This time, the "com_cast" fails and returns a null pointer. The client skips the attempt to call "SetGreeting" and defaults to the previous behaviour. And that, folks, is how you robustly handle the versioning problem: Newer clients robustly work with older servers, newer servers robustly work with older clients.

Conclusion

Well, I hope this article has shown you how easy and elegantly you can write COM servers and clients using Comet. I encourage you to experiment with the example program and peruse the header files - there are a lot of useful classes there.

Hopefully, Comet will allow you to spend more time on your COM designs and interfaces, and less time on "grunge".

Comet Tools

IDL2H.BAT

IDL2H.BAT is a batch file in the bin directory of your Comet installation:

set idlfile=%1.idl
if NOT "%2"=="" set idlfile=%2
midl /nologo /Oicf /D "_DEBUG" /win32 /tlb %1.tlb %idlfile% 
if ERRORLEVEL 0 goto ok
exit /b -1
:ok
tlb2h %1.tlb

It first runs MIDL on the IDL file in order to generate a type library file. It then runs "tlb2h.exe" on the generated .tlb file to produce a C++ header file.

The full usage of idl2h is:

idl2h type_library [IDL filepath]

The second parameter is optional, and just defaults to the type library name with a ".IDL" extension. The ".tlb" extension is added automatically to the type library name. 

For example, to generate a type library file called "MyTypeLibrary.tlb" from an IDL file called "..\MyIDLFile.idl", you would put:

idl2h MyTypeLibrary ..\MyIDLFile.idl

You'll often need to specify the second parameter explicitly if the IDL file is not in your INCLUDE_PATH.

When using idl2h as part of a custom build step, the following is almost guaranteed to do the right thing:

idl2h $(InputName) $(InputPath)

TLB2H.EXE

TLB2H.EXE is a command line executable that reads a type library (or any file that contains a type library embedded as a resource) and generates a C++ header file suitable for use with the Comet library.

The full usage of tlb2h.exe is:

tlb2h [-c|-n] type_library

It opens the type library by using "LoadTypeLib" and then generates a C++ header file, in a similar fashion to the way that #import generates ".tlh" and ".tli" files. The output filename is determined by the name of the library block. For example, if you had the following in your type library:

[
    uuid(0EDC1544-D8FA-4713-B055-F7C58D8CB66E),
    version(1.0)
]
library HELLOCOMETLib
{

Then tlb2h.exe will generate a C++ header file named "HELLOCOMETLib.h".

"-n" turns on the generation of "Nutshell" implementation classes. These are classes which automatically forward the implementation of an interface to another interface which you can change dynamically. As such, they are a way of doing "delegation" or "forwarding" without you having to tediously type in all of the forwarding methods.

"-c" turns on "Client mode". This turns off generation of server-side code and the "Impl" classes. If you aren't using these classes, you get faster compile times with "-c" turned on.