Converting the CONNECT Sample to a Local Server

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

Downloads

Download demo project - 72 Kb


Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: September 10, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Modern mobile applications connect systems-of-engagement (mobile apps) with systems-of-record (traditional IT) to deliver new and innovative business value. But the lifecycle for development of mobile apps is also new and different. Emerging trends in mobile development call for faster delivery of incremental features, coupled with feedback from the users of the app "in the wild". This loop of continuous delivery and continuous feedback is …

  • Learn how cloud-based master data management (MDM) empowers your fast-paced business to get the right data to the right place in real time, so you can remain competitive and agile.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds