Search code examples
c#c++eventscomatl

How do you specify "event type" in COM event source and handler?


I have a COM object written in C# that I'm using in C++, and it worked without issues until I had to add events to it. I've tried looking at countless tutorials, documentation and questions here, but oddly enough none of them fit my exact situation.

From the parts in my code where I hook/unhook the event source to the receiver, I get this error:

Error   C3731   incompatible event 'HRESULT EggplantClient::IEggplantClientEvents::Completed(void)' and handler 'HRESULT CReceiver::Completed(void)'; event source and event handler must have the same event type

I have no idea what this "event type" is. I assume it's the "com" part in the CReceiver class attributes:

[module(name = "EventReceiver")]
[event_receiver(com, true)]
class CReceiver {
...

At least that's what I can gather from the Microsoft documentation regarding the error code. If that is it, how can I set the C# event source to have the same type?

Another very weird error I'm having is this:

Error   C3702   ATL is required for COM events

This points to the line where I define class CReceiver. I have the exact same header files included as in the Microsoft documentation for the error. I also get a warning for usage of ATL attributes is deprecated from line [module(name = "EventReceiver")], I assume these are related?

I've been stuck on this for days. This is my first time doing stuff with COM, and even the basic implementation of the COM server was difficult, but trying to get events working has been a complete nightmare. I'd be incredibly grateful if anyone could help on this in any way, even a link to a tutorial that shows events working from a C# COM server in a C++ client would be more than enough. Below are the relevant parts of what I have been able to piece together so far. I used this for the client code, the server part I can't even find anymore because I've waded through so many pages of this stuff.

C# COM server, the event source

namespace EggplantClient
{
    [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    [Guid("C61C7C47-BB98-4DF3-BC61-7CA9430EDE7A")]
    [ComVisible(true)]
    public interface IEggplantClientEvents
    {
        [DispId(1)]
        void Completed();
    }

    [Guid("0a805b99-756a-493c-96b7-063400f171ed")]
    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.None)]
    [ComSourceInterfaces(typeof(IEggplantClientEvents))]
    [ProgId("EggplantClient.CEggplantClient")]
    public class CEggplantClient : IEggplantClient
    {
        [ComVisible(false)]public delegate void CompletedDelegate();
        public event CompletedDelegate Completed;
...

C++ COM client, the event receiver

#define _ATL_ATTRIBUTES 1
#include <atlbase.h>
#include <atlcom.h>
#include <atlctl.h>
#include <stdio.h>

int Flag = 0;

[module(name = "EventReceiver")]
[event_receiver(com, true)]
class CReceiver {
public:

    HRESULT Completed() {
        printf_s("Event received");
        Flag = 1;
        return S_OK;
    }

    void HookEvent(EggplantClient::IEggplantClient* pSource) {
        __hook(&EggplantClient::IEggplantClientEvents::Completed, pSource, &CReceiver::Completed);
    }

    void UnhookEvent(EggplantClient::IEggplantClient* pSource) {
        __unhook(&EggplantClient::IEggplantClientEvents::Completed, pSource, &CReceiver::Completed);
    }
};

Solution

  • Events in .NET are seen as Connection Point on native side. You can use them with ATL as described here ATL Connection Points and Event Handling Principles

    So here's a small recap.

    Here is your C# class

    namespace Eggplant
    {
        [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
        [Guid("C61C7C47-BB98-4DF3-BC61-7CA9430EDE7A")]
        [ComVisible(true)]
        public interface IEggplantClientEvents
        {
            [DispId(1)]
            void Completed(string text);
        }
    
        [Guid("0a805b99-756a-493c-96b7-063400f171ed")]
        [ComVisible(true)]
        [ClassInterface(ClassInterfaceType.None)]
        [ComSourceInterfaces(typeof(IEggplantClientEvents))]
        [ProgId("EggplantClient.CEggplantClient")]
        public class CEggplantClient
        {
            [ComVisible(false)] public delegate void CompletedDelegate(string text);
            public event CompletedDelegate Completed;
    
            public CEggplantClient()
            {
                // wait 2 seconds and then call every second
                Task.Delay(2000).ContinueWith(async t =>
                {
                    do
                    {
                        Completed?.Invoke("Time is " + DateTime.Now);
                        await Task.Delay(1000);
                    }
                    while (true);
                });
            }
        }
    }
    

    You can register your C# class like this (will create an Eggplant.tlb file) with .NET Framework:

    %windir%\Microsoft.NET\Framework64\v4.0.30319\RegAsm.exe Eggplant.dll /codebase /tlb
    

    note: with .NET core and .NET 5/6/7+ you'll have to build your own .TLB, or copy your C# .NET Core code into a .NET Framework .dll and use this ...

    Here is your C/C++ code (forward refs omitted):

    #include <windows.h>
    #include <stdio.h>
    #include <atlbase.h>
    #include <atlcom.h>
    
    #import "D:\kilroy\was\here\Eggplant\bin\Debug\Eggplant.tlb" // import the tlb
    
    using namespace Eggplant; // #import by default puts generated code in a specific namespace
    
    int main()
    {
        CoInitialize(nullptr);
        {
            CComPtr<IUnknown> app;
            if (SUCCEEDED(app.CoCreateInstance(__uuidof(CEggplantClient))))
            {
                // sink events
                auto sink = new CEggplantClientEventsSink();
                if (SUCCEEDED(sink->Connect(app)))
                {
                    // this message box allows us to wait while events arrive
                    MessageBox(nullptr, L"Click to stop listening", L"Events", MB_OK);
                }
            }
        }
        CoUninitialize();
        return 0;
    }
    
    // this is the event sink
    class CEggplantClientEventsSink : public CDispInterfaceBase<IEggplantClientEvents>
    {
    public:
        CEggplantClientEventsSink() { }
    
        HRESULT Invoke(DISPID dispid, DISPPARAMS* pdispparams, VARIANT* pvarResult)
        {
            switch (dispid)
            {
            case 1: // the Completed DISPID value
                wprintf(L"Completed called text:%s\n", pdispparams->rgvarg[0].bstrVal);
                break;
            }
            return S_OK;
        }
    };
    
    // this is a generic support class to hook IDispatch events
    // adapted from here: https://devblogs.microsoft.com/oldnewthing/20130612-00/?p=4103
    template<typename DispInterface>
    class CDispInterfaceBase : public DispInterface
    {
        LONG m_cRef;
        CComPtr<IConnectionPoint> m_spcp;
        DWORD m_dwCookie;
    
    public:
        CDispInterfaceBase() : m_cRef(1), m_dwCookie(0) { }
    
        // IUnknown
        IFACEMETHODIMP QueryInterface(REFIID riid, void** ppv)
        {
            *ppv = nullptr;
            HRESULT hr = E_NOINTERFACE;
            if (riid == IID_IUnknown || riid == IID_IDispatch || riid == __uuidof(DispInterface))
            {
                *ppv = static_cast<DispInterface*>(static_cast<IDispatch*>(this));
                AddRef();
                hr = S_OK;
            }
            return hr;
        }
    
        IFACEMETHODIMP_(ULONG) AddRef() { return InterlockedIncrement(&m_cRef); }
        IFACEMETHODIMP_(ULONG) Release() { LONG cRef = InterlockedDecrement(&m_cRef); if (!cRef) delete this; return cRef; }
    
        // IDispatch
        IFACEMETHODIMP GetTypeInfoCount(UINT* pctinfo) { *pctinfo = 0; return E_NOTIMPL; }
        IFACEMETHODIMP GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo** ppTInfo) { *ppTInfo = nullptr; return E_NOTIMPL; }
        IFACEMETHODIMP GetIDsOfNames(REFIID, LPOLESTR* rgszNames, UINT cNames, LCID lcid, DISPID* rgDispId) { return E_NOTIMPL; }
        IFACEMETHODIMP Invoke(DISPID dispid, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pdispparams, VARIANT* pvarResult, EXCEPINFO* pexcepinfo, UINT* puArgErr)
        {
            if (pvarResult) VariantInit(pvarResult);
            return Invoke(dispid, pdispparams, pvarResult);
        }
    
        virtual HRESULT Invoke(DISPID dispid, DISPPARAMS* pdispparams, VARIANT* pvarResult) = 0;
    
    public:
        HRESULT Connect(IUnknown* punk)
        {
            CComPtr<IConnectionPointContainer> spcpc;
            HRESULT  hr = punk->QueryInterface(IID_PPV_ARGS(&spcpc));
            if (SUCCEEDED(hr)) hr = spcpc->FindConnectionPoint(__uuidof(DispInterface), &m_spcp);
            if (SUCCEEDED(hr)) hr = m_spcp->Advise(this, &m_dwCookie);
            return hr;
        }
    
        void Disconnect()
        {
            if (m_dwCookie)
            {
                m_spcp->Unadvise(m_dwCookie);
                m_spcp.Release();
                m_dwCookie = 0;
            }
        }
    };
    

    And this is the result:

    .NET COM Events