Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

An ATL Component in C++ that fires COM events

0.00/5 (No votes)
14 Jun 2004 1  
A COM component that implements interprocess communication, and illustrates firing events to a COM container such as Visual Basic

Introduction

Visual Basic is a great development environment, but VC++ has the advantage of being able to produce small distributables with minimal or no dependances. This has lead me to re-write some of my VB code and learn a bit of VB/VC/COM interaction at the same time. Here I show you how I developed a COM component to deliver intra-process communication.

Background

A while back I needed to communicate between two visual basic applications and so I coded an IPC class and module that could included in a VB project. However, I have been moving into deploying purely C++ apps and so needed a similar component that didn't depend on the VB runtime DLLs and also wanted to learn some COM / C++ / VB interaction techniques as I still wanted this component to work with my original VB app I choose to achieve this with COM. This is the result.

Using the code

The basis behind the intra-process comms is using the WM_COPYDATA windows message. This technique is not new and is well described as a way to achieve intra-process comms. The code creates a hidden window at the top level for each instance of the IPC class and this hidden window is sub-classed. Upon receiving the WM_COPYDATA message the data is retrieved from the call and in my example an event is fired in the client application to say the message has been received.

In VB this is relatively easy to achieve, so long as you understand sub-classing. The challenge for me was to write a COM component in C++ that could handle and interface BSTRs and events across COM. For those coming from VB the original VB source is included as you may like to cross reference and learn.

The development environment I have used is VC++ 7 and VB6 for the test app (mainly as I haven't got round to looking at .NET and haven't figured out how to write a VB7 app without .NET, if it can be done. Answers on a postcard please!). I have assumed a familiarity with whatever IDE you favour and basic skills in adding functions / variables to a class.

That said there are a few gotchas worth mentioning.

  • Adding methods to be explosed by the control: Methods such as the SendData method need to be exposed to the client side and therefore need to be present in the IDL file. This is achieved by right clicking on the Implementation class ie IIPC and adding a method, not on the CIPC class in class view.
  • Connection points: You must compile the IDL file BEFORE setting up a connection point.

Creating the Project

Right, first off a quick description of how the project is started. As using the right wizard seems to be half the battle. We need to create a new ATL COM project with the new project wizard. I have called it InterProcessComms. In VC7 remove the attributed option and choose merge proxy/stub code. In VC6 choose the ATL COM App Wizard and again choose merge proxy/stub code. MFC or COM+ support is NOT needed.

New ATL Project

New Project in VC6

A new ATL class now needs to be added.

  • VC7 - Goto Project menu -> Add Class and choose the ATL control.
  • VC6 - Goto insert menu ->New ATL Object ... From the wizard choose the controls catergory and then Full Control.
In both choose a short name of - IPC. You then need to specify that the control uses connection points and is invisible at runtime. I also choose to create a minimal control. For VC7 the options are illustrated below.

New Atl Control in VC7

It is important that a full control is choosen as that connection point support is enabled as we are going to be implementing events.

I am not going to go step by step though the code but rather, just discuss some of the more interesting parts.

The SendData method

[id(2), helpstring("method SendData")] HRESULT SendData([in] LONG lData, 
[in] BSTR sData, [in, optional, defaultvalue(0)] LONG hWnd);

STDMETHODIMP CIPC::SendData(LONG lData, BSTR sData, LONG hWnd)
{
int c;
HWND hWnd2Send;
COPYDATASTRUCT MyCDS;       // The WM_COPYDATA sturcture

CComBSTR localStr(sData);   // Make a new local BSTR with the passed data

                            // We can now get the lenght etc of this string

    
    // Fill the copydata structure

    MyCDS.dwData = lData;                // arbitatry long data to send

    MyCDS.lpData = localStr.m_str;       // a pointer to the buffer containing 

                                         // data to send

    MyCDS.cbData = localStr.ByteLength()+1;  // the size of the buffer  

                                             // NB: + 1 for the end NULL


    // If a window to send to has been specified, then use this

    // otherwise send to all, except ourself

    if ( hWnd != 0 ) {
        hWnd2Send = (HWND) hWnd;
        ::SendMessage(hWnd2Send, WM_COPYDATA, (WPARAM) 0, 
                                  (LPARAM) (LPVOID) &MyCDS);
    }
    else {
        // Get all the windows we need to send to, this will fill and array

        // of hWnds returns the number of windows found. Note excludes self

        if ( this->EnumerateWindows() >= 1 ) {
            for(c=m_hWndArray.GetSize();c>0;c--) {
                hWnd2Send = m_hWndArray[c-1];
                ::SendMessage(hWnd2Send, WM_COPYDATA, (WPARAM) 0, 
                            (LPARAM) (LPVOID) &MyCDS);
            } // end for

        } // end inner if

    } // end else


    return S_OK;
}

The IDL definition is worth explaining. The basic structure is the same as a C declaration with additional attributes specified in square brackets. An excellent explaination is provided at the Callista website. However, I have modified the atrributes to include an optional hWnd parameter at the end. [in, optional, defaultvalue(0)] LONG hWnd. The optional paramater must have a default value supplied. From the MS documentation it seems that [in, optional, defaultvalue("Hello World")] BSTR hWnd is perfectly valid in case you want to do the same thing with strings.

COM and VB use the BSTR data type for strings. This is a pointer to a null terminated string buffer preceeded by the buffer lenght in a dword. ATL provides a class called CComBSTR that seems to work best for my purposes. It avoids MFC and has most functions for simple string manipulation. We use it here to obtain lenghts and a char * buffer that can be packed into the COPYDATA structure.

As an aside I have not yet found a good source that explictly states how to manipulate a string in C++ using COM and return it so that functions for VB can be written. Here is some code to show you how to do this:

// This appears in the IDL file

HRESULT MessWithString([in] BSTR stringIn, [out, retval] BSTR* stringOut);

// This is the function implementation

STDMETHODIMP CIPC::MessWithString(BSTR stringIn, BSTR* stringOut)
{
    // ATL macro needed to use converstion macros!

    USES_CONVERSION;

    // make a local copy using the CComBSTR class

    CComBSTR localStr(stringIn);

    // Reverse the string, as an example. Must convert to a char * for _strrev

    localStr = _strrev( COLE2T(localStr.m_str) );

    // Now to return the string, we need to use SysAllocString to allocate

    // system memory and pass that back to the com container

    *stringOut = ::SysAllocString(localStr.m_str);

    // You must still return with a S_OK from the actual function as this is

    // a COM call. The client will get stringOut as a return value as specified

    // in the IDL

    return S_OK;
}

Finding our hidden windows

The next thing to talk about is the EnumerateWindows() function called in SendData. In C++ this poses a problem. You see the windows API call EnumWindows needs to be passed a pointer to a function that it called back for each window found. But, a member of a C++ class can't be used as the callback function because they are not quite called as declared, but instead with the this pointer pre-pended to the stack. So

MemberFunction(int x)
becomes
MemberFunction(this, x)
Static member functions on the other hand are called as declared, but they don't have access to the this pointer and so can't access any member functions or variables. This problem is dealt with very nicely by two articles, the second of which provides a nice header file which wraps the Windows API call that require callbacks. I have used this header file for the EnumWindows function.
  1. Windows Procedures as Class Member Functions
  2. Use member functions for C-style callbacks and threads - a general solution

The WndProc Function

The WndProc function needed to implement subclassing posed the same problem as described above. And again we want to know what this is so that we can access member variables and methods. The solution given above using the wrapper is complicated and bulky and for my pruposes a far simplier method can be used to call the WndProc method.

You would already have seen that a static member function can be used for a callback. But it doesn't have the this pointer and so can't access member variables / functions. In this application we have one window for each class instance. Windows have a very useful property called USERDATA. Which is a long data type that can hold arbitary data and always starts out as zero. By setting this to the value of this, when a message is raised and calls the static member function we have a way for the static function to extract this info and cast a new pointer to the object ascociated with that window. I think this is best illustrated in a diagram.
WndProc Flow Diagram

Remember this only works well because we have one window per class instance. Also for this application we are only interested in the window receiving the WM_COPYDATA message. This means that we don't mind losing the initial creation messages such as WM_CREATE whilst we wait for CreateWindowEx to return. The Class Member Function article covers this instance, if you need to implement this.

The Event Fire Functions

Events in COM are implemented though something called connection points. If you followed the project startup instructions at the top of the article your class view should have a root for CIPC and IIPC. Additionally, there is _IIPCEvents. In VC6 this appears in the root of class view, in VC7 it is under InterProcessCommsLib. By right clicking on _IIPCEvents you can add methods that will become events in the final component. Once all methods have been added you must compile the IDL file. This is the source of much fustration. Without compiling the IDL there is no type library and the connection point wizard fails.

Once the IDL is compiled, you right click on the class, CIPC in our case and click Implement Connection Point. Select the _IIPCEvents and finish. This creates some new files and a function with the name Fire_[eventname] which can be called to raise events in the client application. Easy wasn't it!

The Test Application

A test application has been included. To use the exe you must have the VB6 runtimes installed and you also need to register the InterProcessComms.dll file using regsvr32.exe InterProcessComms.dll in the directory you unpacked the test app in. Also remember to open more than one instance of the app as it defaults to NOT sending messages to itself.

Further Work

The code works but there are a few ways the reader could take the project further. The messages are limited to BSTRs and longs at the moment. The control could be expanded to other data types. There is a great article descibing transport of C++ objects across DCOM that could be of use. Also it may be an idea to cache the hidden window handles, rather than find them each time a message is sent. Possibly also indroduce some component-2-component IPC to handle new instances and cache the hidden windows.

Final Comment

Much of this information is available in documentation and around the web, buts it seems to be very fragmented and there are not many real world examples. I hope to have consolidated some of this information into a useful, real world application. Most of the useful references I have used are included in the text and if I have missed any out then I appologise and will add you if you email me. This is my first article here so feedback, good or bad is appreciated.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here