My first version of this article had some major problems on NT and W2K as has been pointed out. The following is an
attempt to correct these errors using information that was either overlooked or not available 2 years ago. As a more
recent MSDN Dr. GUI article has pointed out, there are big problems in passing interface pointers between threads.
I was hoping the Dr. would cover the problem specifically for the case of firing events from new threads, and this is
mentioned in the article, but is left as an advanced exercise for the reader. I took some the hints for
this from the article to get my new sample project working.
The CONNECT sample included with Microsoft Visual C++ 6.0 is an example of how to use connection points with ATL.
The in-process server is implemented in connect.dll and the one of the clients is a simple dialog based application
called MDrive. It’s intended to be an example of using connection points within a single process boundary. However,
the first thing you may want to do with connection points is use them between different processes. I couldn’t find an
example of how to do this so I had to improvise.
My real goal was to create a standalone temperature monitoring program with a user interface that would also send
temperature updates to client applications using a connection point event interface. Its debatable whether or not to use polling
or asynchronous event messages in this case, and I chose events. I did not base my final project directly
on this sample however, it’s a lot easier to use the ATL wizards in VC6 and create a new project, and the code
you create will be more up-to-date with current ATL coding conventions. What follows is just a simple example of how
to convert the CONNECT sample to a local server.
First I converted the in-process server DLL to a server EXE. The fastest way to do this is to create a new
application using the ATL COM AppWizard. I called the new application “Conexe” to differentiate it from the original
project. The boilerplate code in conexe.cpp for the new app is ready to use without modification. Retain the use of CoInitialize
in _tWinMain rather than CoInitializeEx.
Then I used the ClassView right click menu to create a new interface called IRandexe. I then copied the IDL interface
related lines over from the IRandom interface in CONNECT. Finally, I just copied all the functions in the original
Random.cpp and definitions in Random.h to complete the new interface. The result is a new interface that works just
like IRandom, but with a new name and IID.
Now for the really interesting parts. I tried quite a few threading designs in creating this project and this is the
only one that seems to work properly. In the local server version I had to add a call to CoInitialize in the
RandomSession thread. So each thread that’s created via a client request will get it’s own private single threaded apartment.
DWORD WINAPI RandomSessionThreadEntry(void* pv)
{
// Need to call CoInitialize on this thread to create a single
// threaded apartment. If you don’t do this you will get the
// “CoInitialize has not been called.” error.CoInitialize(NULL); // new line
CRandexe::RandomSessionData* pS = (CRandexe::RandomSessionData*)pv;
CRandexe* p = pS->pRandom;while (WaitForSingleObject(pS->m_hEvent, 0) != WAIT_OBJECT_0)
p->Fire(pS->m_nID);CoUninitialize(); // new line
return 0;
}Advise could be a good place to do the initial interface marshalling as the Dr. GUI article suggests, since this is where the m_vec
array of event interfaces is grown. This is my override for IConnectionPointImpl::Advise, I just started with the code right out
of the ATL source. For purposes of this demo program I used a fixed array of stream pointers, but you should alter the code
using a collection type of your choice. If there have been 10 Advise calls, I just arbitrarily return CONNECT_E_ADVISELIMIT
so it will fail.HRESULT CRandexe::Advise( IUnknown *pUnkSink, DWORD *pdwCookie ) { ATLTRACE("RANDEXE: CRandexe::Advise entry\n"); // Limit the number of advises in this test program. if( m_nStreamIndex >= 10 ) return CONNECT_E_ADVISELIMIT; //T* pT = static_cast(this); IUnknown* p; HRESULT hRes = S_OK; if (pUnkSink == NULL || pdwCookie == NULL) return E_POINTER; IID iid; GetConnectionInterface(&iid); hRes = pUnkSink->QueryInterface(iid, (void**)&p); if (SUCCEEDED(hRes)) { Lock(); //pT->Lock(); *pdwCookie = m_vec.Add(p); hRes = (*pdwCookie != NULL) ? S_OK : CONNECT_E_ADVISELIMIT; ATLTRACE("RANDEXE: CRandexe::Advise: cookie = %ld\n", *pdwCookie ); HRESULT hr = CoMarshalInterThreadInterfaceInStream(IID_IRandexeEvent, p, &m_StreamArray[m_nStreamIndex]); ErrorUI(hr, "CoMarshalInterThreadInterfaceInStream error."); m_nStreamIndex++; Unlock(); //pT->Unlock(); if (hRes != S_OK) p->Release(); } else if (hRes == E_NOINTERFACE) hRes = CONNECT_E_CANNOTCONNECT; if (FAILED(hRes)) *pdwCookie = 0; ATLTRACE("RANDEXE: CRandexe::Advise exit\n"); return hRes; }This is the implementation for the Fire function. Using the CRandexe member array m_StreamArray, you can just loop through the array
and call CoUnMarshallInterface on each one. An effect of the unmarshalling seems to be the repositioning
of the stream pointer, so to fix this I clone the streams before unmarshalling. You may also get this to
work by repostioning the stream pointer back the the beginning. I was able to streamline the fire function quite a bit using the
member array. I left all the debugging code I was using intact, and you can take it that out if you like.// broadcast to all the objects
HRESULT CRandexe::Fire(long nID)
{
Lock();HRESULT hr = S_OK;
for( int i = 0; i < m_nStreamIndex; i++ )
{
CComPtr<IStream> pStream;
hr = m_StreamArray[i]->Clone( &pStream );IRandexeEvent *pI;
hr = CoUnmarshalInterface(pStream,
IID_IRandexeEvent,
(void**)&pI );if(FAILED(hr))
{
void *pMsgBuf;
::FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER
| FORMAT_MESSAGE_FROM_SYSTEM,
NULL,
hr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT ),
(LPSTR)&pMsgBuf,
0,
NULL );ATLTRACE("RANDEXE: Windows error 0x%lx, %s\n",
(DWORD)hr, (LPSTR)pMsgBuf );LocalFree(pMsgBuf);
}hr = pI->Fire(nID);
}Unlock();
return hr;
}The client MDrive project was simply copied over to a new subdirectory and only modified slightly to
use the new server. Multiple instances of MDrive can be launched and they all have access to the
Conexe.exe local server. One thing to note is that the local server version is a lot slower
as seen by the pixel drawing rate in MDrive. I haven't put a lot of thought into the interactions with
multiple clients, and my testing period was all of 5 minutes on Win98se and Win2K, but so far it seems to
get the job done. There are probably a few bugs that will show up -- and you may think about the effect of
making m_StreamArray and m_nStreamIndex static members -- I didn't try it but would like to. A program with more
elaborate functionality will of course require a lot more effort.References
-
Dr. GUI and COM Events, Part 1
- August 23, 1999 -
Dr. GUI and COM Events, Part 2
- October 25, 1999