COM连接点 - CComDynamicUnkArray::Add问题(5)

时间:2022-08-30 21:36:12

无意中发现一个有趣的问题,连接点中用于保存接收器对象的容器CComDynamicUnkArray有一些小问题。

calloc和_recalloc的参数传递问题

首先看一下连接点的声明:

template <class T, const IID* piid, class CDV = CComDynamicUnkArray >
class ATL_NO_VTABLE IConnectionPointImpl :
public _ICPLocator<piid>
{
typedef CComEnum<IEnumConnections, &__uuidof(IEnumConnections), CONNECTDATA,
_Copy<CONNECTDATA> > CComEnumConnections;
typedef CDV _CDV;
public:
~IConnectionPointImpl();
STDMETHOD(_LocCPQueryInterface)(
_In_ REFIID riid,
_COM_Outptr_ void** ppvObject)
{
#ifndef _ATL_OLEDB_CONFORMANCE_TESTS
ATLASSERT(ppvObject != NULL);
#endif
if (ppvObject == NULL)
return E_POINTER;
*ppvObject = NULL;

if (InlineIsEqualGUID(riid, __uuidof(IConnectionPoint)) || InlineIsEqualUnknown(riid))
{
*ppvObject = this;
AddRef();
#if defined(_ATL_DEBUG_INTERFACES) && !defined(_ATL_STATIC_LIB_IMPL)
_AtlDebugInterfacesModule.AddThunk((IUnknown**)ppvObject, _T("IConnectionPointImpl"), riid);
#endif // _ATL_DEBUG_INTERFACES
return S_OK;
}
else
return E_NOINTERFACE;
}

STDMETHOD(GetConnectionInterface)(_Out_ IID* piid2)
{
if (piid2 == NULL)
return E_POINTER;
*piid2 = *piid;
return S_OK;
}
STDMETHOD(GetConnectionPointContainer)(
_Outptr_ IConnectionPointContainer** ppCPC)
{
T* pT = static_cast<T*>(this);
// No need to check ppCPC for NULL since QI will do that for us
return pT->QueryInterface(__uuidof(IConnectionPointContainer), (void**)ppCPC);
}
STDMETHOD(Advise)(
_Inout_ IUnknown* pUnkSink,
_Out_ DWORD* pdwCookie);
STDMETHOD(Unadvise)(_In_ DWORD dwCookie);
STDMETHOD(EnumConnections)(_COM_Outptr_ IEnumConnections** ppEnum);

CDV m_vec;
};

我们可以看到IConnectionPointImpl是一个模板类,它有一个成员:CDV m_vec;

CDV是一个模板参数,它有一个默认值CComDynamicUnkArray,通常大家用的都是这个默认值CComDynamicUnkArray。

看一下CComDynamicUnkArray::Add函数:

inline DWORD CComDynamicUnkArray::Add(_In_ IUnknown* pUnk)
{
IUnknown** pp = NULL;
if (m_nSize == 0)
{
// Create array with _DEFAULT_VECTORLENGTH number of items.
ATLTRY(pp = (IUnknown**)calloc(sizeof(IUnknown*),_DEFAULT_VECTORLENGTH));
if (pp == NULL)
return 0;
memset(pp, 0, sizeof(IUnknown*)*_DEFAULT_VECTORLENGTH);
m_ppUnk = pp;
m_nSize = _DEFAULT_VECTORLENGTH;
}
// Walk array and use empty slots if any.
DWORD dwCookie = 1;
for (pp = begin(); pp < end(); pp++)
{
if (*pp == NULL)
{
*pp = pUnk;
return dwCookie; // cookie minus one is index into array
}
dwCookie++;
}
// No empty slots so resize array.
// # of new slots is double of current size.
int nAlloc = 0;
HRESULT hr = AtlMultiply(&nAlloc, m_nSize, 2);
if (FAILED(hr))
{
return 0;
}

pp = (IUnknown**)_recalloc(m_ppUnk, sizeof(IUnknown*),nAlloc);
if (pp == NULL)
return 0;
m_ppUnk = pp;
memset(&m_ppUnk[m_nSize], 0, sizeof(IUnknown*)*m_nSize);
m_ppUnk[m_nSize] = pUnk;
dwCookie = m_nSize+1;
m_nSize = nAlloc;
return dwCookie; // cookie minus one is index into array
}

注意:

ATLTRY(pp = (IUnknown**)calloc(sizeof(IUnknown*),_DEFAULT_VECTORLENGTH));

pp = (IUnknown**)_recalloc(m_ppUnk, sizeof(IUnknown*),nAlloc);

是不是参数传错了?

calloc的原型是:

void *calloc(
   size_t num,
   size_t size
);
_recalloc的原型是:

void *_recalloc(
   void *memblock
   size_t num,
   size_t size
);

上面两行代码明明参数传反了啊。calloc的第一个参数是元素的个数,第二个参数是每个元素的大小。可是Add()函数里面,反过来了。_recalloc也是这个问题。

从函数调用来说肯定是错了。

但是会不会导致问题呢?如果是,岂不是很危险?

为了让自己放心,我跟进去calloc函数,发现calloc会调用这个函数:

extern "C" void * __cdecl _calloc_dbg_impl(
size_t nNum,
size_t nSize,
int nBlockUse,
const char * szFileName,
int nLine,
int * errno_tmp
)
{
void * pvBlk;

/* ensure that (nSize * nNum) does not overflow */
if (nNum > 0)
{
_VALIDATE_RETURN_NOEXC((_HEAP_MAXREQ / nNum) >= nSize, ENOMEM, NULL);
}

nSize *= nNum;

/*
* try to malloc the requested space
*/

pvBlk = _nh_malloc_dbg_impl(nSize, _newmode, nBlockUse, szFileName, nLine, errno_tmp);

/*
* If malloc() succeeded, initialize the allocated space to zeros.
* Note that unlike _calloc_base, exactly nNum bytes are set to zero.
*/

if ( pvBlk != NULL )
{
memset(pvBlk, 0, nSize);
}

RTCCALLBACK(_RTC_Allocate_hook, (pvBlk, nSize, 0));

return(pvBlk);
}

我们会看到这一行代码:nSize *= nNum;

然后nSize被传入了_nh_malloc_dbg_imple,那么就是说元素个数和每个元素大小会被乘起来,然后分配内存。

这样的话,calloc参数位置搞错好像也没有什么问题。

不管怎么样,ATLTRY(pp = (IUnknown**)calloc(sizeof(IUnknown*),_DEFAULT_VECTORLENGTH));肯定是参数传错了,但是它不会导致问题。
_recalloc的问题也差不多。

它跑到这里了:

extern "C" _CRTIMP void * __cdecl _recalloc_dbg
(
void * memblock,
size_t count,
size_t size,
int nBlockUse,
const char * szFileName,
int nLine
)
{
size_t size_orig = 0, old_size = 0;
void * retp = NULL;

/* ensure that (size * count) does not overflow */
if (count > 0)
{
_VALIDATE_RETURN_NOEXC((_HEAP_MAXREQ / count) >= size, ENOMEM, NULL);
}
size_orig = size * count;

if (memblock != NULL)
{
old_size = _msize((void*)memblock);
}

retp = _realloc_dbg(memblock, size_orig, nBlockUse, szFileName, nLine);

if (retp != NULL && old_size < size_orig)
{
memset ((char*)retp + old_size, 0, size_orig - old_size);
}
return retp;
}

看这个size_orig = size * count;,也是乘起来去分配内存,那么位置搞错也没有什么关系了。

OK,结论就是:

1. calloc参数传错了,但是不会导致问题

2. _recalloc参数也传错了,但是不会导致问题。


有关_recalloc的另外一个问题,如果_recalloc需要分配更多的内存,而原来的那个地址又不能扩展了,那又如何?

_recalloc位置变换

首先看容器的Add()函数里面有这么一行:

pp = (IUnknown**)_recalloc(m_ppUnk, sizeof(IUnknown*),nAlloc);

m_ppUnk指向原来的一个位置,比如地址是A,它现在已经有1024个字节了,然后又需要分配4096个字节。这个时候有两种情况:

1. A地址后面还有空闲的空间可以分配出4096个字节,那么返回值pp也指向A

2. A地址后面不足4096字节,比如2048以后的空间已经分配给其他用途了,那么pp就可能指向另外一个地址,并不是A了。也就是说系统重新在另外一个地方开辟了空间用来存放1024 + 4096个字节。

如果#2发生了,那么会不会有个问题就是:原来的1024个字节内存上存放的信息就没有了?

看了一下MSDN对于_recallo的介绍 http://msdn.microsoft.com/en-us/library/ms235322.aspx

A combination of realloc and calloc. Reallocates an array in memory and initializes its elements to 0.

如果所有的内存都被清零了,那原来的信息岂不是丢了?

做个简单测试,写下如下代码:

    void* p = _recalloc(NULL, 1, 1024);
memset(p, 'A', 1024);
void* p2 = _recalloc(p, 4, 1024);

运行一下,发现p和p2的地址不一样。

COM连接点 - CComDynamicUnkArray::Add问题(5)

这个说明_recalloc的时候确实是新开辟了一个空间(#2).

然后看看p2,0x00700aa8指向的内存:

COM连接点 - CComDynamicUnkArray::Add问题(5)

最后一个A是0x700EA7 - 0x700AA8 + 1 = 0x400, 也就是十进制的1024个字节。这么说,好像是在开辟新空间的时候,原来的信息还是copy过来了,后面新增长的就全市0x00了。那是不是说_recalloc如果发现原来的地址不能分配出更多内存,而需要在另外一个地方分配的时候,会把原来的数据都copy到新的地方呢?

看了一下MSDN对于_recalloc的说明,好像也没有发现对于这一部分的描述。但是看到_recalloc是由realloc和calloc组成的。查看了一下realloc函数,发现有这么一句话:

The contents of the block are unchanged up to the shorter of the new and old sizes, although the new block can be in a different location.

其实前面半句,我也不是很明白,是不是说:块的内容不会发生变化除非新长度比旧的长度来的短,但是新的块可以是在另外一个地方。

基本应该没错。

那么这样的话,就算#2发生了,也不要紧,旧的数据还是会被copy过去的,然后新增长的内存就统统被清零了。


OK, CComDynamicUnkArray::Add的问题就讲完了。

另外,如果我们不喜欢用CComDynamicUnkArray,还可以用自定义的容器,

template <class T, const IID* piid, class CDV = CComDynamicUnkArray >
class ATL_NO_VTABLE IConnectionPointImpl :
 public _ICPLocator<piid>

自定义一个容器,然后传给IConnectionPointImpl就是了,第三个模板。

CComDynamicUnkArray是用C写的,我们完全可以自己写个容器,可以用STL的vector。

比如写个类,叫做CMyArray,然后实现CComDynamicUnkArray的每一个函数,可以直接用std::vector来存放。

实际上CMyArray就是个adapter。将std::vector适配一下,这样就可以在IConnectionPointImpl里面使用。这也就是适配器模式的一个应用。