Download demo project - 72 Kb
Introduction
Note: 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)
{
CoInitialize(NULL);
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();
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");
if( m_nStreamIndex >= 10 )
return CONNECT_E_ADVISELIMIT;
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();
*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();
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 to 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.
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.
Some references from MSDN Library July 2000:
- Dr. GUI and COM Events, Part 1 August 23, 1999
- Dr. GUI and COM Events, Part 2 , October 25, 1999
Last updated: 12 August 2000