Click here to Skip to main content
Click here to Skip to main content

Sinking events from managed code in unmanaged C++

By , 23 Apr 2008
 

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:

[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:

[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:

[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)

About the Author

Uri Kogan
Software Developer
Israel Israel
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
Hint: For improved responsiveness ensure Javascript is enabled and choose 'Normal' from the Layout dropdown and hit 'Update'.
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
Questionhow can i pass some parameter to receive application(client)? [modified]memberhan dol5 Feb '12 - 20:26 
AnswerRe: how can i pass some parameter to give application(client)?memberUri Kogan5 Feb '12 - 20:43 
GeneralRe: how can i pass some parameter to give application(client)?memberhan dol6 Feb '12 - 0:22 
GeneralRe: how can i pass some parameter to give application(client)?memberUri Kogan6 Feb '12 - 2:24 
GeneralRe: how can i pass some parameter to give application(client)?memberhan dol8 Feb '12 - 23:44 
GeneralRe: how can i pass some parameter to give application(client)?memberUri Kogan11 Feb '12 - 19:50 
GeneralHandling Same Event Between CSharp COM DLL(Registeration Free) and C++ Application.EXEmemberMrKyaw5 Apr '11 - 16:39 
GeneralRe: Handling Same Event Between CSharp COM DLL(Registeration Free) and C++ Application.EXEmemberUri Kogan6 Apr '11 - 0:13 
GeneralThanks!memberMember 218256613 Aug '09 - 6:08 
Generalhimembervvidov10 Oct '08 - 5:26 
GeneralRe: himemberUri Kogan10 Oct '08 - 9:14 
GeneralThanks a lot!memberPrashant SL24 Sep '08 - 1:30 
GeneralQueryInterfacememberChristian Perner4 Sep '08 - 23:11 
GeneralRe: QueryInterfacememberUri Kogan4 Sep '08 - 23:48 
GeneralRegistration of server-classmemberChristian Perner4 Sep '08 - 21:06 
AnswerRe: Registration of server-classmemberUri Kogan4 Sep '08 - 21:22 
GeneralRe: Registration of server-class [modified]memberChristian Perner4 Sep '08 - 22:29 
GeneralRe: Registration of server-classmemberliaohaiwen21 Oct '08 - 21:55 
GeneralRe: Registration of server-classmemberUri Kogan21 Oct '08 - 21:59 
GeneralRe: Registration of server-classmemberliaohaiwen21 Oct '08 - 22:25 
GeneralRe: Registration of server-classmemberChristian Perner22 Oct '08 - 1:12 
GeneralRe: Registration of server-classmemberliaohaiwen22 Oct '08 - 19:29 
GeneralRe: Registration of server-classmemberChristian Perner26 Oct '08 - 20:41 
QuestionGreat work, thanks.memberhuiliu4 Jun '08 - 11:45 
AnswerRe: Great work, thanks.memberUri Kogan4 Jun '08 - 18:53 

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

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130523.1 | Last Updated 24 Apr 2008
Article Copyright 2008 by Uri Kogan
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid