[原] COM 笔记 – 原理(高级)

发布时间: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和组件对象中的存根代码通讯。

客户端与组件通讯分为俩个部分:

  1. 建立连接(建立连接是在获取接口指针中建立起来的)
  2. 使用连接跨进程调用(上面说的就是跨进程调用)

COM整套通讯很复杂,剖析其更本也没有什么必要,我们只要明确我们需要做什么即可。

列集分为自定义列集和标准列集两种。

连接过程(获取接口指针的过程)

当客户端调用诸如QueryInterface此类接口,获取新的Interface的时候,由于客户端获取到的指针是指向"代理对象",调用到的QueryInterface就会转到COM动态创建的对象内部实现的函数,此函数通过COM库中的一些操作后,在组件这边开始列集操作,列集函数为:CoMarshalInterface。此函数内部会做几件事情:

  1. 向对象查询是否支持IMarshal接口。
  2. 如果支持,调用IMarshal::GetUnMashalClass,获取代理对象的CLSID。如果不支持,使用确认代理对象CLSID_StdMarshal。
  3. 调用IMarshal::MarshalInterface建立列集数据包(用于代理对象和组件对象跨进程连接必需的信息)。

然后COM库将CLSID和列集数据包传给客户端。传输过程由SCM控制,它知道客户和组件能够通讯的各种方式。

客户端COM库这边接到数据后,调用ConUnMarshalInterface:

  1. 根据CLSID创建代理对象(通常都是标准的代理对象,COM库已经实现,对我们则是透明的)。
  2. 从代理对象中,请求IMarshal接口。
  3. 使用IMarshal::UnmarshalInterface得到指针接口,返回给客户。

代理对象一定是进程内的组件(由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对象,同一进程中任何线程都可以调用此对象,因此此对象需要做同步处理,以保证线程安全。

若是进程外组件,无论其运行在套间线程还是自由线程,都是间接调用的,列集和散集的结果是自动实现了同步,对象不必做处理。

具体细节,需要大量篇幅来写,不是笔记能做的完的。