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);
}
};
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: