最近看公司的一些新产品和框架 , 发现清一色的“COM思想架构 ”, 这里说的“COM思想架构”是指不完全是标准COM组件的方式,而是指在设计上用到了COM思想。
COM组件技术大概在1993年产生, 20年了, 为什么还有这么多人使用? 
我们先来看看标准COM组件:
标准COM组件(DLL方式)需要实现如下4个导出函数:
DllRegisterServer 将组件信息写入注册表
DllUnregisterServer 取消注册
DllCanUnloadNow判断组件是否可以从内存中卸载
DllGetClassObject返回IClassFactory指针,然后我们就可以通过该接口的CreateInstance方法创建对象并取得所需的接口。
采用标准COM组件,有很多好处:
面向接口和对象编程
语言无关性, 采用二进制标准,可以实现跨语言调用
版本升级方便,增加新接口, 组件升级后老客户程序不用重新编译
位置透明, 客户程序不用关心组件的位置
重用方便, 通过包容和聚合可以快速重用已有组件
我们可以看到标准COM组件非常强大, 但是很多时候我们并不需要标准COM组件的所有特性,比如我们不希望引入注册表, 也不希望引入COM运行库,我们希望我们的程序是完全“绿色”的。这时我们就会采用“COM思想架构“开发非标准的COM组件。
实际上微软本身已经有很多API采用这种设计方案了,我们来看一些例子:
XmlLite
继msxml之后微软提供的另 一款高效的XML解析器, 它本身只有一个绿色DLL XmlLite.dll, 关于它的接口和使用方法可以参考XmlLite Introduction用于本机 C++ 的小巧快捷的 XML 分析器
我们可以用depends.exe看看该DLL的导出函数:


调用这些导出的CreateXXX函数返回返回一个继承于IUnknown的接口, 然后我们就可以调用接口提供的方法了, 可以看下IXmlReader的方法:
    IXmlReader : public IUnknown
    {
    
public:
        
virtual HRESULT STDMETHODCALLTYPE SetInput( 
            
/* [annotation] */ 
            __in_opt  IUnknown 
*pInput) = 0;
        
        
virtual HRESULT STDMETHODCALLTYPE GetProperty( 
            
/* [annotation] */ 
            __in  UINT nProperty,
            
/* [annotation] */ 
            __out  LONG_PTR 
*ppValue) = 0;
        
        
virtual HRESULT STDMETHODCALLTYPE SetProperty( 
            
/* [annotation] */ 
            __in  UINT nProperty,
            
/* [annotation] */ 
            __in_opt  LONG_PTR pValue) 
= 0;
        
        
virtual HRESULT STDMETHODCALLTYPE Read( 
            
/* [annotation] */ 
            __out_opt  XmlNodeType 
*pNodeType) = 0;
        
        
virtual HRESULT STDMETHODCALLTYPE GetNodeType( 
            
/* [annotation] */ 
            __out  XmlNodeType 
*pNodeType) = 0;
        
        
virtual HRESULT STDMETHODCALLTYPE MoveToFirstAttribute( void= 0;
        
        
virtual HRESULT STDMETHODCALLTYPE MoveToNextAttribute( void= 0;
        

        .......

        
    };
Direct2D
关于是微软下一代2D渲染接口, 关于它的详情参考Direct2D, 我们同样分析一下它的导出函数:

实际看到这里也用了COM思想的方法,我们可以看看D2D1CreateFactory返回的ID2D1Factory的接口:
interface DX_DECLARE_INTERFACE("06152247-6f50-465a-9245-118bfd3b6007") ID2D1Factory  : public IUnknown
{
    
//
    
// Cause the factory to refresh any system metrics that it might have been snapped
    
// on factory creation.
    
//
    STDMETHOD(ReloadSystemMetrics)(
        ) PURE;
    
    
//
    
// Retrieves the current desktop DPI. To refresh this, call ReloadSystemMetrics.
    
//
    STDMETHOD_(void, GetDesktopDpi)(
        _Out_ FLOAT 
*dpiX,
        _Out_ FLOAT 
*dpiY 
        ) PURE;
    
    STDMETHOD(CreateRectangleGeometry)(
        _In_ CONST D2D1_RECT_F 
*rectangle,
        _Outptr_ ID2D1RectangleGeometry 
**rectangleGeometry 
        ) PURE;
    
    STDMETHOD(CreateRoundedRectangleGeometry)(
        _In_ CONST D2D1_ROUNDED_RECT 
*roundedRectangle,
        _Outptr_ ID2D1RoundedRectangleGeometry 
**roundedRectangleGeometry 
        ) PURE;
    
    STDMETHOD(CreateEllipseGeometry)(
        _In_ CONST D2D1_ELLIPSE 
*ellipse,
        _Outptr_ ID2D1EllipseGeometry 
**ellipseGeometry 
        ) PURE;
    
    ......

}; 
// interface ID2D1Factory

思考为什么会有越来越多的新程序采用这种”COM思想架构“, 这个要回到COM的根 ---- IUnknown接口:

    IUnknown
    {
    
public:
        BEGIN_INTERFACE
        
virtual HRESULT STDMETHODCALLTYPE QueryInterface( 
            
/* [in] */ REFIID riid,
            
/* [annotation][iid_is][out] */ 
            __RPC__deref_out  
void **ppvObject) = 0;
        
        
virtual ULONG STDMETHODCALLTYPE AddRef( void= 0;
        
        
virtual ULONG STDMETHODCALLTYPE Release( void= 0;
        
        END_INTERFACE
    };

IUnknow接口是个伟大的创造!
 IUnknow的AddRef和Release实现对象的引用计数管理, 引用计数用来管理对象的生存周期。
通过引用计数一来可以很方便的共享对象, 另外也能确保对象被正确释放(确保对象的new和delete在同一模块中)。
QueryInterface实现接口查询, 通过这种方式可以很方便的对现有组件进行升级, 只要接口不改 ,可以随意修改内部实现而不用客户程序重新编译。
另外也可以直接增加新接口, 只要在QueryInterface内增加并可以查询到该新接口, 我们就可以调用该新接口。
我们可以看到QueryInterface让C++这种静态语言有了某些动态语言的特性, 在C# 中我们可以通过反射查询到某个类的成员函数和成员变量, Objective-C中我们也可以根据函数名动态调用某个函数, 在脚本语言中,我们可以在运行时动态查询和修改某个类的信息。通过COM的QueryInterface, 我们可以动态查询某个组件类实现哪些接口(函数)。当然他们之间有本质的区别, 动态语言运行时内存中保存有类信息, 而C++的QueryInterface通过switch case, 返回的是存有虚表指针的对象指针。
最后再简单谈下IUnknown的升级版IDispatch和IInspectable。

先看IDispatch:
    IDispatch : public IUnknown
    {
    
public:
        
virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount( 
            
/* [out] */ __RPC__out UINT *pctinfo) = 0;
        
        
virtual HRESULT STDMETHODCALLTYPE GetTypeInfo( 
            
/* [in] */ UINT iTInfo,
            
/* [in] */ LCID lcid,
            
/* [out] */ __RPC__deref_out_opt ITypeInfo **ppTInfo) = 0;
        
        
virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames( 
            
/* [in] */ __RPC__in REFIID riid,
            
/* [size_is][in] */ __RPC__in_ecount_full(cNames) LPOLESTR *rgszNames,
            
/* [range][in] */ UINT cNames,
            
/* [in] */ LCID lcid,
            
/* [size_is][out] */ __RPC__out_ecount_full(cNames) DISPID *rgDispId) = 0;
        
        
virtual /* [local] */ HRESULT STDMETHODCALLTYPE Invoke( 
            
/* [in] */ DISPID dispIdMember,
            
/* [in] */ REFIID riid,
            
/* [in] */ LCID lcid,
            
/* [in] */ WORD wFlags,
            
/* [out][in] */ DISPPARAMS *pDispParams,
            
/* [out] */ VARIANT *pVarResult,
            
/* [out] */ EXCEPINFO *pExcepInfo,
            
/* [out] */ UINT *puArgErr) = 0;
        
    };

IDispatch继承于IUnknown, 通过IDispatch, 我们可以实现脚本语言对COM组件的调用,我们可以通过GetTypeInfo获取对象的类型信息, 通过GetIDsOfNames函数以字符串的方式获取函数的DISPID, 通过Invoke动态调用某个函数。IE的DOM对象与JS的交互全部是通过IDispatch(Ex)接口实现的。当然,除非你的组件要与脚本语言交互, 否者一般不用实现该接口。
再看IInspectable:
    IInspectable : public IUnknown
    {
    
public:
        
virtual HRESULT STDMETHODCALLTYPE GetIids( 
            
/* [out] */ __RPC__out ULONG *iidCount,
            
/* [size_is][size_is][out] */ __RPC__deref_out_ecount_full_opt(*iidCount) IID **iids) = 0;
        
        
virtual HRESULT STDMETHODCALLTYPE GetRuntimeClassName( 
            
/* [out] */ __RPC__deref_out_opt HSTRING *className) = 0;
        
        
virtual HRESULT STDMETHODCALLTYPE GetTrustLevel( 
            
/* [out] */ __RPC__out TrustLevel *trustLevel) = 0;
        
    };

IInspectable也继承于IUnknown, 它是WinRT所有对象的基接口, 所以WinRT还是基于COM技术。
其中GetTrustLevel返回信任等级, GetRuntimeClassName返回类名, 而GetIids返回当前类对象实现了哪些接口(所有接口的iid), 得到接口的iid后, 我们就可以通过QueryInterface查询我们需要的接口了, 得到接口指针就可以调用内部函数了。
最后总结下,回答下文章开头的问题, 很多人说COM过时了, 也许”纯正的标准COM“确实是使用的人越来越少了, 但是COM的思想却一直在后续的软件开发中被使用和发扬, 可以说COM技术是微软技术框架的“根”(之一)。
posted on 2013-07-20 16:59 Richard Wei 阅读(4587) 评论(6)  编辑 收藏 引用 所属分类: COM

FeedBack:
# re: COM思想的背后
2013-07-20 17:16 | WXX
个人以为COM是从技术框架层面实践了一些设计模式,而设计模式是相对稳定的东西,所以COM背后的思想也相对稳定。COM思想中也有很多不好的东西或者说不好理解的东西,比如:套件、Marshaling等完全可以用其它技术或者框架去替代。至于自动化、类型库这些,可能微软自己都不在推崇了,包括基于ATL框架的AX制作慢慢封死了。
COM是思想体现在ABI,以及对象模型的组织上,他自身提供了一些优秀的实践。  回复  更多评论
  
# re: COM思想的背后
2013-07-20 17:52 | Richard Wei
@WXX
同意, 关于套间、列集/散集, 内部涉及到LPC/RPC技术,微软隐藏了太多东西,尽管Windows操作系统本身用了不少这方面的技术, 但是外部开发人员却因为不好掌控而尽量不用。  回复  更多评论
  
# re: COM思想的背后
2013-07-22 09:25 | 永遇乐
微软通过修改VC编译器(提供新的关键字),让C++的对象之间拥了有一定的通信能力,这里面关键的就是这个iid,通过iid可以在运行时获取某个对象的成员函数指针,然后调用之,这就相当于对象与对象之间通信了。
这也达到了Object C中对象之间通信的效果(一个对象A给另一个对象B发消息M,B收到消息M后,能处理就处理,不能处理就算了)。
而在COM中,A对象要先看B对象能不能处理消息M,即A先查一下,B对象有没有处理M消息的那个成员函数HandleM() (即B对象中有没有哪个接口中有HandleM()方法),如果有(查到了),则调用HandleM(M)。没查到嘛,当然就算了。  回复  更多评论
  
# re: COM思想的背后
2013-07-23 16:58 | tb
嗯 是的 有很多还是比较实用的  回复  更多评论
  
# re: COM思想的背后
2013-07-27 00:35 | rain
这玩意最主要的一点是 太复杂了 套间 Marshaling这些概念把整个 com 原来一个很优秀简单的架构搞得很复杂,让人理解起来很晦涩!  回复  更多评论
  
# re: COM思想的背后
2013-10-24 03:52 | Archie
使用boost的shared_ptr以及C++的RTTI (dynamic_cast)可以轻易实现类似COM IUnknown的框架,并且更简单  回复  更多评论
  

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理