COM编程攻略(六 COM模块的构成)

特别说明 —— 该系列文章皆转自知乎作者 Froser 的COM编程攻略:https://www.zhihu.com/column/c_1234485736897552384



前言

上一篇我们讲了,COM API的CoCreateInstance, CoCreateInstanceEx是如何工作的,以及我们如何编写一个类,如何在注册表中注册它的信息。

到此为止,我们已经了解到了如何实现一个COM对象,如何创建一个COM类,如何注册它,将它暴露给客户。至此,我们已经能够编写一个完完全全的COM模块了。

所以,这一篇,我们将总结一下,一个COM模块包含了哪些内容,需要实现哪些方法。


一、模块需要实现的方法

  1. 实现DllGeClassObject
STDAPI DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID* ppv);

这个我们上一篇讲过,在调用CoGetClassObject,或者CoCreateInstance时,dll中的这个导出函数会被调用,负责返回一个类出去。

  1. DllRegisterServer, DllUnregisterServer, DllInstall
STDAPI DllRegisterServer(void);
STDAPI DllUnregisterServer(void);
STDAPI DllInstall(BOOL bInstall, _In_opt_  LPCWSTR pszCmdLine);

这3个都是regsvr32.exe中会触发的导出函数。前2个我们上一篇说了,主要是注册、反注册相关注册表信息。第三个DllInstall相当于它们两者的结合,用bInstall表示是否是安装流程。实现可以如下:

STDAPI DllInstall(BOOL bInstall, _In_opt_  LPCWSTR pszCmdLine)
{
	HRESULT hr = E_FAIL;
	static const wchar_t szUserSwitch[] = L"user";

	if (pszCmdLine != nullptr)
	{
		if (_wcsnicmp(pszCmdLine, szUserSwitch, _countof(szUserSwitch)) == 0)
		{
			ATL::AtlSetPerUserRegistration(true);
		}
	}

	if (bInstall)
	{
		hr = DllRegisterServer();
		if (FAILED(hr))
		{
			DllUnregisterServer();
		}
	}
	else
	{
		hr = DllUnregisterServer();
	}

	return hr;
}
  1. DllCanUnloadNow
STDAPI DllCanUnloadNow(void);

这个方法之前我们没有说过。我们知道,我们写的COM模块(dll)是由COM服务器加载起来。当客户调用CoFreeUnusedLibraries时,COM服务器会询问每一个加载的COM模块,也就是调用每个COM模块的DllCanUnloadNow。如果它返回S_OK,说明这个dll能够被卸载,那么它就会被释放。如果不希望被释放,那么就返回S_FALSE。

判定一个dll是否能被卸载,最直观的方式,就是这个dll中没有存在任何活动的COM对象。因此,我们可以使用引用计数的方式:

LONG g_cLocks = 0;
void LockModule(void) { g_cLocks++; }
void UnlockModule(void) { g_cLocks--; }
STDAPI DllCanUnloadNow(void)
{
  return g_cLocks == 0 ? S_OK : S_FALSE;
}

在任何对象从无到有被创建时,我们调用LockModule,在对象被释放的时候,我们调用UnlockModule。

STDMETHODIMP_(ULONG) CYourClass::AddRef(void)
{
  if (m_cRef == 0) LockModule();
  return ++m_cRef;
}

STDMETHODIMP_(ULONG) CYourClass::Release(void)
{
  LONG res = --m_cRef;
  if (res == 0)
  {
    UnlockModule();
    delete this;
  }
  return res;
}

二、ATL组织模块的方式

如果我们用VS来生成一个ATL工程,那么它自动可以为我们生成上面5个函数。我们可以摘抄一下它生成的代码:

using namespace ATL;

// 用于确定 DLL 是否可由 OLE 卸载。
_Use_decl_annotations_
STDAPI DllCanUnloadNow(void)
{
	return _AtlModule.DllCanUnloadNow();
}

// 返回一个类工厂以创建所请求类型的对象。
_Use_decl_annotations_
STDAPI DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID* ppv)
{
	return _AtlModule.DllGetClassObject(rclsid, riid, ppv);
}

// DllRegisterServer - 向系统注册表中添加项。
_Use_decl_annotations_
STDAPI DllRegisterServer(void)
{
	// 注册对象、类型库和类型库中的所有接口
	HRESULT hr = _AtlModule.DllRegisterServer();
	return hr;
}

// DllUnregisterServer - 移除系统注册表中的项。
_Use_decl_annotations_
STDAPI DllUnregisterServer(void)
{
	HRESULT hr = _AtlModule.DllUnregisterServer();
	return hr;
}

// DllInstall - 按用户和计算机在系统注册表中逐一添加/移除项。
STDAPI DllInstall(BOOL bInstall, _In_opt_  LPCWSTR pszCmdLine)
{
	HRESULT hr = E_FAIL;
	static const wchar_t szUserSwitch[] = L"user";

	if (pszCmdLine != nullptr)
	{
		if (_wcsnicmp(pszCmdLine, szUserSwitch, _countof(szUserSwitch)) == 0)
		{
			ATL::AtlSetPerUserRegistration(true);
		}
	}

	if (bInstall)
	{
		hr = DllRegisterServer();
		if (FAILED(hr))
		{
			DllUnregisterServer();
		}
	}
	else
	{
		hr = DllUnregisterServer();
	}

	return hr;
}

除了DllInstall之外,其他4个函数均转调了_AtlModule全局变量。现在我们依次来看它们都做了些什么。

1. DllCanUnloadNow

全局变量_AtlModule继承于ATL::CAtlDllModuleT,我们可以在里面找到它转调的方法。

// ATL::CAtlDllModuleT::DllCanUnloadNow
HRESULT DllCanUnloadNow() throw()
{
	T* pT = static_cast<T*>(this);
	return (pT->GetLockCount()==0) ? S_OK : S_FALSE;
}

DllCanUnloadNow,和我们说的方法一致,它持有一个引用计数。它在基类CAtlModule中被实现。

在CComObject的实现中:

template <class Base>
class CComObject :
	public Base
{
public:
	typedef Base _BaseClass;
	CComObject(_In_opt_ void* = NULL)
	{
		_pAtlModule->Lock();
	}
	// Set refcount to -(LONG_MAX/2) to protect destruction and
	// also catch mismatched Release in debug builds
	virtual ~CComObject()
	{
		this->m_dwRef = -(LONG_MAX/2);
		this->FinalRelease();
#if defined(_ATL_DEBUG_INTERFACES) && !defined(_ATL_STATIC_LIB_IMPL)
		_AtlDebugInterfacesModule.DeleteNonAddRefThunk(_GetRawUnknown());
#endif
		_pAtlModule->Unlock();
	}
};

对象进入构造就会将模块的引用计数+1,析构时-1。虽然时机和我们刚刚实现的版本不同,不过原理都是一样的。

2. DllGetClassObject

这个方法最终调入了AtlComModuleGetClassObject

ATLINLINE ATLAPI AtlComModuleGetClassObject(
	_Inout_ _ATL_COM_MODULE* pComModule,
	_In_ REFCLSID rclsid,
	_In_ REFIID riid,
	_COM_Outptr_ LPVOID* ppv)
{
	if (ppv == NULL)
	{
		return E_POINTER;
	}

	*ppv = NULL;

	ATLASSERT(pComModule != NULL);
	if (pComModule == NULL)
	{
		return E_INVALIDARG;
	}

	if (pComModule->cbSize == 0)  // Module hasn't been initialized
	{
		return E_UNEXPECTED;
	}

	HRESULT hr = S_OK;

	for (_ATL_OBJMAP_ENTRY_EX** ppEntry = pComModule->m_ppAutoObjMapFirst; ppEntry < pComModule->m_ppAutoObjMapLast; ppEntry++)
	{
		if (*ppEntry != NULL)
		{
			const _ATL_OBJMAP_ENTRY_EX* pEntry = *ppEntry;

			if ((pEntry->pfnGetClassObject != NULL) && InlineIsEqualGUID(rclsid, *pEntry->pclsid))
			{
				_ATL_OBJMAP_CACHE* pCache = pEntry->pCache;

				if (pCache->pCF == NULL)
				{
					CComCritSecLock<CComCriticalSection> lock(pComModule->m_csObjMap, false);
					hr = lock.Lock();
					if (FAILED(hr))
					{
						ATLTRACE(atlTraceCOM, 0, _T("ERROR : Unable to lock critical section in AtlComModuleGetClassObject\n"));
						ATLASSERT(FALSE);
						break;
					}

					if (pCache->pCF == NULL)
					{
						IUnknown *factory = NULL;
						hr = pEntry->pfnGetClassObject(pEntry->pfnCreateInstance, __uuidof(IUnknown), reinterpret_cast<void**>(&factory));
						if (SUCCEEDED(hr))
						{
							pCache->pCF = reinterpret_cast<IUnknown*>(::EncodePointer(factory));
						}
					}
				}

				if (pCache->pCF != NULL)
				{
					// Decode factory pointer
					IUnknown* factory = reinterpret_cast<IUnknown*>(::DecodePointer(pCache->pCF));
					_Analysis_assume_(factory != nullptr);
					hr = factory->QueryInterface(riid, ppv);
				}
				break;
			}
		}
	}

	if (*ppv == NULL && hr == S_OK)
	{
		hr = CLASS_E_CLASSNOTAVAILABLE;
	}

	return hr;
}

为了了解它怎么实现的,我们主要看下面这个循环:

for (_ATL_OBJMAP_ENTRY_EX** ppEntry = pComModule->m_ppAutoObjMapFirst; ppEntry < pComModule->m_ppAutoObjMapLast; ppEntry++)
{
  ...
}

依照MSDN的文档:
Object Map Macros
ATL提供了一个叫做对象表(Object Map)的宏OBJECT_ENTRY_AUTO(clsid, class),如果你的CYourClass继承了CComCoClass(准确地来说,是定义了创建模型(如DECLARE_AGGREGATABLE)和工厂类(DECLARE_CLASSFACTORY)),那么在全局的地方使用OBJECT_ENTRY_AUTO(你的classid, 你的class名),便可以将你的CYourClass的对象信息放入 ATL$__m 数据段中。上述的循环m_ppAutoObjectMapFirst, m_ppAutoObjMapLast,便是拿到了ATL$__m的一首一尾,作为迭代的返回,遍历用户的每一个Object Map。这个可以说是“奇技淫巧”了,按照C++的观点,我们可以使用全局构造函数来注册我们的Object Map,不过代价是,这个注册是在运行时,而不是直接放入某个数据段的编译时。

对象表包含以下信息:

struct _ATL_OBJMAP_ENTRY110
{
	const CLSID* pclsid;  // 对象的classid
	HRESULT (WINAPI *pfnUpdateRegistry)(_In_ BOOL bRegister); // 对象更新注册表的方法
	_ATL_CREATORFUNC* pfnGetClassObject; // 对象GetClassObject方法
	_ATL_CREATORFUNC* pfnCreateInstance; // 对象创建实例的方法
	_ATL_OBJMAP_CACHE* pCache; // 对象缓存
	_ATL_DESCRIPTIONFUNC* pfnGetObjectDescription; // 对象获取描述方法
	_ATL_CATMAPFUNC* pfnGetCategoryMap; // 对象的类型表
	...
};

OBJECT_ENTRY_AUTO宏定义如下:

#define OBJECT_ENTRY_AUTO(clsid, class) \
    __declspec(selectany) ATL::_ATL_OBJMAP_CACHE __objCache__##class = { NULL, 0 }; \
	const ATL::_ATL_OBJMAP_ENTRY_EX __objMap_##class = {&clsid, class::UpdateRegistry, class::_ClassFactoryCreatorClass::CreateInstance, class::_CreatorClass::CreateInstance, &__objCache__##class, class::GetObjectDescription, class::GetCategoryMap, class::ObjectMain }; \
	extern "C" __declspec(allocate("ATL$__m")) __declspec(selectany) const ATL::_ATL_OBJMAP_ENTRY_EX* const __pobjMap_##class = &__objMap_##class; \
	OBJECT_ENTRY_PRAGMA(class)

依次对比结构,我们可以发现:

pclsid=&clsid;
pfnUpdateRegistry=class::UpdateRegistry;
pfnGetClassObject=class::_ClassFactoryCreatorClass::CreateInstance;
pfnCreateInstance=class::_CreatorClass::CreateInstance;
pfnGetObjectDescription=class::GetObjectDescription;
pfnGetCategoryMap=class::GetCategoryMap;
...

class::_ClassFactoryCreatorClass::CreateInstance是由宏DECLARE_CLASSFACTORY展开,表示创建一个继承了IClassFactory的类对象。

class::_CreatorClass::CreateInstance是由宏DECLARE_AGGREGATABLE等4个宏其中的一个展开得到,表示如何创建一个对象本身。

所以,pfnGetClassObject的作用是,创建一个类的工厂对象,pfnCreateInstance作用是创建一个类的对象的实例。

回过头我们再来看这段循环:

for (_ATL_OBJMAP_ENTRY_EX** ppEntry = pComModule->m_ppAutoObjMapFirst; ppEntry < pComModule->m_ppAutoObjMapLast; ppEntry++)
{
	if (*ppEntry != NULL)
	{
		const _ATL_OBJMAP_ENTRY_EX* pEntry = *ppEntry;

		if ((pEntry->pfnGetClassObject != NULL) && InlineIsEqualGUID(rclsid, *pEntry->pclsid))
		{
			...
				IUnknown *factory = NULL;
				hr = pEntry->pfnGetClassObject(pEntry->pfnCreateInstance, __uuidof(IUnknown), reinterpret_cast<void**>(&factory));
				if (SUCCEEDED(hr))
				{
					pCache->pCF = reinterpret_cast<IUnknown*>(::EncodePointer(factory));
				}
			...
		}
	}
}

留意

hr = pEntry->pfnGetClassObject(pEntry->pfnCreateInstance, __uuidof(IUnknown), reinterpret_cast<void**>(&factory))

首先,创建一个工厂类对象,默认情况下,pfnGetClassObject=class::_ClassFactoryCreatorClass::CreateInstance,传入创建对象实例的函数pfnCreateInstance。默认情况下,pfnCreateInstance=CComCreator<CComObject<>>::CreateInstance。

/ CComClassFactory
STDMETHOD(CreateInstance)(
	_Inout_opt_ LPUNKNOWN pUnkOuter,
	_In_ REFIID riid,
	_COM_Outptr_ void** ppvObj)
{
	ATLASSUME(m_pfnCreateInstance != NULL);
	HRESULT hRes = E_POINTER;
	if (ppvObj != NULL)
	{
		*ppvObj = NULL;
		// can't ask for anything other than IUnknown when aggregating

		if ((pUnkOuter != NULL) && !InlineIsEqualUnknown(riid))
		{
			ATLTRACE(atlTraceCOM, 0, _T("CComClassFactory: asked for non IUnknown interface while creating an aggregated object"));
			hRes = CLASS_E_NOAGGREGATION;
		}
		else
			hRes = m_pfnCreateInstance(pUnkOuter, riid, ppvObj);
	}
	return hRes;
}

// helper
void SetVoid(_In_opt_ void* pv)
{
	m_pfnCreateInstance = (_ATL_CREATORFUNC*)pv;
}

CComClassFactory::Instance的第一个参数,为创建对象实例的函数指针,它通过SetVoid赋值给了自己的m_pfnCreateInstance,然后又调用

hRes = m_pfnCreateInstance(pUnkOuter, riid, ppvObj);

这便相当于直接调用了:

hRes = CComCreator<CComObject<xxx>>::CreateInstance(pUnkOuter, rrid, ppvObj);

DECLARE_CLASSFACTORY和DECLARE_AGGREGATABLE由于默认写在了CComCoClass中,为了能在ATL中自动实现DllGetClassObject,你只需要将自己的CYourClass继承CComCoClass,并且通过ENTRY_OBJECT_AUTO暴露出来就可以了,剩下的,ATL会帮你完成。

3. DllRegisterServer, DllUnregisterServer

这2个方法最终会分别调入AtlComModuleRegisterServer, AtlComModuleUnregisterServer这两个方法中。同样地,ATL会取出所有的对象表,然后依次调用pfnUpdateRegistry, pfnGetCategoryMap等方法,完成注册表的添加或删除。

可以说,CComCoClass就是类工厂的基类,ENTRY_OBJECT_AUTO宏是将它暴露出来的宏。


以上便是COM模块中需要实现的几个方法,以及ATL给出的实现。ATL配合上模板非常绕,光看MSDN还是会难以理解,还是得要跟着源代码一起理解才可以。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值