特别说明 —— 该系列文章皆转自知乎作者 Froser 的COM编程攻略:https://www.zhihu.com/column/c_
前言
上一篇文章中说到,微软推出了ATL库实现了IUnknown接口,推出了基于继承(CComObject<>)的模板类和基于聚合(CComAggObject<>)的模板类。
这篇文章来将一下,如何创建一个COM对象,它的统一创建函数的签名为何是这样设计的。
一、传统的C++对象创建方式
假设客户的代码运行在A.exe,客户使用了开发商提供的一个库B.dll,B.dll中导出了一个类ClassB,那么在A.exe中,创建一个ClassB对象,假如ClassB是这样的:
interface IRead : IUnknown {
... }; interface IWrite : IUnknown {
... }; class ClassB : public IRead, public IWrite {
... };
那么可以是这样来写:
ClassB* b = new ClassB();
这样,我们便可以将ClassB对象创建出来。不过,它有几个问题:
- new运算符跨越了边界。operator new的行为在A中被实现,例如虚表指针是在哪里,是否在结构中有额外的数据,而ClassB实现在B.dll。除非我们使用兼容的编译器,生成相同的ClassB结构,否则可能会引起一些二进制兼容问题。
- new对象和释放对象可能不在同一个模块。尤其是COM对象,分配在堆上面的COM对象,是通过Release()来释放自己的,如果在另外的模块new出来,在自己模块释放,在某些情况下(如mt, md不一致),会出现问题。为了解决这个问题,我们需要在模块B.dll中暴露一个创建对象的函数。
- 一个ClassB这样的COM对象,我们不关心它的实现类ClassB,而只需要拿它的某一个接口。例如,我们只需要在ClassB中拿到IRead或者IWrite接口,如果我们调用了ClassB中除了这两个接口之外的函数或者成员并调用了,那么它就和ClassB的实现耦合了起来,违背了COM对象解决二进制兼容的原则。所以,在创建对象的时候,我们要指明,我们需要拿哪个接口。
综上,我们设计出第一版创建ClassB的函数(伪代码),它实现在了B.dll:
void* CreateInstance(REFIID iid) {
ClassB* b = new ClassB(); if (iid是IID_IWrite) return static_cast<IWrite*>(b); if (iid是IID_IRead) return static_cast<IRead*>(b); return nullptr; }
上面这一版是我们创建ClassB的雏形,它需要进一步优化。首先,按照COM标准,它需要返回一个值指示是否转换成功。其次,它需要获取什么接口,可以由ClassB::QueryInterface来得到。于是我们设计出了第二版创建函数:
HRESULT CreateInstance(REFIID iid, void** ppvObject) {
ClassB* b = new ClassB(); return b->QueryInterface(iid, ppvObject); }
接下来我们再进行一次优化。回想起CComAggObject,它是一个聚合模型,它本身是IUnknown,它所包含的对象也是一个IUnknown。我们创建的IUnknown可能就是要塞入这个CComAggObject聚合对象中的,这种情况下,创建出来的对象必须要感知到,外层对象(也就是CComAggObject所包含的对象)的指针是什么,这样自己在QueryInterface, AddRef或Release的时候才能转调外层。为了支持这种形式的创建,我们最终将签名改为:
HRESULT CreateInstance(IUnknown* pUnkOuter, REFIID iid, void** ppvObject);
上面的例子创建的是继承关系下的reader对象,在ATL中,reader其实就是CComObject。如果是创建一个聚合对象,那么它其实就是创建的CComAggObject。
以上便是COM中最标准的创建函数的签名。当然HRESULT应该返回什么,MSDN上做了详细的说明,大家实现的时候记得要参考MSDN。
二、ATL中的CreateInstance的实现
我们再次看一下这个继承关系图。上一篇文章里说到,CComObjectRoot提供了内部的AddRef, Release和QueryInterface的实现。那么这一篇文章就介绍CComCoClass<>,它主要是在CYourClass中添加了CreateInstance静态方法,并且可以指定这个对象创建出来是继承模型(CComObject<>)还是聚合模型(CComAggObject<>)。
CComCoClass实现在atlcom.h:
template <class T, const CLSID* pclsid = &CLSID_NULL> class CComCoClass {
public: DECLARE_CLASSFACTORY() DECLARE_AGGREGATABLE(T) typedef T _CoClass; template <class Q> static HRESULT CreateInstance( _Inout_opt_ IUnknown* punkOuter, _COM_Outptr_ Q** pp) {
return T::_CreatorClass::CreateInstance(punkOuter, __uuidof(Q), (void**) pp); } template <class Q> static HRESULT CreateInstance(_COM_Outptr_ Q** pp) {
return T::_CreatorClass::CreateInstance(NULL, __uuidof(Q), (void**) pp); }
上面源码省略了错误处理相关函数,且我们先不用关系第二个模板参数pclsdid的含义。
关注到CreateInstance的两个重载。它调用了模板T中的_CreatorClass类中的CreateInstance函数。区别在于,一个需要传入一个punkOuter,一个不需要,默认传的是NULL,我们便推测得出这两个方法,一个是用于创建聚合对象,聚合对象的QueryInterface将转调punkOuter->QueryInterface;一个是用于创建继承模型下的对象的。
那么,_CreatorClass到底是什么呢?我们看到有一个宏:
DECLARE_AGGREGATABLE(T)
它定义在了altcom.h:
#define DECLARE_AGGREGATABLE(x) public:\ typedef ATL::CComCreator2< ATL::CComCreator< ATL::CComObject< x > >, ATL::CComCreator< ATL::CComAggObject< x > > > _CreatorClass;
其中CComCreator用于创建一个对象:
template <class T1> class CComCreator {
public: static HRESULT WINAPI CreateInstance( _In_opt_ void* pv, _In_ REFIID riid, _COM_Outptr_ LPVOID* ppv) {
ATLASSERT(ppv != NULL); if (ppv == NULL) return E_POINTER; *ppv = NULL; HRESULT hRes = E_OUTOFMEMORY; T1* p = NULL; ATLPREFAST_SUPPRESS(6014 28197) /* prefast noise VSW */ ATLTRY(p = _ATL_NEW T1(pv)) ATLPREFAST_UNSUPPRESS() if (p != NULL) {
p->SetVoid(pv); p->InternalFinalConstructAddRef(); hRes = p->_AtlInitialConstruct(); if (SUCCEEDED(hRes)) hRes = p->FinalConstruct(); if (SUCCEEDED(hRes)) hRes = p->_AtlFinalConstruct(); p->InternalFinalConstructRelease(); if (hRes == S_OK) {
hRes = p->QueryInterface(riid, ppv); _Analysis_assume_(hRes == S_OK || FAILED(hRes)); } if (hRes != S_OK) delete p; } return hRes; } };
我们看到CComCreator,T1在DECLARE_AGGREGATABLE中无非就是CComObject或CComAggObject,说明T1是指定了它的聚合模式。如果T1是CComObject,那么就有
CComObject* p = new CComObject(pv);
如果是T1是CComAggObject,那么
CComAggObject* p = new CComAggObject(pv);
事实上,CComObject中拿到了pv根本没有用上,因为它不存在外部对象。pv只对于CComAggObject才是有效的。
CComCreator2的实现则显得非常鸡贼:
template <class T1, class T2> class CComCreator2 {
public: static HRESULT WINAPI CreateInstance( _In_opt_ void* pv, _In_ REFIID riid, _COM_Outptr_ LPVOID* ppv) {
ATLASSERT(ppv != NULL); return (pv == NULL) ? T1::CreateInstance(NULL, riid, ppv) : T2::CreateInstance(pv, riid, ppv); } };
它其实是一个选择器,根据是否有外部对象pv,来决定是调用哪一个的CreateInstance。
在:
#define DECLARE_AGGREGATABLE(x) public:\ typedef ATL::CComCreator2< ATL::CComCreator< ATL::CComObject< x > >, ATL::CComCreator< ATL::CComAggObject< x > > > _CreatorClass;
定义了DECLARE_AGGREGATABLE的情况下,T1=CComCreator<CComObject>,T2=CComCreator<CComAggObject>,说明存在pv,则用T1创建CComObject,否则用T2创建CComAggObject。
除了这个宏,ATL还有另外几个宏DECLARE_NOT_AGGREGATABLE, DECLARE_ONLY_AGGREGATABLE, DECLARE_POLY_AGGREGATABLE:
#define DECLARE_NOT_AGGREGATABLE(x) public:\ typedef ATL::CComCreator2< ATL::CComCreator< ATL::CComObject< x > >, ATL::CComFailCreator<CLASS_E_NOAGGREGATION> > _CreatorClass; #define DECLARE_ONLY_AGGREGATABLE(x) public:\ typedef ATL::CComCreator2< ATL::CComFailCreator<E_FAIL>, ATL::CComCreator< ATL::CComAggObject< x > > > _CreatorClass; #define DECLARE_POLY_AGGREGATABLE(x) public:\ typedef ATL::CComCreator< ATL::CComPolyObject< x > > _CreatorClass;
其中CComFailCreator实现为无论如何都会失败的Creator,并返回HRESULT T:
template <HRESULT hr> class CComFailCreator {
public: static _Always_(_Post_satisfies_(return == hr || return == E_POINTER)) HRESULT WINAPI CreateInstance( _In_opt_ void*, _In_ REFIID, _COM_Outptr_result_maybenull_ LPVOID* ppv) {
if (ppv == NULL) return E_POINTER; *ppv = NULL; return hr; } };
通过以上分析,我们就知道了,CComCoClass需要和DECLARE_AGGREGATABLE, DECLARE_NOT_AGGREGATABLE, DECLARE_ONLY_AGGREGATABLE, DECLARE_POLY_AGGREGATABLE其中一个一起使用,调用它的CreateInstance方法创建对象。它们的区别在于:
- DECLARE_AGGREGATABLE: 如果有外部对象pv,创建CComAggObject;否则创建CComObject
- DECLARE_NOT_AGGREGATABLE: 如果有外部对象pv,创建失败;否则创建CComAggObject
- DECLARE_ONLY_AGGREGATABLE: 如果有外部对象pv,创建CComAggObject,否则创建失败。
- DECLARE_POLY_AGGREGATABLE: 创建CComPolyObject
以上4个宏,指定的就是创建COM对象的模型。
就这样,我们便可以通过CComCoClass来为CYourClass添加一个标准的创建函数了。
下一篇将讲类厂及如何用COM API来创建COM对象。
今天的文章
com口定义_com编程精彩实例有哪些软件分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/80610.html