COM, ATL

How ATL Implements Connections

디버그정 2008. 8. 28. 08:40

[Previous] [Next]

How ATL Implements Connections

Now that you have the idea behind connection points, let's look at how they work within ATL.

Setting Up Outgoing Interfaces in ATL

To support the connection points functionality, the object needs to implement IConnectionPointContainer and IConnectionPoint. ATL's support for connections consists of some template classes and a set of macros. Let's examine how to set up connections using ATL.

Setting up the outgoing interfaces in an ATL-based object involves two steps:

  1. Describing the callback interface the client needs to implement
  2. Adding the connection support to the object

As with all other interfaces in COM, the callback interfaces for an ATL-based object are described in IDL. For example, imagine you're implementing an object named CATLConnectionPointsObj that has an outgoing interface. Normally, you'd add the object to your server by selecting New ATL Object from the Insert menu. The ATL Object Wizard dialog box pops up and asks you to name the object and to specify other aspects of the ATL object, such as the threading model. One of the options you can select is whether the object implements connections. The following code shows the C++ class the wizard creates (with the Support Connection Points option checked).

class ATL_NO_VTABLE CATLConnectionPointsObj : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CATLConnectionPointsObj, 
        &CLSID_ATLConnectionPointsObj>,
    public IConnectionPointContainerImpl<CATLConnectionPointsObj>,
    public IDispatchImpl<IATLConnectionPointsObj, 
        &IID_IATLConnectionPointsObj,
        &LIBID_ATLCONNECTIONPOINTSSVRLib>
{
public:
    CATLConnectionPointsObj()
    {
    }

DECLARE_REGISTRY_RESOURCEID(IDR_ATLCONNECTIONPOINTSOBJ)

DECLARE_PROTECT_FINAL_CONSTRUCT()

BEGIN_COM_MAP(CATLConnectionPointsObj)
    COM_INTERFACE_ENTRY(IATLConnectionPointsObj)
    COM_INTERFACE_ENTRY(IDispatch)
    COM_INTERFACE_ENTRY(IConnectionPointContainer)
END_COM_MAP()
BEGIN_CONNECTION_POINT_MAP(CATLConnectionPointsObj)
END_CONNECTION_POINT_MAP()

// IATLConnectionPointsObj
public:
};

Callback interfaces are interfaces described by the object and implemented by the client. When using ATL, the starting place for defining the outgoing interface is within the IDL. Listing 12-3 shows an outgoing interface defined in the IDL.

Listing 12-3. IDL code describing an outgoing interface.

import "oaidl.idl";
import "ocidl.idl";
    [
        object,
        uuid(F4416E5D-AB04-11D2-803A-B8B4F0000000),

        helpstring("IAConnectableObject Interface"),
        pointer_default(unique)
    ]
    interface IAConnectableObject : IUnknown
    {
        [helpstring("method Method1")] HRESULT Method1();
        [helpstring("method Method2")] HRESULT Method2();
    };
[
    uuid(F4416E51-AB04-11D2-803A-B8B4F0000000),
    version(1.0),
    helpstring("abcdefg 1.0 Type Library")
]
library ABCDEFGLib
{
    importlib("stdole32.tlb");
    importlib("stdole2.tlb");

    [
        uuid(F4416E5F-AB04-11D2-803A-B8B4F0000000),
        helpstring("_IAConnectableObjectEvents Interface")
    ]
    dispinterface _IAConnectableObjectEvents
    {
        properties:
        methods:
            [id(1)]void OnEvent1();
            [id(2)]void OnEvent2();
    };
    [
        uuid(F3336E5F-AB04-11D2-803A-B8B4F0000000),
        helpstring("_IAConnectableObjectEvents Interface")
    ]
    dispinterface _IAConnectableObjectEvents2
    {
        properties:
        methods:
            [id(1)]void OnEvent3();
            [id(2)]void OnEvent4();
    };

    [
        uuid(F4416E5E-AB04-11D2-803A-B8B4F0000000),
        helpstring("AConnectableObject Class")
    ]
    coclass AConnectableObject
    {
        [default] interface IAConnectableObject; 
        [default, source] dispinterface 
            _IAConnectableObjectEvents;
        [source] dispinterface _IAConnectableObjectEvents2;
    };
};

Notice that this IDL listing includes an incoming interface named IAConnectableObject. The IDL also has two outgoing interfaces, named _IAConnectableObjectEvents and _IAConnectableObjectEvents2.

When the project is compiled, the MIDL compiler produces a type library. The clients of this object use this information to know how to implement the callback interface. Notice in this example that the outgoing interface is a dispatch interface (an instance of IDispatch). The outgoing interface doesn't have to be a dispatch interface; however, if it is, your object can call back to a wider variety of clients.

The next step in developing the object is to come up with some way to call through the outgoing interface. The ATL wizards make it easy to add the individual connection points to your application. After compiling the project once to produce the type library, simply select the class to which you want to add connection points inside ClassView and click the right mouse button. Select Implement Connection Point from the context menu. Microsoft Visual Studio will read the type library for your project. Remember that the type library contains binary descriptions of the outgoing interfaces. Visual Studio displays a dialog box asking you to select the interfaces to which you want to add connection points. Simply select the check boxes for those interfaces and click OK. Visual Studio adds the connection points for each outgoing interface listed in the dialog box. Figure 12-3 shows the outgoing interfaces listed in the dialog box.

 

사용자 삽입 이미지

Figure 12-3. Implementing connection points for a control's outgoing interface.

Visual Studio produces the following code for the incoming interface and the outgoing interface described in Listing 12-1.

class ATL_NO_VTABLE CAConnectableObject : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CAConnectableObject, 
        &CLSID_AConnectableObject>,
    public IConnectionPointContainerImpl<CAConnectableObject>,
    public IAConnectableObject,
    public CProxy_IAConnectableObjectEvents<CAConnectableObject>,
    public CProxy_IAConnectableObjectEvents2<CAConnectableObject>
{
public:
    CAConnectableObject()
    {
    }

DECLARE_REGISTRY_RESOURCEID(IDR_ACONNECTABLEOBJECT)

DECLARE_PROTECT_FINAL_CONSTRUCT()

BEGIN_COM_MAP(CAConnectableObject)
    COM_INTERFACE_ENTRY(IAConnectableObject)
    COM_INTERFACE_ENTRY(IConnectionPointContainer)
    COM_INTERFACE_ENTRY_IMPL(IConnectionPointContainer)
END_COM_MAP()
BEGIN_CONNECTION_POINT_MAP(CAConnectableObject)
    CONNECTION_POINT_ENTRY(DIID_ _IAConnectableObjectEvents)
    CONNECTION_POINT_ENTRY(DIID_ _IAConnectableObjectEvents2)
END_CONNECTION_POINT_MAP()


// IAConnectableObject
public:
    STDMETHOD(Method2)();
    STDMETHOD(Method1)();
};

What's happening in this code? Notice that the code includes implementations of IAConnectableObject and IConnectionPointContainer (look for IAConnectableObject and IConnectionPointContainerImpl in the inheritance list). Notice that IAConnectableObject and IConnectionPointContainer are also in the interface map. Finally, notice the two proxy classes (CProxy_IAConnectableObjectEvents and CProxy_IAConnectableObjectEvents2) and the entries in the connection map for each connection point. These pieces of code represent ATL's implementation of connection points.

Using these tools from within Microsoft Visual C++ is fairly straightforward, and you can use them without understanding the underpinnings. However, you're always better off understanding how ATL implements connection points so that you can make important design decisions and have an easier time debugging the code. Let's take a look at what ATL is doing under the hood, starting with how IConnectionPointContainer is implemented.

ATL and IConnectionPointContainer

Remember that the basic premise behind COM is separating interfaces from their implementations. As long as the client gets back the interface (function table) that it requested through QueryInterface, the client is happy. That interface might point to some C++-based code, some Visual Basic_based code, some Delphi-based code, or whatever. The client doesn't care what happens behind the interface (as long as it works, of course). When some client code uses IConnectionPointContainer and IConnectionPoint pointers connected to a COM object implemented using ATL, the client is talking to some C++-based source code written using templates. ATL implements IConnectionPointContainer through a template named IConnectionPointContainerImpl.

IConnectionPointContainerImpl is parameterized with one parameter—the class implementing IConnectionPointContainer (that's the ATL-based class you're in the middle of implementing). Remember that the purpose of IConnectionPointContainer is to provide a way for clients to ask whether an object supports current outgoing interfaces (each represented by a separate IConnectionPoint interface).

IConnectionPointContainerImpl maintains a collection of IConnectionPoint interfaces using the ATL helper class CComEnum. Before diving into IConnectionPointContainerImpl, we need to examine a mechanism called connection maps, which ATL uses to maintain a collection of connection points. ATL's connection maps are implemented through a set of macros including BEGIN_CONNECTION_POINT_MAP, CONNECTION_POINT_ENTRY, and END_CONNECTION_POINT_MAP. For example, if you want to set up a list of connection points in the CConnectionObj class, you'd sandwich the CONNECTION_POINT_ENTRY between the BEGIN_CONNECTION_POINT_MAP and the END_CONNECTION_POINT_MAP, like this:

BEGIN_CONNECTION_POINT_MAP(CAConnectableObject)
    CONNECTION_POINT_ENTRY(DIID_ _IAConnectableObjectEvents)
    CONNECTION_POINT_ENTRY(DIID_ _IAConnectableObjectEvents2)
END_CONNECTION_POINT_MAP()

As with most of the maps described by macros in ATL (and MFC for that matter), ATL's connection map macros define a table. This time, the table simply represents a collection of offsets described by a structure named _ATL_CONNMAP_ENTRY:

struct _ATL_CONNMAP_ENTRY
{
    DWORD dwOffset;
};

BEGIN_CONNECTION_POINT_MAP defines a pointer to an array of _ATL_CONNMAP_ENTRY structures and a function for retrieving that pointer:

#define BEGIN_CONNECTION_POINT_MAP(x)\
    typedef x _atl_conn_classtype;\
    static const _ATL_CONNMAP_ENTRY* GetConnMap(int* pnEntries) {\
    static const _ATL_CONNMAP_ENTRY _entries[] = {

The _ATL_CONNMAP_ENTRY is simply an address that points to an IConnectionPoint interface.

The CONNECTION_POINT_ENTRY macro calculates the address of the pointer on the fly using a helper class named _ICPLocator, which performs a QueryInterface-style operation to find the pointer based on the GUID:

#define CONNECTION_POINT_ENTRY(iid){offsetofclass(ICPLocator<&iid>,
    _atl_conn_classtype)-\
    offsetofclass(IConnectionPointContainerImpl<_atl_conn_classtype>,
    _atl_conn_classtype)},

Finally, END_CONNECTION_POINT_MAP terminates the array of connection points.

#define END_CONNECTION_POINT_MAP() {(DWORD)-1} }; \
    if(pnEntries)*pnEntries =
        sizeof(_entries)/sizeof(_ATL_CONNMAP_ENTRY) _ 1;\
        return _entries;}

ATL's IConnectionPointContainerImpl implements EnumConnectionPoints by simply filling the connection point collection and passing back the IEnumConnectionPoint interface. IConnectionPointContainerImpl uses the connection map's GetConnPoint to retrieve the list of connection points and fill the collection of connection points.

IConnectionPointContainerImpl implements FindConnectionPoint by using the connection map's GetConnPoint to retrieve the list of connection points. FindConnectionPoint just rips through the list of connection points to find the requested connection point. When FindConnectionPoint locates the connection, the function passes back the connection point interface after calling AddRef through it. IConnectionPointContainer is fairly straightforward. All that's missing now is to see how ATL implements IConnectionPoint.

ATL and IConnectionPoint

The last item to examine within ATL's connection point machinery is the proxy classes used to call back to the client. The proxy is where ATL-based COM classes implement IConnectionPoint. Listing 12-4 shows the code for the IAConnectableObjectEvents proxy.

Listing 12-4. The connection proxy generated by the ATL Wizard.

template <class T>
class CProxy_IAConnectableObjectEvents : 
    public IConnectionPointImpl<T, 
        &DIID_ _IAConnectableObjectEvents, 
        CComDynamicUnkArray>
{
// Warning: this class may be re-created by the wizard.
public:
    VOID Fire_OnEvent1()
    {
        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();
            IDispatch* pDispatch =
                reinterpret_cast<IDispatch*>(sp.p);
            if(pDispatch != NULL)
            {
                DISPPARAMS disp = { NULL, NULL, 0, 0 };
                pDispatch->Invoke(0x1, IID_NULL, 
                    LOCALE_USER_DEFAULT, 
                    DISPATCH_METHOD, &disp, 
                    NULL, NULL, NULL);
            }
        }
    }

    VOID Fire_OnEvent2()
    {
        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();
            IDispatch* pDispatch =
                reinterpret_cast<IDispatch*>(sp.p);
            if(pDispatch != NULL)
            {
                DISPPARAMS disp = { NULL, NULL, 0, 0 };
                pDispatch->Invoke(0x2, IID_NULL, 
                    LOCALE_USER_DEFAULT, 
                    DISPATCH_METHOD, &disp, 
                    NULL, NULL, NULL);
            }
        }
    }
};

The ATL Wizard took a look at the object's type library to learn about the outgoing interfaces. IAConnectableObjectEvents is the first outgoing interface listed in the type library. IAConnectableObjectEvents is a dispatch interface with two functions: Event1 and Event2.

ATL implements IConnectionPoint through a templatized class named IConnectionPointImpl. Look back at the ATL proxy in Listing 12-2 and notice that it derives from IConnectionPointImpl. IConnectionPointImpl's template parameters include the class implementing IConnectionPoint (the proxy class), the GUID of the connection point, and a class that manages the connections.

IConnectionPointImpl implements the individual connection points of an ATL-based COM class. IConnectionPointImpl doesn't have much state—it maintains the GUID identifying the connection point and a collection of IUnknown interfaces that the object uses to call back to the client. That's really all the state required for implementing a connection point. The rest of IConnectionPointImpl is implemented as a set of function templates. The two most important functions of IConnectionPointImpl are Advise and Unadvise. When a client wants to subscribe to callbacks, the client calls IConnectionPoint::Advise, passing in an unknown pointer. IConnectionPointImpl implements Advise by inserting the unknown pointer into the collection of callback interfaces and returning the vector position in the pdwCookie parameter.

Clients use IConnectionPoint::Unadvise to stop receiving callbacks. IConnectionPointImpl implements Unadvise by looking up the unknown pointer using the dwCookie parameter, which happens to be the index into the collection of unknown pointers. If Unadvise finds the unknown pointer in the vector, Unadvise removes the pointer from the advise list and then releases the pointer.

The rest of the IConnectionPoint functions (GetConnectionInterface, GetConnectionPointContainer, and EnumConnections) aren't used as often. Even so, IConnectionPoint implements them just to make sure the interface implementation contract is complete. IConnectionPointImpl implements GetConnectionInterface by simply returning the GUID representing the connection point. IConnectionPointImpl maintains a pointer to the connection point container class (which was passed in as a template parameter). IConnectionPointImpl implements GetConnectionPointContainer by casting that pointer as IConnectionPointContainerImpl to return the IConnectionPointContainer vtable. (This, of course, assumes the IConnectionPointContainer class is derived from IConnectionPointContainerImpl.) Finally, IConnectionPointImpl implements EnumConnections by filling a CComDynamicUnkArray-based class with the unknown pointers known by the object and passing back the IEnumConnections interface implemented by the CComEnum-based class.

Notice that the proxy classes simply generate calls back to the client by wrapping an IDispatch-based interface that the client provides. The proxy sets up all the IDispatch cruft so that you don't need to do it by hand. When it comes time to fire either OnEvent1 or OnEvent2, you simply need to call the Fire_OnEvent1 and Fire_OnEvent2 members of the CProxy_IAConnectableObjectEvents class as shown here:

class ATL_NO_VTABLE CAConnectableObject : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public IDispatchImpl<IAConnectableObject, 
        &IID_IAConnectableObject, 
        &LIBID_ACONNECTABLESVRLib>,
    public CComControl<CAConnectableObject>,
    public CComCoClass<CAConnectableObject,
        &CLSID_AConnectableObject>,
    public CProxy_IAConnectableObjectEvents< CAConnectableObject >,
    public CProxy_IAConnectableObjectEvents2< CAConnectableObject >
{
public:
    CAConnectableObject()
    {
    }

DECLARE_REGISTRY_RESOURCEID(IDR_ACONNECTABLEOBJECT)

DECLARE_PROTECT_FINAL_CONSTRUCT()

BEGIN_COM_MAP(CAConnectableObject)
    COM_INTERFACE_ENTRY(IAConnectableObject)
    COM_INTERFACE_ENTRY(IDispatch)
    COM_INTERFACE_ENTRY_IMPL(IConnectionPointContainer)
END_COM_MAP()

BEGIN_CONNECTION_POINT_MAP(CAConnectableObject)
    CONNECTION_POINT_ENTRY(IID_IPropertyNotifySink)
    CONNECTION_POINT_ENTRY(DIID_ _IAConnectableObjectEvents)
    CONNECTION_POINT_ENTRY(DIID_ _IAConnectableObjectEvents2)
END_CONNECTION_POINT_MAP()

// IAConnectableObject
public:

    STDMETHOD(Method2)();
    STDMETHOD(Method1)();

    Void TestEvents()
    {
        Fire_OnEvent1();
        Fire_OnEvent2();
    }
};