发布时间:2012-12-16 20:23
分类名称:COM
重用性(包容、聚合)
透明性(列集、散集)
安全性
多线程特性(套间)
重用性 (包容/聚合)
包容
聚合
对象A是已经被实现好的COM对象。
包容
B实现ISomeInterface和IOtherInterface接口,然后在ISomeInterface的实现中调用A对象提供的服务。一般的,A的生存期在B生存期内。
聚合
B只实现了IOtherInterface,当客户要求B对象提供ISomeInterface时,由于A被聚合到了B中,B可以提供此服务。一般的,A的生存期在B生存期内。
聚合的复杂性在于,B虽然知道A实现了什么接口,但A不知道B实现了什么接口。这样就导致通过B获取到A的实例指针后,通过此指针调用的QueryInterface是A的,也就无法Query出B中实现的接口来。而且A和B的接口继承于俩个不同的IUnknown,获取回的指针也不一样。这样就违背了QueryInterface需要遵循的原则。(什么原则?参考我的《COM笔记-原理(基础)》里面的内容)
解决聚合复杂性的办法,在A中留下一个IUnknown 指针(依赖抽象),通过外部(例如B)将其IUnknown传入给A,A就可以利用此接口来代理B 来做QueryInterface了。
实现:
/************** B组件实现代码**************/
class CB : public IOtherInterface
{
......
private:
IUnknown *m_pUnknownInner; // Point to A's IUnknown
......
}
CB::QueryInterface(...)
{
......
if (iid == IID_SomeInterface)
return m_pUnknownInner->QueryInterface(iid, ppv);
......
}
/*********************************************/
A分被聚合和未被聚合俩种情况。
如果被聚合,A就使用内部这个指针(即B传入的自己实例指针)来QueryInterface,相当于是B在Query Interface,获取的IUnknown都是B的。而且不存在A找不到B的Interface。 而且可以看到,B组件中,如果Query是A的组件,又回回到A自己的QueryInterface中,去Query到自己的Interface。(看起来比较绕,)
如果未被聚合,那么就什么都不做,不使用这个指针。
CoCreateInstance有一个参数,叫IUnknown *pUnknownOuter,这个指针,就是A中要保存的B实例指针。
A内部,为了区分聚合和非聚合,定了了俩个IUnknown(delegating unknown, undelegating unknown).这个俩个委托和非委托名词,曾经困扰了我很久,后来发现是因为这俩个词语本身的语义对我的误导。如果要以我自己的方式区分,我将其区分为一个IUnknown是Proxy(代理),一个IUnknown是正常的IUnknown。代理只是个中转站,负责分发,其内部其实就是一个判断,如果A的pUnknownOuter为NULL,那么就转发给正常的IUnknown。如果pUnknownOuter不为NULL,那么就调用它自身的接口函数。
class INondelegationUnknown
{
public:
virtual HRESULT _stdcall NondelegationQueryInterface(...) = 0;
virtual ULONG _stdcall NondelegationAddRef() = 0;
virtual ULONG _stdcall NondelegationRelease() = 0;
};
class CA :
public ISomeInterface,
public INondelegationUnknown
{
......
public:
NondelegationQueryInterface(...);
NondelegationAddRef();
NondelegationRelease();
QueryInterface(...);
AddRef();
Release();
......
private:
IUnknown *m_pUnknownOuter;
......
};
CA::NondelegationQueryInterface(...)
{ 正常Query,就像什么都没发生过 }
CA::NondelegationAddRef()
{ 正常AddRef,就像什么都没发生过 }
CA::NondelegationRelease()
{ 正常Release,就像什么都没发生过 }
CA::QueryInterface(...){
if (NULL == m_pUnknownOuter) // 转发
Call NondelegationQueryInterface
else
Call m_pUnknownOuter->QueryInterface
}
CA::AddRef(){
if (NULL == m_pUnknownOuter) // 转发
Call NondelegationAddRef
else
Call m_pUnknownOuter->AddRef
}
CA::Release(){
if (NULL == m_pUnknownOuter) // 转发
Call NondelegationRelease
else
Call m_pUnknownOuter->AddRef
}
再看看B何时将它自己的指针传递给A:
HRESULT CB::Init()
{
IUnknown *pUnknownOuter = (IUnknown *) this;
HRESULT result = ::CoCreateInstance(CLSID_ComponetA, pUnknownOuter, CLSCTX_INPROC_SERVER, IID_IUnknown, (void **)&m_pUnknownInner);
......
传入pUnknownOuter(既B的this),而且得到m_pUnknownInner(A的this)。
}
UML图如下:ISomeInterface能由CB来Query,换句话说,Client不知道CA的存在。可以看到INondelegationUnknown是个的辅助接口。CA use IUnknown接口,就能接受任何COM对象, 在CoCreateInstance的时候,将CB的this传入,可以看到类厂的CreateInstance也有一个Outter接口,CreateInstance将CB的实例指针传入CA中的m_pUnknownOuter保存。类厂CreateInstance完毕,
将CA的实例由参数m_pUnknownInner获得。
这里会有个奇怪的现象,B获取到的this指针指向的是INondelegationUnknown接口,而且B并不知道,他认为这个接口是IUnknown接口。更加奇怪的是,当你使用innter 指针调用AddRef,Release和QueryInterface的时候,你会惊奇的发现代码会跑到NondelegationAddRef/Release/QueryInterface,很神奇。这也就是INondelegationUnknown的作用,他和IKnown的接口布局完全相同,属于在汇编级的调用。那为什么CB获取的是INondelegationUnknown的接口,而不是IKnown的接口呢?如果是IKnown,那么当B调用A去Query的时候,调用的是A的QueryInterface,内部判断Outer指针不为NULL,转掉B的QueryInterface,然后B的QueryInterface,又去调用A的QueryInterface,这样以来,就成了死循环。
INondelegationUnknown伪造了IKnown,在汇编一级的函数调用,只是一个this取偏移量而已。(详情参看: Inside C++ Object model 这本书)例如:m_pUnknownInner->AddRef(),汇编级别,会被转换为:
( *m_pUnknownInner->vptr[1] ) ( m_pUnknownInner),当得到的m_pUnknownInner指向的是INondelegationUnknown的时候,显然就调用到了NondelegationAddRef()里。
进程透明性(列集、散集)
进程外组件与客户程序调用的基本模型图
代理对象和存根代码实际上存在于同一个DLL中,它们是被系统的COM库自动加载和调用的。如果自己没有实现代理对象和存根代码,默认使用MIDL编译IDL文件后,会生成*_i.c,*_p.c,*.def,dlldata.c,通过编译这几个文件,就能编译出一个DLL来。
在客户端为何叫代理对象? 在客户端通过QueryInterface获取到指定接口的对象,这个对象实际上并不是客户端想象到的C++层面的对象,客户端获取到的其实是个"代理对象",这个代理对象是由COM库动态构造出来的。
由于对虚函数的在汇编层面只是个对this->vptr索引调用,这种实现则完全有可行,当调试的时候会发现,自己的代理对象/存根代码模块(DLL)被动态加载,进入的函数是诸如:*_Proxy(在*_p.c文件实现),这个函数体就是代理对象的函数体,接着内部会将传入的参数做列集处理,然后通过LPC/RPC和组件对象中的存根代码通讯。
客户端与组件通讯分为俩个部分:
COM整套通讯很复杂,剖析其更本也没有什么必要,我们只要明确我们需要做什么即可。
列集分为自定义列集和标准列集两种。
连接过程(获取接口指针的过程)
当客户端调用诸如QueryInterface此类接口,获取新的Interface的时候,由于客户端获取到的指针是指向"代理对象",调用到的QueryInterface就会转到COM动态创建的对象内部实现的函数,此函数通过COM库中的一些操作后,在组件这边开始列集操作,列集函数为:CoMarshalInterface。此函数内部会做几件事情:
然后COM库将CLSID和列集数据包传给客户端。传输过程由SCM控制,它知道客户和组件能够通讯的各种方式。
客户端COM库这边接到数据后,调用ConUnMarshalInterface:
代理对象一定是进程内的组件(由COM库来搞定),所以客户端对COM对象的调用时直接进行的。
IMarshal : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE GetUnmarshalClass(
/* [in] */ REFIID riid,
/* [unique][in] */ void *pv,
/* [in] */ DWORD dwDestContext,
/* [unique][in] */ void *pvDestContext,
/* [in] */ DWORD mshlflags,
/* [out] */ CLSID *pCid) = 0;
virtual HRESULT STDMETHODCALLTYPE GetMarshalSizeMax(
/* [in] */ REFIID riid,
/* [unique][in] */ void *pv,
/* [in] */ DWORD dwDestContext,
/* [unique][in] */ void *pvDestContext,
/* [in] */ DWORD mshlflags,
/* [out] */ DWORD *pSize) = 0;
virtual HRESULT STDMETHODCALLTYPE MarshalInterface(
/* [unique][in] */ IStream *pStm,
/* [in] */ REFIID riid,
/* [unique][in] */ void *pv,
/* [in] */ DWORD dwDestContext,
/* [unique][in] */ void *pvDestContext,
/* [in] */ DWORD mshlflags) = 0;
virtual HRESULT STDMETHODCALLTYPE UnmarshalInterface(
/* [unique][in] */ IStream *pStm,
/* [in] */ REFIID riid,
/* [out] */ void **ppv) = 0;
virtual HRESULT STDMETHODCALLTYPE ReleaseMarshalData(
/* [unique][in] */ IStream *pStm) = 0;
virtual HRESULT STDMETHODCALLTYPE DisconnectObject(
/* [in] */ DWORD dwReserved) = 0;
};
使用连接跨进程调用过程(调用一般函数)
客户端通过代理对象调用接口函数,会转到存根/代理模块的代码中(对应的是*_proxy函数),进而进行Marshal处理,通过RPC/LPC方式发送到组件端。组件端COM组件接受到消息和数据,同样也会转调入根/代理模块的代码中(对应的是*_stub函数),存根函数里面会保存组件真正的对象指针,回调回组件端真正的代码。
调用的驱动方式为:
客户 <-> COM库 <-> 存根/代理模块 <-> RPC <-> COM库 <-> 存根/代理模块 <-> 组件。
标准列集中,每个代理对象(ITF*)不仅实现了它自身代理的接口(组件对象的接口),还实现了IRpcProxyBuffer,
COM管理器如何创建接口代理对象和接口存根?
注册表中:
Interface
+ {IID}
-ProxyStubClsid32 = {CLSID} //此CLSID对应的是存根/代理对象的CLSID
在HEKY_CLASS_ROOT\CLSID中,能够找到此CLSID,
{CLSID}
InProcServer32 会存储真正的路径
如图:
当客户调用QueryInterface的时候,代码管理器会用如下代码创建代理对象:
clsid = LookUpInRegister(iid);
CoGetClassObject(clsid, CLSCTX_SERVER, NULL, IID_IPSFactoryBuffer, &pPSFactory);
pPSFactory->CreateProxy(pUnkOuter, riid, &pProxy, &piid);
存根得到IPSFactoryBuffer后,调用CreateStub,创建了接口存根。
clsid = LookUpInRegister(iid);
CoGetClassObject(clsid, CLSCTX_SERVER, NULL, IID_IPSFactoryBuffer, &pPSFactory);
pPSFactory->CreateStub(iid, pUnkServer, &pStub);
代理存根中,并没有使用IClassFactory创建,而是使用IPSFactoryBuffer。(原因我觉得是这俩个组件很特殊,存在形式和一般对象也不一样)。
IPSFactoryBuffer : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE CreateProxy(
/* [in] */ IUnknown *pUnkOuter,
/* [in] */ REFIID riid,
/* [out] */ IRpcProxyBuffer **ppProxy,
/* [out] */ void **ppv) = 0;
virtual HRESULT STDMETHODCALLTYPE CreateStub(
/* [in] */ REFIID riid,
/* [unique][in] */ IUnknown *pUnkServer,
/* [out] */ IRpcStubBuffer **ppStub) = 0;
};
PRC通讯
IRpcChannelBuffer : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE GetBuffer(
/* [in] */ RPCOLEMESSAGE *pMessage,
/* [in] */ REFIID riid) = 0;
virtual HRESULT STDMETHODCALLTYPE SendReceive(
/* [out][in] */ RPCOLEMESSAGE *pMessage,
/* [out] */ ULONG *pStatus) = 0;
virtual HRESULT STDMETHODCALLTYPE FreeBuffer(
/* [in] */ RPCOLEMESSAGE *pMessage) = 0;
virtual HRESULT STDMETHODCALLTYPE GetDestCtx(
/* [out] */ DWORD *pdwDestContext,
/* [out] */ void **ppvDestContext) = 0;
virtual HRESULT STDMETHODCALLTYPE IsConnected( void) = 0;
};
接口代理和存根通讯时, 首先调用IRpcChannelBuffer::GetBuffer获取一个数据缓冲区,然后调用SendReceive后,组件进程中的RPC通道就会调用接口存根的IRpcStubBuffer的Invoke成员函数,Invoke则调用组件对象的成员函数,返回结果。再次使用GetBuffer获取缓冲区,存放返回结果。存根接口返回,最终代理的SendReceive返回最终结果。这个过程可以是同步的,也可以是异步的。
IRpcProxyBuffer : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE Connect(
/* [unique][in] */ IRpcChannelBuffer *pRpcChannelBuffer) = 0;
virtual void STDMETHODCALLTYPE Disconnect( void) = 0;
};
IRpcStubBuffer : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE Connect(
/* [in] */ IUnknown *pUnkServer) = 0;
virtual void STDMETHODCALLTYPE Disconnect( void) = 0;
virtual HRESULT STDMETHODCALLTYPE Invoke(
/* [in] */ RPCOLEMESSAGE *_prpcmsg,
/* [in] */ IRpcChannelBuffer *_pRpcChannelBuffer) = 0;
virtual IRpcStubBuffer *STDMETHODCALLTYPE IsIIDSupported(
/* [in] */ REFIID riid) = 0;
virtual ULONG STDMETHODCALLTYPE CountRefs( void) = 0;
virtual HRESULT STDMETHODCALLTYPE DebugServerQueryInterface(
void **ppv) = 0;
virtual void STDMETHODCALLTYPE DebugServerRelease(
void *pv) = 0;
};
安全性
激活安全
COM如何被安全启动,如果安全建立连接,保护公共资源,系统注册表等。
调用安全
调用组件之间传输的数据如何保护等。
多线程特性(套间)
线程
UI线程(包含一个消息循环),所有的消息都是按照一定顺序执行的,对于消息可以不做同步处理。
工作线程。
COM中,和UI线程对应的是套间线程,和工作线程对应的是自由线程。
套间线程
属于此线程的COM对象,通过消息循环调用函数,其他线程若要调用此COM对象,不能直接调用,必须通过消息循环分发调用。因此,套间线程以外的线程只能通过代理/存根调用此对象。
自由线程
属于此线程的COM对象,同一进程中任何线程都可以调用此对象,因此此对象需要做同步处理,以保证线程安全。
若是进程外组件,无论其运行在套间线程还是自由线程,都是间接调用的,列集和散集的结果是自动实现了同步,对象不必做处理。
具体细节,需要大量篇幅来写,不是笔记能做的完的。