Exposing managed events to COM using C++

2019-02-15 20:28发布

问题:

It is possible to exposed managed events written in C# to be exposed and used in a COM object written using c++. not being that familiar with com and atl. Can you please show what would the C++ side of things would look like for the example shown in the MSDN article

http://msdn.microsoft.com/en-us/library/dd8bf0x3.aspx

The VB6 code shown proves that it is doable.

回答1:

The easiest way in C++ would be, IMO, to implement an event sink with help of ATL's IDispEventImpl and IDispEventSimpleImpl templates. An explanation with sample project can be found here.

There are many online resources about how to do this, e.g. this or this, but here is the list of required steps:

First let's take a look at managed side.

In order to provide events, we must do the following:

  • declare an event interface (IDispatch-based)
  • mark the coclass with ComSourceInterfaces attribute to bind the event interface to coclass
  • implement matching events in the coclass

Here are is the managed code:

[ComVisible(true), 
 Guid("D6D3565F-..."), 
 InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] //! must be IDispatch
public interface IMyEvents
{
    [DispId(1)] // the dispid is used to correctly map the events
    void SomethingHappened(DateTime timestamp, string message);
}

[ComVisible(true)]
[Guid("E22E64F7-...")]
[ProgId("...")]
[ClassInterface(ClassInterfaceType.None)]
[ComSourceInterfaces(typeof(IMyEvents))] // binding the event interface
public class MyComServer : IMyComServer  
{
    // here we declare the delegate for the event
    [ComVisible(false)]
    public delegate void MyEventHandler(DateTime timestamp, string message);

    // and a public event which matches the method in IMyEvents
    // your code will raise this event when needed
    public event MyEventHandler SomethingHappened;
    ... 
}

Now, back to unmanaged side. I will use ATL, as I find it the most effective way to write COM clients, but you can try MFC or do it 'manually'.

The following steps are required:

  • the sink will inherit IDispEventSimpleImpl (or IDispEventImpl)
  • sink map with all the needed methods is declared
  • handler methods are written for each event
  • the sink is registered with event source
  • eventually, when not needed anymore, the sink is disconnected

Here's the code in an ATL C++ client:

// import the typelib of your COM server
// 'named_guids' ensures friendly ID of event interface
#import "myserver.tlb" named_guids 
const UINT SINK_ID = 234231341; // we need some sink id

class MyClient : public IDispEventSimpleImpl<SINK_ID, MyClient, &MyServer::DIID_IMyEvents >
{
    public:
    // now you need to declare a sink map - a map of methods handling the events
    BEGIN_SINK_MAP(MyClient)
      SINK_ENTRY_INFO(SINK_ID, MyServer::DIID_IMyEvents, 0x1, OnSomethingHappened, &someEvent)
                                                   ^      ^      ^                   ^
      // event interface id (can be more than 1)---+      |      |                   |
      // must match dispid of your event -----------------+      |                   |
      // method which handles the event  ------------------------+                   |
      // type information for event, see below --------------------------------------+
    END_SINK_MAP()

// declare the type info object. You will need one for each method with different signature.
// it will be defined in the .cpp file, as it is a static member
static _ATL_FUNC_INFO someEvent;  // 'placeholder' object to carry event information (see below)

// method which handles the event
STDMETHOD (OnSomethingHappened)(DATE timestamp, BSTR message)
{ 
   // usually it is defined it in the .cpp file
}

... 

}

Now, we need to define the type info members in the cpp file (i.e. the someEvent instance from example above):

_ATL_FUNC_INFO MyClient::traceEvent = { CC_STDCALL, VT_EMPTY, 2 , {VT_DECIMAL, VT_BSTR} };  // dispid = 1
                                               ^        ^     ^              ^
// calling convention (always stdcall) --------+        |     |              |
// type of return value (only VT_EMPTY makes sense) ----+     |              |
// number of parameters to the event -------------------------+              |
// Variant types of event arguments -----------------------------------------+

This can be tricky as type mappings are not always obvious (e.g. a it might be clear that managed int maps to VT_I4, but it is less obvious that DateTime maps to VT_DECIMAL). You need to declare each event you plan to use in the sink map - if you don't need all of them, don't map them.

Now you need to connect your sink to the event source:

// IUnknown* pUnk = interface to you COM server instance
pMyClient->DispEventAdvise(pUnk);
// .. from this point, events will be caught by the client
// when you are done, disconnect:
pMyClient->DispEventUnadvise(pUnk);

This is it, more or less. Using IDispEventImpl instead of IDispEventSimpleImpl results in a bit less code, as you don't need to supply the type info objects which might be the ugliest part. However, it has two drawbacks:

  • requires access to the typelib (as it needs to read the interface metadata in order to provide the type info itself)
  • is a bit slower (but I would guess not significantly)


回答2:

If you can use C++/CLI you can just do (source):

// class that defines methods that will called when event occurs
ref class EventReceiver {
public:
   void OnMyClick(int i, double d) {
      Console::WriteLine("OnClick: {0}, {1}", i, d);
   }

   void OnMyDblClick(String^ str) {
      Console::WriteLine("OnDblClick: {0}", str);
   }
};

int main() {
   EventSource ^ MyEventSource = gcnew EventSource();
   EventReceiver^ MyEventReceiver = gcnew EventReceiver();

   // hook handler to event
   MyEventSource->OnClick += gcnew ClickEventHandler(MyEventReceiver, &EventReceiver::OnMyClick);
}


回答3:

The solution proposed by Zdeslav Vojkovic was almost the complete answer for me, however I ran into stability issues, when implementing the sink in an Outlook add-in. After the first unadvise the system became unstable and could crash at the next advise. The solution for me was to make the sink as a COM object.

For brevity I have only added the ATL side of the solution, as the managed code can be left exactly as Zdeslav Vojkovic proposed.

I followed these steps in Visual Studio 2017:

Create ATL Simple Object

  1. Right-click on Project and select Add > New Item...
  2. Select ATL Simple Object and click the Add button
  3. In the Other tab select "Single" for Threading model
  4. Click Finish

Fill in the sink interface details

In the generated idl add handlers for initialization and shutdown:

[
    object,
    uuid(a5211fba-...),
    dual,
    nonextensible,
    pointer_default(unique)
]
interface IMyClient : IDispatch
{
    [id(1), helpstring("method InitHandler"), local] HRESULT InitHandler(IUnknown* myserver);
    [id(2), helpstring("method ShutdownHandler"), local] HRESULT ShutdownHandler(void); 
};

Fill in the header file (MyClient.h)

const UINT SINK_ID = 234231341;
extern _ATL_FUNC_INFO SomethingHappenedInfo;

// LIBID_MyATLComLib should point to the LIBID of the type library 
class ATL_NO_VTABLE CMyClient :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CMyClient, &CLSID_MyClient>,
    public IDispatchImpl<IMyClient, &IID_IMyClient, &LIBID_MyATLComLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
    public IDispEventSimpleImpl<SINK_ID, CMyClient, &MyServer::DIID_IMyEvents>
{
public:

    typedef IDispEventSimpleImpl</*nID =*/ SINK_ID, CMyClient, &MyServer::DIID_IMyEvents> SomethingHappenedEvent;

    ...

    BEGIN_SINK_MAP(CMyClient)
        SINK_ENTRY_INFO(SINK_ID, MyServer::DIID_IMyEvents, 0x1, OnSomethingHappened, &SomethingHappenedInfo)
    END_SINK_MAP()

    ...

private:
    CComQIPtr<MyServer::IMyComServer> mMyComServer; 

public:    
    STDMETHOD(InitHandler)(IUnknown* myserver);
    STDMETHOD(ShutdownHandler)(void);

    void __stdcall OnSomethingHappened(DateTime timestamp, string message);     
};

Some of the generated code has been left as "..."

Fill in the C++ code (MyClient.cpp)

_ATL_FUNC_INFO SomethingHappenedInfo = { CC_STDCALL, VT_EMPTY, 2 , {VT_DECIMAL, VT_BSTR} };


STDMETHODIMP CMyClient::InitHandler(IUnknown* myserver)
{
    this->mMyComServer = myserver;
    SomethingHappenedEvent::DispEventAdvise((IDispatch*)this->mMyComServer);

    return S_OK;
}


STDMETHODIMP CMyClient::ShutdownHandler(void)
{    
    SomethingHappenedEvent::DispEventUnadvise(this->mMyComServer);          

    return S_OK;
}

void __stdcall CMyClient::OnSomethingHappened(DateTime timestamp, string message)
{   
    ...
}

Notice that the Advise/Unadvise calls are done differently here.



标签: c# c++ events com atl