Click here to Skip to main content
15,867,568 members
Articles / Desktop Programming / ATL
Article

Sinking events from managed code in unmanaged C++

Rate me:
Please Sign up or sign in to vote.
4.85/5 (12 votes)
23 Apr 2008CPOL5 min read 90K   1.3K   33   30
Raising events in managed code and sinking it in unmanaged C++.

Introduction

As time goes by, some parts of the big project I am working on are translated to .NET from native C++. One of the first modules that was translated is a lower level module that should be used by an unmanaged code application. This raised an interesting problem. The lower level library uses Interop to wrap itself in the COM shell. Since the library works with asynchronous I/O, it can raise events. The only example usage for C++ code I've found is an extension of a Microsoft article, How to: Raise Events Handled by a COM Sink. The full article contains not only a VB6 sink example but also C++ sinks: COM Interop: Really Raising Events Handled by a COM Sink.

The only problem with all those examples is that none of them worked for me. The ATL/C++ example threw an AccessViolationException in the managed .NET object whenever the delegate was invoked. The control never reached the handling function in the C++ sink. If not handled, an AccessViolationException was returned as the E_POINTER (0x80004003) result to the COM client.

After a week of suffering and trying to find a solution that will make delegates work, I turned for help to Jason Hunt of the above article. The only solution that was brought up was to do all the work manually. This was not as hard as it sounded.

Implementation

Let's start with the implementation. First of all, let's look at the interface of the events handler. This interface is defined by the .NET object and will have to be implemented by the COM client:

C#
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IHehServerEvents
{
    void EmptyEvent();
}

The interface describes a single event handler which will be raised by the .NET component. InterfaceType is set to IUnknown for simplicity, so the COM client will have a smaller interface to implement.

Let's continue with the interface. It is possible to expose the actual implementation of the server, but it's cleaner to define only a narrow, well defined interface:

C#
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IHehServer
{
    void TestAddListener(IHehServerEvents evt);
    void TestEvent();
}

Our COM object implemented in .NET will expose only two methods:

  • TestAddListener - allows to add one more listener to the event.
  • TestEvent - initiates the event.

And now, the real stuff, the actual implementation of the class raising the event:

C#
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
public class HehServer : IHehServer
{
    public List<IHehServerEvents> m_listeners = new List<IHehServerEvents>();

    public void TestEvent()
    {
        foreach (IHehServerEvents evt in m_listeners)
        {
            evt.EmptyEvent();
        }
    }

    public void TestAddListener(IHehServerEvents evt)
    {
        m_listeners.Add(evt);
    }
}

First of all, the type of the interface hides the implementation of the server by setting its ClassInterfaceAttribute to ClassInterfaceType.None. The client will use it only via the interface implemented: IHehServer. Pay attention that there is no signs that this implementation throws any kind of events like it is done in every other example using delegates: ComSourceInterfacesAttributes is omitted.

The implementation holds a list of listeners subscribed to the event. Subscribing to events using TestAddListener just adds the listener to the list. This way, when the event is called, the implementation just has to go over the list and call every listener.

When building, don't forget to compile the component as a DLL visible to COM. In .NET Project Properties, set "Output type" to "Class Library", then enter "Assembly Information", and select "Make assembly COM-Visible". I also use the "Register for COM Interop" option in the "Build" tab to integrate the component more tightly with Visual Studio.

Now, let's use the implemented class. We are going over to the dark side: C++. One of the headers should include a reference to the compiled TLB file:

#import "..\comserver\bin\Debug\comserver.tlb" \
    raw_interfaces_only, named_guids, no_namespace

And, here is the class declaration:

class CClientTest : public IHehServerEvents
{
private:
    IHehServerPtr m_server;
    int m_EmptyEventTester;
public:
    CClientTest(void);
    ~CClientTest(void);
    void TestEvent(void);
    HRESULT __stdcall QueryInterface(const IID &, void **);
    ULONG __stdcall AddRef(void) { return 1; }
    ULONG __stdcall Release(void) { return 1; }
    HRESULT __stdcall EmptyEvent(void);
};

First of all, the class inherits from the events interface IHehServerEvents which makes it a listener. Since IHehServerEvents has an IUnknown parent from the point of view of the C++ compiler, we are required to implement the QueryInterface, AddRef, and Release methods. This class is not a real COM object that should count its references, so it is enough to just return a dummy value for adding and removing references to the object. The only method that should be implemented "for real" is QueryInterface. And, of course, each one of the event handling methods should be implemented by this class.

Pay attention to the fact that every method here is declared with an __stdcall attribute because this is the way COM works. It is possible to use ATL's STDMETHOD/STDMETHODIMP macros here.

The implementation of the class is fairly simple. First of all, we need to instantiate a COM object that throws the event:

CClientTest::CClientTest(void)
{
    HRESULT hr = S_OK;
    hr = m_server.CreateInstance(__uuidof(HehServer));
    ATLASSERT(SUCCEEDED(hr));
}

Here, the .NET provided libraries are used. IHehServerPtr has methods that allow creating relevant instances of the underlying class. Note that the GUID of the created class is the identification of the implementation and not the interface.

Using the class is also very simple:

void CClientTest::TestEvent(void)
{
    m_server->TestAddListener(this);
    m_EmptyEventTester = 0;
    m_server->TestEvent();
    ATLASSERT(m_EmptyEventTester != 0);
}

First, the class subscribes itself for events of the .NET component. And then, the TestEvent method is being called. The only thing the handler of the event is doing is change the value of the flag used for testing:

HRESULT CClientTest::EmptyEvent(void)
{
    m_EmptyEventTester = 1;
    return S_OK;
}

And last, but definitely not least, TestAddListener accepts an IHehServerEvents argument while our class is of a different type. Since we are working in the COM world, simple casting is not and the QueryInterface should be used:

HRESULT CClientTest::QueryInterface(const IID & iid,void ** pp)
{
    if (iid == __uuidof(IHehServerEvents) || iid == __uuidof(IUnknown))
    {
        *pp = this;
        AddRef();
        return S_OK;
    }
    return E_NOINTERFACE;
}

Viola! We have our callbacks from the COM code.

Points of Interest

Of cause, a real world implementation should include error checking and an option to unsubscribe from the events, but this is a pretty simple task after the principles are understood. If I am wrong, leave a comment, and I will see what can be done about it. This exercise provides one more proof that you can not always rely on the tools provided by compilers and platforms. Sometimes, you just have to perform the work yourself.

Credits

Thanks Jason Hunt from Noticably Different for ideas and help in writing this article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Israel Israel
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionIn MFC application : Exception thrown at 0x7281027F (mscorwks.dll) in MFCApplication1.exe: 0xC0000005: Access violation reading location 0x00000008. Pin
Noobs R Us24-Mar-17 5:22
professionalNoobs R Us24-Mar-17 5:22 
Questionhow can i pass some parameter to receive application(client)? Pin
han dol5-Feb-12 20:26
han dol5-Feb-12 20:26 
AnswerRe: how can i pass some parameter to give application(client)? Pin
Uri Kogan5-Feb-12 20:43
Uri Kogan5-Feb-12 20:43 
GeneralRe: how can i pass some parameter to give application(client)? Pin
han dol6-Feb-12 0:22
han dol6-Feb-12 0:22 
GeneralRe: how can i pass some parameter to give application(client)? Pin
Uri Kogan6-Feb-12 2:24
Uri Kogan6-Feb-12 2:24 
GeneralRe: how can i pass some parameter to give application(client)? Pin
han dol8-Feb-12 23:44
han dol8-Feb-12 23:44 
GeneralRe: how can i pass some parameter to give application(client)? Pin
Uri Kogan11-Feb-12 19:50
Uri Kogan11-Feb-12 19:50 
GeneralHandling Same Event Between CSharp COM DLL(Registeration Free) and C++ Application.EXE Pin
MrKyaw5-Apr-11 16:39
MrKyaw5-Apr-11 16:39 
GeneralRe: Handling Same Event Between CSharp COM DLL(Registeration Free) and C++ Application.EXE Pin
Uri Kogan6-Apr-11 0:13
Uri Kogan6-Apr-11 0:13 
GeneralThanks! Pin
Member 218256613-Aug-09 6:08
Member 218256613-Aug-09 6:08 
Generalhi Pin
vvidov10-Oct-08 5:26
professionalvvidov10-Oct-08 5:26 
GeneralRe: hi Pin
Uri Kogan10-Oct-08 9:14
Uri Kogan10-Oct-08 9:14 
GeneralThanks a lot! Pin
Prashant SL24-Sep-08 1:30
Prashant SL24-Sep-08 1:30 
GeneralQueryInterface Pin
Christian Perner4-Sep-08 23:11
Christian Perner4-Sep-08 23:11 
GeneralRe: QueryInterface Pin
Uri Kogan4-Sep-08 23:48
Uri Kogan4-Sep-08 23:48 
GeneralRegistration of server-class Pin
Christian Perner4-Sep-08 21:06
Christian Perner4-Sep-08 21:06 
AnswerRe: Registration of server-class Pin
Uri Kogan4-Sep-08 21:22
Uri Kogan4-Sep-08 21:22 
I had this problem when trying to register incorrect DLL with incorrect TLB.
You should register built DLL with regasm with "/tlb" switch and then copy generated TLB to the location from which you are including it.
Most chances here are that you have number of TLB files and DLL files in different directories. Incorrect one is used during your C/C++ program compilation.
GeneralRe: Registration of server-class [modified] Pin
Christian Perner4-Sep-08 22:29
Christian Perner4-Sep-08 22:29 
GeneralRe: Registration of server-class Pin
liaohaiwen21-Oct-08 21:55
liaohaiwen21-Oct-08 21:55 
GeneralRe: Registration of server-class Pin
Uri Kogan21-Oct-08 21:59
Uri Kogan21-Oct-08 21:59 
GeneralRe: Registration of server-class Pin
liaohaiwen21-Oct-08 22:25
liaohaiwen21-Oct-08 22:25 
GeneralRe: Registration of server-class Pin
Christian Perner22-Oct-08 1:12
Christian Perner22-Oct-08 1:12 
GeneralRe: Registration of server-class Pin
liaohaiwen22-Oct-08 19:29
liaohaiwen22-Oct-08 19:29 
GeneralRe: Registration of server-class Pin
Christian Perner26-Oct-08 20:41
Christian Perner26-Oct-08 20:41 
QuestionGreat work, thanks. Pin
huiliu4-Jun-08 11:45
huiliu4-Jun-08 11:45 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.