ATL COM: Access Event Methods From Other Thread

2019-07-09 02:53发布

I'm implementing a COM interface to an existing VS2010 C++ MFC-application. Most parts of the COM interface interaction works great, but I am confused of how to trigger COM events from another thread that the one where the COM interface is running / defined. The application is multi-threaded with one main thread running the COM interface and handling GUI changes (thread 1) and one thread to receive incoming messages from a C-library (thread 2).

For certain messages received in thread 2 I want to notify the COM clients by sending a COM event. I have read many threads (Firing COM Event From Another Thread is one of them) and CoMarshalInterThreadInterfaceInStream / CoGetInterfaceAndReleaseStream is mentioned. Using Google I can't seem to find any usage of these methods that makes any sense to me; I just don't understand how to implement these functions and if they really will help me.

Relevant code parts:

TestCOM.idl: (the interface definition)

 interface ITestCOM: IDispatch
{
    [id(1), helpstring("method Test")] HRESULT Test();
};

dispinterface _ITestCOMEvents
{
    properties:
    methods:
        [id(1), helpstring("event ExecutionOver")] HRESULT TestEvent();
};

coclass TestAppCOM
{
    [default] interface ITestCOM;
    [default, source] dispinterface _ITestCOMEvents;
};

ITestCOMEvents_CP.h (VS generated class for Connection Points / events)

template<class T>
class CProxy_ITestCOMEvents : 
   public ATL::IConnectionPointImpl<T, &__uuidof(_ITestCOMEvents)>
{
public:
    HRESULT Fire_TestEvent()
    {
       HRESULT hr = S_OK;
       T * pThis = static_cast<T *>(this);
       int cConnections = m_vec.GetSize();
       for (int iConnection = 0; iConnection < cConnections; iConnection++)
       {
          pThis->Lock();
          CComPtr<IUnknown> punkConnection = m_vec.GetAt(iConnection);
          pThis->Unlock();
...

TestCOM.h (class implementing the methods and CProxy_ITestCOMEvents class)

class ATL_NO_VTABLE CTestCOM :
   public CComObjectRootEx<CComMultiThreadModel>,
   public CComCoClass<CTestCOM, &CLSID_TestCOM>,
   public IConnectionPointContainerImpl<CTestCOM>,
   public CProxy_ITestCOMEvents<CTestCOM>,
   public IDispatchImpl<IMecAppCOM, &IID_ITestCOM, &LIBID_TestLib, /*wMajor =*/ 1, /*wMinor =*/ 0>
{
public:
   static CTestCOM * p_CTestCOM;

CTestCOM()
{
    p_CTestCOM = this;
}

Incoming.CPP (a class running on thread 2 that should trigger an event in the following case statement)

case INCOMING_EVENT_1:
// Trigger Fire_TestEvent in thread 1
// CTestCOM::p_CTestCOM->Fire_TestEvent(); trigger event on thread 2

In the code above you can find my current workaround for this problem which is to create a pointer object p_CTestCOM that will allow any class running on thread 1 to trigger the COM events. Thread 2 can access that object but it would trigger it in thread 2, which wouldn't work. To solve this all methods defined in Incoming.CPP could post a message (using PostMessage()) to thread 1 which would use p_CTestCOM to access and send the COM event. This would work, but I am sure there must be a better (and safer) solution that more accurately follows the COM design principles.

I someone could shed some light I would be most grateful!

2条回答
一夜七次
2楼-- · 2019-07-09 03:26

The important thing to start with is the threading model of your COM object. It is is MTA, then you might want to simply initialize your worker thread respectively with COM, with CoInitializeEx(NULL, COINIT_MULTITHREADED) and fire events right from there.

If the object is STA, which is most likely the case, and what might be necessary e.g. to well integrate the object with some environments, then you need to fire events from the main thread, and to get there from the worker thread you need to use some synchronization. For example, on your worker thread you discover a need to fire an event. You code might set a flag or an internal synchronization object (such as event) so that main thread code would eventually notice it and proceed with raising an external event from there.

Another common solution for STA COM object is to create in advance a hidden/message-only window so that a worker thread could post a message on it. The message will be dispatched to the window's procedure on window creation thread, that is on STA thread and the message handler would be a safe place to fire COM event from.

Alternatively you can create an internal COM object and marshal it into worker thread to create a proxy/stub pair, and have your call from worker thread automatically converted into call of your method on main thread through marhsaling. This is workable but inferior to window messaging in almost every way: your interface/object needs to be suitable for proxy/stub pair creation, COM call is blocking while with a windows message you always have a choice between SendMessage and PostMessage, and it is the latter you wlil want to prefer to avoid deadlocks in interthread communication.

查看更多
你好瞎i
3楼-- · 2019-07-09 03:27

Roman R. provides some good options but there is a better alternative, IMO: you can marshal the listeners to the thread which fires the event. As advising the listeners is typically done inside IConnectionPointImpl class in an ATL project, you 'just' need to modify the default IConnectionPointImpl to do the marshalling for you (e.g. via GIT which is simpler that marshaling API).

The big advantage is that the rest of the code remains almost the same as before, so no message passing or synchronization is needed - only the generated *CP.h file needs to be updated.

The implementation is discussed in Microsoft knowledge base article KB280512 which seems to be removed now, but there is an improved implementation by PJ Naughter which you can use to replace the default implementation.

Here's the version that I use, based on the missing KB article. The usage is simple, just rename the class in the generated CP.h file and modify m_vec.GetAt parts, as described in the gist I have linked.

查看更多
登录 后发表回答