COM, ATL

Dispinterface vs. Events and Runtime Sinks - 싱크에서 디스핀터페이스 사용 코딩 구현 참조

디버그정 2008. 8. 28. 06:13
Dispinterface vs. Events and Runtime Sinks
Rating: none

Andrew Whitechapel (view profile)
April 12, 2001

In the normal COM scheme of things, the communication between client and server is driven by the client. The client creates the server's COM object, and makes calls into the object as it needs to. The object generally sits there passively waiting for calls from clients. However, sometimes, things happen which make the server want to notify the client, and these things might be asynchronous to anything the client is doing, and perhaps only peripherally associated with any client activity. For example, if your control has a user interface, and the user clicks on it, you might want to notify the container. Or, if your object encapsulates some business rule that has been broken, you might want to tell your client. Or, say an object that looks after a database might be updating its table indexes, and connected clients need to know when this processing is finished so that they can continue retrieving rowsets.

To code a solution to these requirements, you only really have two choices: client-initiated polling; and object-initiated notification. With polling, the client decides when to ask if some condition has been met (control clicked, business rule broken, indexes updated, etc). Clearly, this is inefficient. Much better is the solution where the object initiates the notification - since, after all, the object knows immediately when the notification is required, and only the one call is needed, instead of the iterative polling calls.
(continued)

In order for this to work in a COM universe, it requires the client to implement an interface that the object makes calls on. This hooks into all the usual advantages of specifying COM interfaces, and logically represents the model since for the purposes of the event notification, the object is acting like a client, and the client is acting like a server.

However, the object still specifies the interface - this, too, is reasonable, since the object must clearly know about and support the interface event though it doesn't actually implement it. Also, the interface can be described in the server's IDL code and built into its type library, while the client on the other hand is unlikely to have any IDL code of its own. Since the object is the source of the calls on this outgoing interface, this interface is called a source interface. The client is the sink for calls on this interface.

The source interface

Writing a source interface in IDL is very simple, eg:

[ uuid(1F8C0130-CDF5-11d3-B77E-00104BDC292F),
 helpstring("_IRobotEvents Interface")
]
dispinterface _IRobotEvents
{
 properties:
 methods:
 [id(1), helpstring("method Finished")] HRESULT Finished();
};

The Finished event will be fired by the COM object whenever it deems appropriate. There's nothing in the IDL for the interface itself that tells us whether the interface is a source or sink interface. It can be either, depending on how it's used. Whether it's a source interface is determined by our particular object, so we specify it in the coclass section of our IDL:

coclass Robot
{
 [default] interface IMotion;
 [default, source] dispinterface _IRobotEvents;
};

Note that you can have two default interfaces, so long as one is a source interface and the other isn't. Also, remember that the source object does NOT implement the source interface - it merely defines it through its type library. The client will actually implement the source interface, having read the server's type library to find out the details of this interface.

IConnectionPoint and IConnectionPointContainer

On the other hand, the source object will implement a connection point. Your source object will have exactly one connection point for each outgoing interface. Connection points are separate COM objects, but they're created by your object, not by CoCreateInstance, and typically implement only two interfaces: IUnknown and IConnectionPoint.

COM objects can support more than one connection point. In order to do this, the source object must implement the IConnectionPointContainer interface. This interface allows the client object to obtain a connection point.

IConnectionPointContainer is a simple interface, with only two methods: FindConnectionPoint returns a pointer to the connection point specified by the IID passed by the client object, and EnumConnectionPoints returns an IEnumConnectionPoints enumerator that allows the holder to walk through all of the connection points supported by the source object.

So, the source object implements IConnectionPointContainer. The object also maintains a collection of connection points that can be searched and enumerated via the methods in IConnectionPointContainer. Each connection point maintains a list of active connections, set up by calls to the connection point's Advise method and ended by calls to the connection point's Unadvise method.

Sequence of calls to set up an event

  • The client queries the source object for IConnectionPointContainer. If an object implements IConnectionPointContainer, it indicates that it does fire events of some kind.
  • If the QueryInterface succeeds, the client passes IConnectionPointContainer::FindConnectionPoint the IID of an event interface it wants to receive. If the connectable object supports that interface, it returns a pointer to the server's connection point object for that interface. Alternatively, the client can call IConnectionPointContainer::EnumConnectionPoints to get an enumerator, so it can examine all of the supported connection points (and see whether it understands any of them).
  • Assuming that we've got a pointer to a connection point, the client calls IConnectionPoint::Advise on that pointer, passing an IUnknown pointer pointing to the mini-object that will actually receive the events (that is, the object in the client code that implements the specified event interface). The server object's implementation of Advise stores this interface pointer internally, and returns a cookie to the client, which the client in turn stores internally. This cookie is a DWORD with a unique value that represents this connection.
  • From this point, the server's source object can fire an event by getting the interface pointer from the connection point object and calling a method on that pointer. In other words, the server object calls through its internally stored sink object pointer to the client's sink object method. Since there may be multiple clients, the source object maintains a list of connected clients, enumerates these connections and calls the implemented event interface method on each one.
  • When it wants to stop receiving events (such as before it shuts down), the client calls IConnectionPoint::Unadvise, passing the cookie it stored previously. This causes the connection point object to delete the associated interface pointer.

Modifying a Server to Support Events

The following steps detail the modifications necessary to an existing ATL object in order to support firing an event through a dispinterface. If you were creating a new COM object from scratch, you'd make it support connection points by choosing that option in the ATL Object Wizard. In this case, you'll duplicate the effects of choosing that option by adding that support manually.
  1. Add a dispinterface to the library section of the .idl file and get a new GUID, as indicated below. Also add the dispinterface to the coclass as the [default, source] interface.
    [uuid(1F8C0130-CDF5-11d3-B77E-00104BDC292F),
     helpstring("_IRobotEvents Interface")
    ]
    dispinterface _IRobotEvents
    {
     properties:
     methods:
    };
    
  2. In ClassView, right-click the interface _IRobotEvents and add an arbitrary method named Finished to the interface. For simplicity, specify void return, no arguments.
  3. Now, generate code to fire the event. First, run the MIDL compiler on the .idl file to get the built type library. Then, in ClassView, right-click the COM object class and choose Implement Connection Point. Select the _IRobotEvents interface and click OK. The wizard then makes the following changes to your COM object class (but, be warned, there are bugs in the wizard, so check that all the changes have actually been made correctly):
    • Adds the interface IConnectionPointContainerImpl to the multiple inheritance list
    • Adds IConnectionPointContainer to the COM map.
    • Adds a connection point map with an entry for the dispinterface.
    • (MSVC 6.0 sometimes incorrectly inserts IID__IRobotEvents as the argument to the CONNECTION_POINT_ENTRY macro. Be sure to verify that DIID__IRobotEvents was entered - the first 'D' is the convention to indicate a dispinterface, and there are two underscores because there was already one at the beginning of the interface name).
    • Includes the connection point header file ATLRobotCP.h.
    • Adds the connection point proxy class (CProxy_IRobotEvents) to the multiple inheritance list. Examine this proxy class:
    template <class T>
    class CProxy_IRobotEvents :
    public IConnectionPointImpl<T, &DIID__IRobotEvents,
    CComDynamicUnkArray>
    {
    public:
     HRESULT Fire_Finished()
     {
      CComVariant varResult;
      T* pT = static_cast(this);
      int nConnectionIndex;
      int nConnections = m_vec.GetSize();
    
      for (nConnectionIndex = 0;
           nConnectionIndex < nConnections;
           nConnectionIndex++)
      {
       pT->Lock();
       CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex);
       pT->Unlock();
    
       IDispatch* pDispatch =
        reinterpret_cast<IDispatch*>(sp.p);
    
       if (pDispatch != NULL)
       {
        VariantClear(&varResult);
        DISPPARAMS disp = { NULL, NULL, 0, 0 };
        pDispatch->Invoke(0x1, IID_NULL,
                          LOCALE_USER_DEFAULT,
                          DISPATCH_METHOD,
                          &disp, &varResult,
                          NULL, NULL);
       }
      }
      return varResult.scode;
     }
    };
    
  4. Next, fire the event: in any method in the object: at some appropriate point, call the implemented Fire_Finished method of the inherited proxy class, bearing in mind that this will actually walk the list of connected sinks and call the Finished method on each client.

Modifying a Client to Support Events

Again, suppose you already have an MFC client. The next section lists the steps needed to add event sink support, so that you can clearly see what the extra code involved should be. In this exercise, you will add a new class and additional code so that the client can receive events from the COM object server that you modified in the previous exercise. You will use an ATL class as the base class for your sink object.
  1. For example, start with a regular MFC dialog-based application, and #import the server's type library. Add an interface pointer as a member in the dialog class, using the #import-generated _com_ptr_t smart pointer class, (eg ISomethingPtr m_pIS). Initialize this in the OnInitDialog, eg:
    try
    {
     m_pIS = ISomethingPtr(CLSID_Robot);
    }
    catch (_com_error &e)
    {
     MessageBox(e.ErrorMessage());
    }
    
  2. Put a button on the dialog, and get a BN_CLICKED handler. Code this handler to call any of the object's methods, eg:
    void CMfcClientDlg::OnButton1()
    {
     try
     {
      int j = 0;
      m_pIS->SomeFunc(&j);
      char s[8];
      wsprintf(s, "%d", j);
      MessageBox(s);
     }
     catch (_com_error &e)
     {
      MessageBox(e.ErrorMessage());
     }
    }
    
  3. We now need to add support for the ATL: add the following lines of code to the stdafx.h:
    #include <atlbase.h>
    extern CComModule _Module;
    #include <atlcom.h>
    
  4. Add this line to the stdafx.cpp
    #include <atlimpl.cpp>
    
  5. Now, create, initialize, and terminate a global instance of the CComModule class. First, at file-level scope in the implementation file of the application object (say, just above the constructor), declare a CComModule object:
    CComModule _Module;
    
  6. In the InitInstance, initialize and terminate the _Module object around the declaration and use of the dialog object. Force the dialog object's destructor to execute before the call to CComModule::Term by placing the declaration and use of the dialog object within a new block - this is because our dialog is going to use the COM object with ATL support, and we can't have its destructor doing any such work after the CComModule::Term has erased ATL COM support.
    _Module.Init(NULL, m_hInstance);
    // declaration of dialog object, and DoModal, etc
    _Module.Term();
    
    
  7. Now add a new class to implement the event interface. First, add a new C++ source file to the project. Name the file EventHandler.cpp. Also add a new C++ header file to the project. Name the file EventHandler.h. In the header file EventHandler.h, add a declaration for a class named CEventHandler that publicly inherits from IDispEventImpl. Add the six arguments to this template class. You can get the IID of the dispinterface from the .tlh file. Add a SINK map to the class CEventHandler by using BEGIN_SINK_MAP() AND END_SINK_MAP. To the SINK map, add a SINK_ENTRY_EX macro for the event method Finished. Add a prototype for the Finished method. Remember that the function must use the __stdcall calling convention. You can find this function's signature in the .tlh file.
  8. Add the following lines of code to the file EventHandler.cpp:
    #include "stdafx.h"
    #include "EventHandler.h"
    
  9. Write code for the Finished function - for the exercise, just call AfxMessageBox to display a suitable message.
  10. Now to incorporate the EventHandler class within the dialog application. First, add a CEventHandler pointer as aprivate member variable in the dialog class - call it m_pHandler. Include the header file EventHandler.h in the dialog class header file. Add code to the try block in the OnInitDialog to create a sink object and to make the connection point in the server aware of this sink with the function DispEventAdvise, as shown below.
    m_pHandler = new CEventHandler;
    IUnknownPtr pUnk = m_pIS;
    m_pHandler->DispEventAdvise(pUnk);
    
  11. Add a destructor to the dialog class. In the destructor, call the DispEventUnadvise function and delete the CEventHandler object, as shown below:
    IUnknownPtr pUnk = m_pIS;
    m_pHandler->DispEventUnadvise(pUnk);
    delete m_pHandler;
    
  12. Build and test the project. When you click the button, you should get a message box from the Finished event.

Dispinterface vs VTBL Events

Recall that the type of interface for our events is a dispinterface. Why, you might ask? Well, despite the fact that dispatch interfaces are slower, they do simplify things into just an implementation of IDispatch. Also, with a dispatch interface, the object receiving the calls doesn't have to provide implementations of all the methods in the interface. Remember that when you implement a regular custom (or, for that matter, dual) interface, you must implement all of the methods on that interface (at least to return E_NOTIMPL).

For a dispatch interface, you have to implement all of the methods of IDispatch, but your implementation of Invoke doesn't have to implement every method in the dispatch interface -- it can just return an error (DISP_E_MEMBERNOTFOUND) for methods it doesn't support (in other words, events it doesn't care to handle).

So, because it's easier for a language such as VB or VBA to get event calls on a dispatch interface rather than on a custom interface, that's all they support. If you're writing your own sink in C++ and you don't care about other clients, you can use a custom interface for improved performance. But in most cases, events are relatively rare, so usually the performance isn't a big issue.

If you want to have an event interface that can be high performance and compatible with VB, implement two equivalent source interfaces (one custom and one dispatch) with different interface IDs (IIDs), and make the dispatch interface the default so VB will be happy.

Server with VTBL Event

We'll continue with our ATL object, and modify the current version so that its event interface is also exposed by a VTBL (custom) interface, ie an interface derived from IUnknown.
  1. Create a new GUID and add an interface named IFinishedEvent, which is derived from IUnknown, to the library section of the .idl file, as shown below. Add the interface IFinishedEvent to the coclass as a source interface. Copy the Finished method from the dispinterface to the IFinishedEvent interface. Make sure the return type is HRESULT.
    [uuid(Fresh guid here),
     helpstring("...")
    ]
    interface IFinishedEvent : IUnknown
    {
    };
    
    
  2. Now run the MIDL compiler on the .idl file to generate the type library. Then, in ClassView, right-click the COM object class and choose Implement Connection Point. Select both the _IRobotEvents interface and the IFinishedEvent interface, and click OK. This will regenerate the ATLRobotCP.h file with proxy classes for each event interface. The one based on _IRobotEvents will be the same as before, but note the code for the new one:
    template <class T>
    class CProxyIFinishedEvent
    : public IConnectionPointImpl<T,
                                  &IID_IFinishedEvent,
                                  CComDynamicUnkArray>
    {
    public:
     HRESULT Fire_Finished()
     {
      HRESULT ret;
      T* pT = static_cast<T*>(this);
      int nConnectionIndex;
      int nConnections = m_vec.GetSize();
    
      for (nConnectionIndex = 0;
           nConnectionIndex < nConnections;
           nConnectionIndex++)
      {
       pT->Lock();
       CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex);
       pT->Unlock();
    
       IFinishedEvent* pIFinishedEvent =
        reinterpret_cast<IFinishedEvent*>(sp.p);
    
       if (pIFinishedEvent != NULL)
        ret = pIFinishedEvent->Finished();
      }
    
      return ret;
     }
    };
    
    
  3. Replace the event-firing call that you previously put into one of the object's methods with a pair of calls to both versions of the Finished event. You must use the full method name of the event from both source interfaces, as shown below. Then build the server again.
    CProxy_IRobotEvents<CRobot>::Fire_Finished();
    CProxyIFinishedEvent<<Robot>::Fire_Finished();
    

Client supporting VTBL Events

Now modify your current client (with dispinterface event support), so that it implements the custom event interface on the ATL Robot server instead of the dispinterface event.
  1. First take a copy of your current MFC dialog-based client project. We need all the ATL support as before, including the various #includes, the CComModule object, and the Init and Term in the InitInstance.
  2. Now the event handler class. Comment out the previous dispinterface-based CEventHandler class declaration, and declare a replacement. This class will also be called CEventHandler, but will inherit from CComObjectRoot and IFinishedEvent. Add a COM map to the class, and a COM_INTERFACE_ENTRY for the IFinishedEvent interface. Use the STDMETHOD macro to add a prototype to the function that will receive the event. If you are unsure of the function name, look at the primary type library (TLH) file. In the EventHandler.cpp, comment out the previous implementation of the function, and replace it with this:
    STDMETHODIMP CEventHandler::raw_Finished()
    {
     AfxMessageBox("Got a VTBL event");
     return S_OK;
    }
    
    
  3. In the dialog class, remove the CEventHandler pointer member, and replace it with a DWORD for the cookie. In the OnInitDialog, add code to create an instance of the event handler class, and use AtlAdvise to connect to the source, eg:
    CComObject<CEventHandler> *pHandler;
    CComObject<CEventHandler>::CreateInstance(&pHandler);
    HRESULT rc = AtlAdvise(m_pIM, pHandler->GetUnknown(),
                           IID_IFinishedEvent, &m_dwCookie);
    
    
  4. In the WM_DESTROY handler, call AtlUnadvise, as shown below. Then built and test.
    if (m_pIM)
     AtlUnadvise(m_pIM, IID_IFinishedEvent, m_dwCookie);
    

Multiple Clients, Single Server

If your COM object resides in a DLL server, it will be loaded into the address space of a client - if there are multiple clients, there will be multiple instances of the DLL, one for each client. However, in some situations, your server will be performing some central, poolable activity such that multiple clients need to connect to the same instance of the server's COM object(s). In this case, you need to set up your server as an EXE server, and register the class factory or factories for multiple use (the default with the ATL COM AppWizard generated code). Also, you need to set your COM object class factory for single object creation.
  1. Create a new ATL COM AppWizard project, and make it an EXE server. Insert a new ATL object. Make sure the "Support connection points" box is checked. Add whatever methods you want to the interface, and implement them in some simple way.
  2. Add the DECLARE_CLASSFACTORY_SINGLETON macro to the object's C++ class. Add a method to the event interface, called Finished, no parameters.
  3. Compile the IDL. Right-click on the CRobot class and select "Implement connection point". Select the _IMotionEvents dispinterface and OK everything. Then check that the wizard has done its job properly. Build the server.
  4. Take a copy of the previous mfcClient project, and make the following changes. First, change the #import to use the new LocalRobot type library. Double-check the interface ID used throughout, especially in the CEventHandler class - it should probably be DIID__IMotionEvents. Also change the LIBID. Build and test the client - everything should work as before.
  5. Now run multiple clients - they should all be using the same instance of the server object - so when any one of them triggers the event, it will be sent to all of them.

Runtime Sinks

Some clients - notably scripting clients - might wish to implement a connection point sink at runtime. In order to support this, your component needs to support IProvideClassInfo2. This extends IProvideClassInfo with the GetGUID method, which allows a client to retrieve an object's outgoing interface IID for its default event set. In an ATL object, you can use IProvideClassInfo2Impl to support this.

You may also add the IObjectSafety interface - this marks a component as safe for scripting. Again, the ATL supports this with the IObjectSafetyImpl class. Finally, you must add an entry for IProvideClassInfo to the COM map; however, because this interface serves as the base for IProvideClassInfo2, you need not add IProvideClassInfo to the multiple inheritance list.

  1. Create a new ATL COM AppWizard project: call it SimpleSink, all defaults. Insert a new ATL object, with the short name Simple. Make sure the "Support connection points" box is checked. Then #include to the top of your Simple.h. Add IProvideClassInfo2Impl and IObjectSafetyImpl to the multiple inheritance:
    public IProvideClassInfo2Impl<&CLSID_Simple,
     &DIID__ISimpleEvents,
     &LIBID_SIMPLESINKLib>,
    public IObjectSafetyImpl<CSimple,
     INTERFACESAFE_FOR_UNTRUSTED_CALLER>
    
  2. Update the COM map:
    COM_INTERFACE_ENTRY(IProvideClassInfo)
    COM_INTERFACE_ENTRY(IProvideClassInfo2)
    COM_INTERFACE_ENTRY(IObjectSafety)
    
  3. Add a method to the _SimpleObject event interface, called e1. Compile the IDL, right-click the CSimple class and Implement Connection Point. Add a method to the ISimple interface, called m1. Implement this to call Fire_e1. Build. File|New|File: add an HTML file called TestSimpleSink, and insert the following code where the comment is:
    <OBJECT ID="Simple"
     CLASSID="CLSID:64C7097E-5FB7-11D4-A44B-00104BDC292F">
    </OBJECT>
    
    <INPUT TYPE="BUTTON" ID="b1"
     VALUE="Invoke Event-Firing Method">
    
    <SCRIPT LANGUAGE="VBS">
    sub b1_OnClick()
     Simple.m1
    end sub
    
    sub Simple_e1()
     MsgBox "Received Event"
    end sub
    </SCRIPT>
    
    
  4. Save the HTML file, and double-click it in Explorer, or right-click in the Visual Studio editor, and select Preview.

OK, we've had a quick review of the basics of events in ATL, we've considered why you might want a dispinterface event instead of a custom event interface, we've seen how to implement a 'one-object-to-many-clients' event solution, and implemented a scriptable runtime sink.