none
Interop: COM - After tlbimp, events not firing from C# RRS feed

  • Question

  • Hi,

    We are currently facing a strange problem while communicating with a ATL COM dll from C#.

    The COM dll had a class 'Conn' which had two methods - OnClose() and RecieveMsg()
    which could be subscribed to by clients.

    This in C++ COM was being achieved using Connection Points and AtlAdvise()

    Now, we have generated the managed Wrapper dll by using tlbimp.exe,
    which has converted these two functions to Events.

    All is right so far.

    Now in C#, we are subscribing to these events as follows:

    conn.RecieveMsg += new ConnLib.IConnEvent_RecieveMsgEventHandler(RecieveMessage);
    conn.OnClose += new ConnLib.IConnEvent_OnCloseEventHandler(OnClose);

    The problem that we're facing is that only the first event handler which we have subscribed to is firing.
    That is, given the order above (First RecieveMsg and then OnClose) only RecieveMsg is fired, whereas when we
    subscribe to OnClose first, OnClose is fired when required.

    Now in the Conn.dll (C++ COM dll) code, we have the following:

    // For RecieveMsg
    if(m_vec.GetSize()>0)
    {
        IUnknown **pUnknown=m_vec.begin();
        if(*pUnknown!=NULL)
        {
            m_pConnEvent=(IConnEvent*)*pUnknown;
        }
        if(m_pConnEvent)
        {
            m_pConnEvent->RecieveMsg(bstrData);
        }
    }

    // For OnClose
    if(m_vec.GetSize()>0)
    {
        IUnknown **pUnknown=m_vec.begin();
        if(*pUnknown!=NULL)
        {
            m_pConnEvent=(IConnEvent*)*pUnknown;
        }
        if(m_pConnEvent)
        {
            m_pConnEvent->OnClose();
        }
    }

    I tried a few things and found out the following:

    If I subscribe to only one event from C#, and then fire the same, m_vec.GetSize() gives me a value of 1 - I'm printing that in the code above.
    But, when I subscribe to both events from C# abd then fire any one, m_vec.GetSize() gives me a value of 4.

    My best guess was that somehow 4 clients are getting created in the m_vec list,
    with the first one having the only the first events subscription.

    Now as to why these 4 clients are being created, I'm stumped.
    I tried to check the IL as well (By doing a tlbimp /out=Conn.il on the Generated ConnLib.dll), but could not figure out if anything's amiss.

    By the way, if we have a C++ COM app as a client which does an AtlAdvise, everything works fine.

    Any help would be super-welcome! :)

    Thanks,

    Santosh

     

    Tuesday, June 21, 2011 9:08 AM

Answers

  • Hello SanTats,

     

    1. Use of Possibly Invalid m_vec.

    1.1 If your "conn" ATL COM class supports only the IConnEvent source interface, then the same m_vec (of the IConnectionPointImpl<> class for IConnEvent) would be used for all incoming sinks.

    1.2 This means that with two events being subscribed from the C# side (i.e. ReceiveMsg() and OnClose()), there would be 2 valid sinks in m_vec. This means that as you iterate through m_vec, the first 2 elements in m_vec will always return non-NULL. The remaining elements will be NULL.

    1.3 Is your "conn" ATL COM class supporting only the IConnEvent source interface ? If it supports more than one, then you must specify the correct m_vec that you are referring to by scoping it properly, e.g. :

    CProxyIConnEvent<CConn>::m_vec.GetAt(iConnection);

    1.4 However, I believe that this should not be the issue or else the compiler would have complained of ambiguity.

    1.5 I suggest instead that you step through the ATL codes for IConnectionPointImpl<>::Advise() in atlcom.h :

    template <class T, const IID* piid, class CDV>
    STDMETHODIMP IConnectionPointImpl<T, piid, CDV>::Advise(IUnknown* pUnkSink, DWORD* pdwCookie)
    {
      ...
    }

    This ATL method will be called when the C# code attempts to connect with an event of the COM object, e.g. :

    conn.RecieveMsg += new ConnLib.IConnEvent_RecieveMsgEventHandler(RecieveMessage);

    1.6 Now since the C# side will be connecting with the ReceiveMsg() and OnClose() events, IConnectionPointImpl<>::Advise() will be called twice. Make sure your breakpoint is called twice.

    1.7 As you step through this function, note that in both cases, it must be the same m_vec that gets filled with the IUnknown pointer from the C# Sink Helper.

    1.8 Then when the time comes for firing the events, make sure it is the same m_vec that is used.

    1.9 If something is amiss, you may need to fully recompile your code, or revisit it for any signs of bugs.

    1.10. By the way, why don't you use the services of the IConnectionPointImpl<> class generated by ATL for event firing ?

     

    2. The Count of Elements in m_vec.

    2.1 The m_vec array is always set to a size larger than the current count of elements in it. The first time memory is allocated, it's size is set to allow 4 IUnknown pointers.

    2.2 Then when all 4 slots have been taken up, its size is increased by a multiple of 2 (i.e. 4, then 4 x 2 == 8, then 8 x 2 == 16, then 16 x 2 == 32 and so on).

     

    - Bio.

     


    Please visit my blog : http://limbioliong.wordpress.com/
    • Marked as answer by Paul Zhou Wednesday, June 29, 2011 8:11 AM
    Thursday, June 23, 2011 9:56 AM
  • Thank you Bio!!

     

    We found the reason for the multiple firing.

    It was a bug in our code - we were not  setting m_pConnEvent to null in the for loop! :)

     

    Thanks again for all the help! We learnt a lot from your detailed information! :)

    Cheers!

     

    • Marked as answer by Paul Zhou Wednesday, July 6, 2011 3:51 AM
    Tuesday, July 5, 2011 12:53 PM

All replies

  • Hello Santosh,

     

    1. The Cause of the Problem.

    The problem is caused by 2 issues :

    1.1 The implementation of the .NET Sink Helper Class.

    1.2 The way you fire the individual events of the IConnEvent interface.

    I will explain these in greater detail below.

     

    2. .NET Sink Helper Class.

    2.1 When you import a COM type library into a C# project or use the tlbimp.exe to generate an interop assembly, a private Sink Helper Class will be generated for each event source interface found in the type library. In your case, since IConnEvent is such an event interface, there will be a IConnEvent_SinkHelper class generated. You can see this in the Object Browser.

    2.2 The Sink Helper class provides an implementation of the event interface. Hence IConnEvent_SinkHelper implements the methods of IConnEvent. It serves as a sink object that at runtime connects with the IConnectionPoint interface of the COM object (i.e. the IConnectionPointContainer). When the COM Object's IConnectionPoint::Advise() is called, an instance of the Sink Helper Class is passed as the IUnknown pointer parameter.

    2.3 The Sink Helper Class contains the connection point cookie (m_dwCookie) that identifies one singular connection. It also contains delegate members corresponding to the respective methods of the event interface. When an event is fired from the COM object, the corresponding event handler of the Sink Helper will get called (as part of the connection point protocol). Inside the event method, the relevant delegate will be invoked.

    2.4 However, there is a twist to the Sink Helper class : one Sink Helper object is created for each delegate connected to an event of the COM object. Therefore, for the code below :

    conn.ReceiveMsg += new ConnLib.IConnEvent_ReceiveMsgEventHandler(ReceiveMessage);
    conn.OnClose += new ConnLib.IConnEvent_OnCloseEventHandler(OnClose);

    there will be 2 Sink Helper objects (more specifically, 2 IConnEvent_SinkHelper objects) created. One in charge of firing the ReceiveMessage() method. The other in charge of firing the OnClose() method. If you were to add another event handler to the ReceiveMessage() event, e.g. :

    conn.ReceiveMsg += new ConnLib.IConnEvent_ReceiveMsgEventHandler(ReceiveMessage2);

    another IConnEvent_SinkHelper object will be created that takes charge of firing the ReceiveMessage2() method. And so on.  

    2.5 The main issue is that each Sink Helper instance will only fire the delegate that it is assigned to. It is not in its design to take charge of firing more than one delegate. This is obviously inefficient as most developer will agree. And if you were to add more and more event handlers, more and more Sink Helpers will be created. Hence more and more sinks will be connected to the IConnectionPoint of the COM Object, leading to an increase in size in m_vec (m_vec.GetSize() will return an ever greater number).

    You can see this in action by putting a breakpoint in the following function :

    template <class T, const IID* piid, class CDV>
    STDMETHODIMP IConnectionPointImpl<T, piid, CDV>::Advise(IUnknown* pUnkSink, DWORD* pdwCookie)

    in atlcom.h.

    However, this is the way the Sink Helper class has been designed. I will provide a link to a discussion on this in the summary section later on.

    2.6 In your particular case, the implication is that since there are 2 event handlers for the COM object, there will be 2 IConnEvent_SinkHelper objects. Hence there are 2 sinks connected to the COM Object. This brings me to the next cause of the problem.

     

    3. Firing Events of an Event Interface via ATL.

    3.1 When you fire the events of a source interface, you must take note that there may be more than one sink connected with the connection point.

    3.2 This is precisely the case where your client is the C# application which has implemented one sink for every event handler method. In your case, there are 2 event handler methods and so there will be 2 sinks.

    3.3 Hence you should emulate the code of a typical IConnectionPointImpl<> class generated for each connection point of an ATL COM class. The following is how I suggest you should fire the various IConnEvent events :

    // For RecieveMsg
    int cConnections = m_vec.GetSize();
     
    for (int iConnection = 0; iConnection < cConnections; iConnection++)
    {
      IConnEvent* m_pConnEvent = NULL;
        
      Lock();
      IUnknown* pUnknown = m_vec.GetAt(iConnection);
      Unlock();
      
      if(pUnknown != NULL)
      {
        m_pConnEvent=(IConnEvent*)pUnknown;
      }
      
      if(m_pConnEvent != NULL)
      {
        m_pConnEvent->ReceiveMsg(bstrData);
      }  
    }

     

    // For OnClose
    int cConnections = m_vec.GetSize();
     
    for (int iConnection = 0; iConnection < cConnections; iConnection++)
    {
      IConnEvent* m_pConnEvent = NULL;
        
      Lock();
      IUnknown* pUnknown = m_vec.GetAt(iConnection);
      Unlock();
      
      if(pUnknown != NULL)
      {
        m_pConnEvent=(IConnEvent*)pUnknown;
      }
      
      if(m_pConnEvent != NULL)
      {
        m_pConnEvent->OnClose();
      }  
    }

     

    4. In Summary.

    4.1 You should not assume that the very first IUnknown pointer returned from m_vec.begin() contains the one and only relevant sink. You should iterate through all elements of m_vec and fire the relevant event using each returned IUnknown pointer.

    4.2 Indeed, in the long run, your COM Object could have many sinks, managed or unmanaged. 

    4.3 As mentioned in point 2.5 above, here is a link to a discussion on the observed inefficiency of the .NET Sink Helper Class :

    http://connect.microsoft.com/VisualStudio/feedback/details/278750/the-com-interop-event-provider-advises-a-new-sink-helper-instance-for-every-event-subscription

     

    - Bio.


    Please visit my blog : http://limbioliong.wordpress.com/

    Thursday, June 23, 2011 5:36 AM
  • Hi Bio,

    Thanks for the really detailed explanation! :)
    Thins are a bit clearer now :)

    I tried your suggestion of iterating through the contents of m_vec and firing the events,
    but now I seem to have run into another problem.

    The issue is that, now, the OnClose event handler is firing thrice every time.

    I tried changing the order of subscription, as before, and turns out that the event subscribed second fires thrice.
    The first event fires once correctly.

    I went through the C++ COM code, and here's what I noticed:
    Lets assume the order of subscrition is as below:

    conn.RecieveMsg += new ConnLib.IConnEvent_RecieveMsgEventHandler(RecieveMessage);
    conn.OnClose += new ConnLib.IConnEvent_OnCloseEventHandler(OnClose);

    Now, for RecieveMsg, in the for loop, the m_vec list has 4 connections, but only the first one is not null, so that is fired only once.

    But, for OnClose, in the for loop, the first connection in m_vec is null, and is not fired, but the other three pass the null check and fire thrice.

    I'm not sure why this is happening.
    Also, by your explanation above, my m_vec count should return 2 and not 4 right? I mean 2 for the individual Sink Helper objects? How am I getting 4 as the count?

    Thanks again, for all the help :)
    Thursday, June 23, 2011 8:59 AM
  • Hello SanTats,

     

    1. Use of Possibly Invalid m_vec.

    1.1 If your "conn" ATL COM class supports only the IConnEvent source interface, then the same m_vec (of the IConnectionPointImpl<> class for IConnEvent) would be used for all incoming sinks.

    1.2 This means that with two events being subscribed from the C# side (i.e. ReceiveMsg() and OnClose()), there would be 2 valid sinks in m_vec. This means that as you iterate through m_vec, the first 2 elements in m_vec will always return non-NULL. The remaining elements will be NULL.

    1.3 Is your "conn" ATL COM class supporting only the IConnEvent source interface ? If it supports more than one, then you must specify the correct m_vec that you are referring to by scoping it properly, e.g. :

    CProxyIConnEvent<CConn>::m_vec.GetAt(iConnection);

    1.4 However, I believe that this should not be the issue or else the compiler would have complained of ambiguity.

    1.5 I suggest instead that you step through the ATL codes for IConnectionPointImpl<>::Advise() in atlcom.h :

    template <class T, const IID* piid, class CDV>
    STDMETHODIMP IConnectionPointImpl<T, piid, CDV>::Advise(IUnknown* pUnkSink, DWORD* pdwCookie)
    {
      ...
    }

    This ATL method will be called when the C# code attempts to connect with an event of the COM object, e.g. :

    conn.RecieveMsg += new ConnLib.IConnEvent_RecieveMsgEventHandler(RecieveMessage);

    1.6 Now since the C# side will be connecting with the ReceiveMsg() and OnClose() events, IConnectionPointImpl<>::Advise() will be called twice. Make sure your breakpoint is called twice.

    1.7 As you step through this function, note that in both cases, it must be the same m_vec that gets filled with the IUnknown pointer from the C# Sink Helper.

    1.8 Then when the time comes for firing the events, make sure it is the same m_vec that is used.

    1.9 If something is amiss, you may need to fully recompile your code, or revisit it for any signs of bugs.

    1.10. By the way, why don't you use the services of the IConnectionPointImpl<> class generated by ATL for event firing ?

     

    2. The Count of Elements in m_vec.

    2.1 The m_vec array is always set to a size larger than the current count of elements in it. The first time memory is allocated, it's size is set to allow 4 IUnknown pointers.

    2.2 Then when all 4 slots have been taken up, its size is increased by a multiple of 2 (i.e. 4, then 4 x 2 == 8, then 8 x 2 == 16, then 16 x 2 == 32 and so on).

     

    - Bio.

     


    Please visit my blog : http://limbioliong.wordpress.com/
    • Marked as answer by Paul Zhou Wednesday, June 29, 2011 8:11 AM
    Thursday, June 23, 2011 9:56 AM
  • Thank you Bio!!

     

    We found the reason for the multiple firing.

    It was a bug in our code - we were not  setting m_pConnEvent to null in the for loop! :)

     

    Thanks again for all the help! We learnt a lot from your detailed information! :)

    Cheers!

     

    • Marked as answer by Paul Zhou Wednesday, July 6, 2011 3:51 AM
    Tuesday, July 5, 2011 12:53 PM