特别说明 —— 该系列文章皆转自知乎作者 Froser 的COM编程攻略:https://www.zhihu.com/column/c_1234485736897552384
文章目录
前言
上一篇我们讲了,COM API的CoCreateInstance, CoCreateInstanceEx是如何工作的,以及我们如何编写一个类,如何在注册表中注册它的信息。
到此为止,我们已经了解到了如何实现一个COM对象,如何创建一个COM类,如何注册它,将它暴露给客户。至此,我们已经能够编写一个完完全全的COM模块了。
所以,这一篇,我们将总结一下,一个COM模块包含了哪些内容,需要实现哪些方法。
一、模块需要实现的方法
- 实现DllGeClassObject
STDAPI DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID* ppv);
这个我们上一篇讲过,在调用CoGetClassObject,或者CoCreateInstance时,dll中的这个导出函数会被调用,负责返回一个类出去。
- 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;
}
- 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还是会难以理解,还是得要跟着源代码一起理解才可以。