From: http://blog.csdn.net/zj510/article/details/39056097
连接点,connection point,在COM里面也是挺重要的。简单讲,COM里面的连接点就好像是C语言的回调函数,只不过它是基于面向对象实现的。连接点的作用也就是COM对象将一些事件通知客户(调用者)。
先来做一个简单的连接点例子吧,之后再慢慢探讨。
用ATL创建一个工程(DLL COM),取名MyCom,
然后创建一个COM接口,IMyCar. 在options那里选上connection points。
然后在class view那里找到_IMyCarEvents(在MyComLib下面),右键点击Add Method.如图,增加一个方法OnStop。
增加完之后,可以看到IDL文件里面多了个方法,如下图:
- library MyComLib
- {
- importlib("stdole2.tlb");
- [
- uuid(2CF347A8-63ED-4CE0-8A6D-F98D60C98B8C)
- ]
- dispinterface _IMyCarEvents
- {
- properties:
- methods:
- [id(1)] HRESULT OnStop([in] FLOAT Distance);
- };
- [
- uuid(DA6770F3-CBB6-4F34-A137-2B02A27AB219)
- ]
- coclass MyCar
- {
- [default] interface IMyCar;
- [default, source] dispinterface _IMyCarEvents;
- };
- };
library MyComLib
{
importlib("stdole2.tlb");
[
uuid(2CF347A8-63ED-4CE0-8A6D-F98D60C98B8C)
]
dispinterface _IMyCarEvents
{
properties:
methods:
[id(1)] HRESULT OnStop([in] FLOAT Distance);
};
[
uuid(DA6770F3-CBB6-4F34-A137-2B02A27AB219)
]
coclass MyCar
{
[default] interface IMyCar;
[default, source] dispinterface _IMyCarEvents;
};
};
之后在CMyCar上点击右键,选择Add Connection Point。
选择_IMyCarEvents,点击>按钮。然后Finish.
这样,我们就增加了一个连接点,在CProxy_IMyCarEvents里面可以看到多了个函数。如下
- #pragma once
- template<class T>
- class CProxy_IMyCarEvents :
- public ATL::IConnectionPointImpl<T, &__uuidof(_IMyCarEvents)>
- {
- public:
- HRESULT Fire_OnStop(FLOAT Distance)
- {
- HRESULT hr = S_OK;
- T * pThis = static_cast<T *>(this);
- int cConnections = m_vec.GetSize();
- for (int iConnection = 0; iConnection < cConnections; iConnection++)
- {
- pThis->Lock();
- CComPtr<IUnknown> punkConnection = m_vec.GetAt(iConnection);
- pThis->Unlock();
- IDispatch * pConnection = static_cast<IDispatch *>(punkConnection.p);
- if (pConnection)
- {
- CComVariant avarParams[1];
- avarParams[0] = Distance;
- avarParams[0].vt = VT_R4;
- CComVariant varResult;
- DISPPARAMS params = { avarParams, NULL, 1, 0 };
- hr = pConnection->Invoke(1, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, ¶ms, &varResult, NULL, NULL);
- }
- }
- return hr;
- }
- };
#pragma once
template<class T>
class CProxy_IMyCarEvents :
public ATL::IConnectionPointImpl<T, &__uuidof(_IMyCarEvents)>
{
public:
HRESULT Fire_OnStop(FLOAT Distance)
{
HRESULT hr = S_OK;
T * pThis = static_cast<T *>(this);
int cConnections = m_vec.GetSize();
for (int iConnection = 0; iConnection < cConnections; iConnection++)
{
pThis->Lock();
CComPtr<IUnknown> punkConnection = m_vec.GetAt(iConnection);
pThis->Unlock();
IDispatch * pConnection = static_cast<IDispatch *>(punkConnection.p);
if (pConnection)
{
CComVariant avarParams[1];
avarParams[0] = Distance;
avarParams[0].vt = VT_R4;
CComVariant varResult;
DISPPARAMS params = { avarParams, NULL, 1, 0 };
hr = pConnection->Invoke(1, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, ¶ms, &varResult, NULL, NULL);
}
}
return hr;
}
};
现在来看看怎么使用这个连接点吧。
先个IMyCar增加一个方法,
- STDMETHODIMP CMyCar::Run()
- {
- // TODO: Add your implementation code here
- this->Fire_OnStop(1000);
- return S_OK;
- }
STDMETHODIMP CMyCar::Run()
{
// TODO: Add your implementation code here
this->Fire_OnStop(1000);
return S_OK;
}
然后写个客户程序,如下:
- // TestCom.cpp : Defines the entry point for the console application.
- //
- #include "stdafx.h"
- #include <atlbase.h>
- #include <atlcom.h>
- #include "../MyCOM/MyCOM_i.h"
- #include "../MyCOM/MyCOM_i.c"
- #include <iostream>
- #include <thread>
- using namespace std;
- class CSink :
- public CComObjectRoot,
- public _IMyCarEvents
- {
- BEGIN_COM_MAP(CSink)
- COM_INTERFACE_ENTRY(IDispatch)
- COM_INTERFACE_ENTRY(_IMyCarEvents)
- END_COM_MAP()
- public:
- virtual ~CSink(){}
- STDMETHODIMP GetTypeInfoCount(UINT *pctinfo) { return E_NOTIMPL; }
- STDMETHODIMP GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo) { return E_NOTIMPL; }
- STDMETHODIMP GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId) { return E_NOTIMPL; }
- STDMETHODIMP Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)
- {
- printf("sink, id: %d, parm: %f", dispIdMember, pDispParams->rgvarg[0].fltVal);
- return S_OK;
- }
- };
- CComModule m_commodule;
- int _tmain(int argc, _TCHAR* argv[])
- {
- CoInitializeEx(0, COINIT_APARTMENTTHREADED);
- {
- CComPtr<IMyCar> spCar;
- spCar.CoCreateInstance(CLSID_MyCar, NULL, CLSCTX_INPROC_SERVER);
- CComObject<CSink>* sinkptr = nullptr;
- CComObject<CSink>::CreateInstance(&sinkptr);
- DWORD cookies = 0;
- AtlAdvise(spCar, sinkptr, __uuidof(_IMyCarEvents), &cookies);
- spCar->Run();
- }
- CoUninitialize();
- return 0;
- }
// TestCom.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <atlbase.h>
#include <atlcom.h>
#include "../MyCOM/MyCOM_i.h"
#include "../MyCOM/MyCOM_i.c"
#include <iostream>
#include <thread>
using namespace std;
class CSink :
public CComObjectRoot,
public _IMyCarEvents
{
BEGIN_COM_MAP(CSink)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(_IMyCarEvents)
END_COM_MAP()
public:
virtual ~CSink(){}
STDMETHODIMP GetTypeInfoCount(UINT *pctinfo) { return E_NOTIMPL; }
STDMETHODIMP GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo) { return E_NOTIMPL; }
STDMETHODIMP GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId) { return E_NOTIMPL; }
STDMETHODIMP Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)
{
printf("sink, id: %d, parm: %f", dispIdMember, pDispParams->rgvarg[0].fltVal);
return S_OK;
}
};
CComModule m_commodule;
int _tmain(int argc, _TCHAR* argv[])
{
CoInitializeEx(0, COINIT_APARTMENTTHREADED);
{
CComPtr<IMyCar> spCar;
spCar.CoCreateInstance(CLSID_MyCar, NULL, CLSCTX_INPROC_SERVER);
CComObject<CSink>* sinkptr = nullptr;
CComObject<CSink>::CreateInstance(&sinkptr);
DWORD cookies = 0;
AtlAdvise(spCar, sinkptr, __uuidof(_IMyCarEvents), &cookies);
spCar->Run();
}
CoUninitialize();
return 0;
}
跑一下,发现CSink里面的invoke被调用到了。
这样,我们就有了一个简单的连接点例子。其中pDispParams->rgvarg[0].fltVal就是COM对象触发这个连接点的时候传进来的参数1000.
Sink类
其实,这个东西比较固定,反正新手那么写就行了。其他的都可以忽略,主要就是Invoke函数,当COM对象fire一个事件的时候,Invoke函数会被调用。然后在这个函数里面处理可以获取事件的id:dispIdMember以及参数。这样调用者,也就是事件的接收者就可以处理这些事件了,并且返回结果。
如何创建和挂载sink
首先,我们需要创建一个sink实例。就通过下面2行。先声明一个null指针,然后调用CComObject<CSink>::CreateInstance来创建一个sink对象。等CreateInstance返回后,成功的话,sinkptr就指向一个CComSink<CSink>对象。
- CComObject<CSink>* sinkptr = nullptr;
- CComObject<CSink>::CreateInstance(&sinkptr);
CComObject<CSink>* sinkptr = nullptr;
CComObject<CSink>::CreateInstance(&sinkptr);
然后将这个sink对象挂载到对应的COM对象上。下面的2行代码就是将sinkptr挂载到spCar对象上面。cookies的作用查看MSDN.简单讲cookies是一个传出参数,代表这次挂载,这个值是唯一的。如果想取消这个挂载,就可以用这个cookies和AtlUnadvise来取消连接。
- DWORD cookies = 0;
- AtlAdvise(spCar, sinkptr, __uuidof(_IMyCarEvents), &cookies);
DWORD cookies = 0;
AtlAdvise(spCar, sinkptr, __uuidof(_IMyCarEvents), &cookies);
细心一点的同学可能会问,这个地方是不是有个内存泄漏?
sinkptr指向了一个CComObject<CSink>对象,然后sinkptr本身死亡前又没有释放,那岂不是内存泄漏?
这是个很好的问题,我们先看一下AtlAdvise的实现:
- ATLINLINE ATLAPI AtlAdvise(
- _Inout_ IUnknown* pUnkCP,
- _Inout_opt_ IUnknown* pUnk,
- _In_ const IID& iid,
- _Out_ LPDWORD pdw)
- {
- if(pUnkCP == NULL)
- return E_INVALIDARG;
- CComPtr<IConnectionPointContainer> pCPC;
- CComPtr<IConnectionPoint> pCP;
- HRESULT hRes = pUnkCP->QueryInterface(__uuidof(IConnectionPointContainer), (void**)&pCPC);
- if (SUCCEEDED(hRes))
- hRes = pCPC->FindConnectionPoint(iid, &pCP);
- if (SUCCEEDED(hRes))
- hRes = pCP->Advise(pUnk, pdw);
- return hRes;
- }
ATLINLINE ATLAPI AtlAdvise(
_Inout_ IUnknown* pUnkCP,
_Inout_opt_ IUnknown* pUnk,
_In_ const IID& iid,
_Out_ LPDWORD pdw)
{
if(pUnkCP == NULL)
return E_INVALIDARG;
CComPtr<IConnectionPointContainer> pCPC;
CComPtr<IConnectionPoint> pCP;
HRESULT hRes = pUnkCP->QueryInterface(__uuidof(IConnectionPointContainer), (void**)&pCPC);
if (SUCCEEDED(hRes))
hRes = pCPC->FindConnectionPoint(iid, &pCP);
if (SUCCEEDED(hRes))
hRes = pCP->Advise(pUnk, pdw);
return hRes;
}
我们可以看到pUnk被传到了pCP->Advise(pUnk, pdw);里面。再看看相关代码:
- template <class T, const IID* piid, class CDV>
- IConnectionPointImpl<T, piid, CDV>::~IConnectionPointImpl()
- {
- IUnknown** pp = m_vec.begin();
- while (pp < m_vec.end())
- {
- if (*pp != NULL)
- (*pp)->Release();
- pp++;
- }
- }
- template <class T, const IID* piid, class CDV>
- STDMETHODIMP IConnectionPointImpl<T, piid, CDV>::Advise(
- _Inout_ IUnknown* pUnkSink,
- _Out_ DWORD* pdwCookie)
- {
- T* pT = static_cast<T*>(this);
- IUnknown* p;
- HRESULT hRes = S_OK;
- if (pdwCookie != NULL)
- *pdwCookie = 0;
- if (pUnkSink == NULL || pdwCookie == NULL)
- return E_POINTER;
- IID iid;
- GetConnectionInterface(&iid);
- hRes = pUnkSink->QueryInterface(iid, (void**)&p);
- if (SUCCEEDED(hRes))
- {
- pT->Lock();
- *pdwCookie = m_vec.Add(p);
- hRes = (*pdwCookie != NULL) ? S_OK : CONNECT_E_ADVISELIMIT;
- pT->Unlock();
- if (hRes != S_OK)
- p->Release();
- }
- else if (hRes == E_NOINTERFACE)
- hRes = CONNECT_E_CANNOTCONNECT;
- if (FAILED(hRes))
- *pdwCookie = 0;
- return hRes;
- }
template <class T, const IID* piid, class CDV>
IConnectionPointImpl<T, piid, CDV>::~IConnectionPointImpl()
{
IUnknown** pp = m_vec.begin();
while (pp < m_vec.end())
{
if (*pp != NULL)
(*pp)->Release();
pp++;
}
}
template <class T, const IID* piid, class CDV>
STDMETHODIMP IConnectionPointImpl<T, piid, CDV>::Advise(
_Inout_ IUnknown* pUnkSink,
_Out_ DWORD* pdwCookie)
{
T* pT = static_cast<T*>(this);
IUnknown* p;
HRESULT hRes = S_OK;
if (pdwCookie != NULL)
*pdwCookie = 0;
if (pUnkSink == NULL || pdwCookie == NULL)
return E_POINTER;
IID iid;
GetConnectionInterface(&iid);
hRes = pUnkSink->QueryInterface(iid, (void**)&p);
if (SUCCEEDED(hRes))
{
pT->Lock();
*pdwCookie = m_vec.Add(p);
hRes = (*pdwCookie != NULL) ? S_OK : CONNECT_E_ADVISELIMIT;
pT->Unlock();
if (hRes != S_OK)
p->Release();
}
else if (hRes == E_NOINTERFACE)
hRes = CONNECT_E_CANNOTCONNECT;
if (FAILED(hRes))
*pdwCookie = 0;
return hRes;
}
我们可以看到pUnkSink被放到了m_vec里面(当然,如果中间过程出错的话,直接被release了)。然后在析构函数里面,我们可以看到m_vec里面的所有元素都会调用release来释放。
而spCar的实现类CMyCar有个基类叫做CProxy_IMyCarEvents,CProxy_IMyCarEvents有个基类是IConnectionPointImpl。
那么当spCar在析构的时候,自然就会调用IConnectionPointImpl的析构函数。也就是说会把所有sink对象全部释放掉。
换句话说,一旦将sink对象成功挂载到了COM对象,那么sink对象的生命周期就由对应的COM对象来管理。
我们可以做个简单测试,在上面的例子代码里面,当spCar析构的时候,也就是代码要跑出大括号(spCar->Run();下面)的时候,sink对象也应该被释放。跟了一把,确实是这样。
ok,我们现在不用再担心内存泄漏问题了。一旦挂载成功,sink对象就托管给对应的COM对象了,如果对应的COM对象析构了,那么所有它管理的sink对象也就释放了。
上面的例子,其实很傻,它的主要过程是:
1. 创建sink对象
2. 挂载sink对象到一个COM对象上
3. COM对象调用Run函数
4. Run函数内部触发了一个连接点
5. 调用者的对应函数会被调用,这里是Invoke。
细细看上面的例子,其实Invoke是在Run函数中被调用的。这个好像没有什么意义。真正的回调应该是调用者调用完之后做其他事情了,然后等有回调来的时候,对应的函数会被触发。
anyway,本文中的例子是个非常简单的例子,只是为了展示一个连接点。连接点的威力根本没有发挥,至于真正的连接点应该怎样?待续。。。