COM组件运作机制的初步探索

说明:《COM技术内幕》看了一半,感觉不错,但总觉得节奏太慢,有时作者还会扯到他的孩童时代,呵呵。总之,看这本书需要点耐性。基于这个原因,我做了一下整理,也许对于想快速 get started 的你会有所帮助。同时,也算是对我这段时间学习的一个整理吧。另外,对多态性及虚拟函数表的了解将有助于本文的理解。
 
我们将在本文中构造一个COM组件“原型”,来探索COM组件的运作机制。在这个“原型”中,我们无需包含任何与COM编程相关的头文件。与真正的COM程序相比,“原型”显得很粗糙,但我想它足以说明COM组件的运作机制。在下一篇文章中,我们将开始一个真正的但却简单的“COM编程实例”,然后学习稍微复杂点的“COM组件的包容及聚合”。在建立“原型”之前,我们先来了解一些COM组件的基本概念。
 
一、基本概念
 

图 1-1 简单的组件模型
 
如图1-1,COM组件是以Win32动态链接库(DLL)或可执行文件(EXE)发布的,我们称之为组件服务器;一个组件服务器,可以有一个或多个组件;而一个组件也可以有一个或多个接口。这里只讨论DLL形式的组件服务器。
 
为了更好的理解,这里列出了组件基本概念与C++实现的对应关系:
1、  接口:纯抽象基类
2、  组件:一个或多个“纯抽象基类”的实现类
3、  组件服务器:编译一个或多个“实现类”(还有DEF文件)生成的DLL文件
 
二、COM组件原型
1、  组件接口的查询
刚才已经提到过,接口对应于C++语言中的“纯抽象基类”,而组件对应于“纯抽象基类的实现”。如图1-2所示,IUnknown是所有接口的基类,组件ComponentA实现了InterfaceX和InterfaceY两个接口。
 
图1-2 组件与接口
 
客户程序(Client)总是通过接口来使用组件的,而接口的实现对于Client来说是不可见的。当Client创建一个组件ComponentA后――稍后将介绍“组件的创建”――所拥有的只是一个指向ComponentA对象的IUnknown指针,所以在使用组件接口InterfaceX(或InterfaceY)之前需要进行查询,也就是判断ComponentA是否实现了InterfaceX(或InterfaceY),并返回接口InterfaceX(或InterfaceY)的指针。
下面列出相关代码(详情请参阅原代码):
(1)       接口的定义
// Interface.h
 
#define interface struct
#define ULONG unsigned long
#define IID unsigned int
 
// 定义接口的唯一标识码, 这里简单的把它们定义成一些整数
// 在命令行中执行VC++提供的guidgen将生成128位的串
// 可以用一个结构体来存储这些位串,作为接口的唯一标识码,如:
// {A56003FE-32CF-41a8-9CD0-D1B9E91BA218}
// const GUID IID_Sample =
// { 0xa56003fe, 0x32cf, 0x41a8, { 0x9c, 0xd0, 0xd1, 0xb9, 0xe9, 0x1b, 0xa2, 0x18 } };
 
#define IID_IUNKNOWN 0
#define IID_IX                  1
#define IID_IY                  2
#define IID_IZ                  3
 
interface IUnknown
{
    virtual bool __stdcall QueryInterface( const IID iid, void **ppv )=0;
// 下面两个函数用于组件生命期的管理,稍后再作解释
    virtual ULONG __stdcall AddRef( void )=0;
    virtual ULONG __stdcall Release( void )=0;
};
 
interface InterfaceX : public IUnknown
{
    virtual void __stdcall XShow( void )=0;
};
 
interface InterfaceY : public IUnknown
{
    virtual void __stdcall YShow( void )=0;
};
(2)       组件的实现
class ComponentA : public InterfaceX, public InterfaceY
{
       ……
};
 
bool __stdcall ComponentA::QueryInterface( const IID iid, void **ppv )
{
       if ( iid == IID_IUNKNOWN )
       {
              *ppv = static_cast<IUnknown *>( static_cast<InterfaceX *>( this ) );
       }
       else if ( iid == IID_IX )
       {
              *ppv = static_cast<InterfaceX *>( this );
       }
       else if ( iid == IID_IY )
       {
              *ppv = static_cast<InterfaceY *>( this );
       }
       else
       {
              *ppv = NULL;
              return false;
       }
       AddRef();
       return true;
}
(3)       查询
假设我们创建了组件ComponentA的一个实例:
       IUnknown *pComponent = CreateInstance();
我们现在就可以进行接口InterfaceX(或InterfaceY)的查询及调用:
InterfaceX *pIX = NULL;
if ( pComponent->QueryInterface( IID_IX, (void **)&pIX ) )
{
       pIX->XShow();
       pIX->Release();
}
如果我们使用IID_IZ作为接口标识码进行查询,将返回false,由上面的代码可以看出,接口查询的过程其实就是“向上转型”的过程,即:把组件的this指针正确转换成接口InterfaceX(或InterfaceY)指针。下面是组件ComponentA一个实例的内存结构,从这个结构可以更好的理解查询过程:
 
图1-3 组件实例的内存结构
 
2、  组件的创建
因为组件的实现对于Client来说是不可见的,所以组件的“构造”只能由组件服务器(DLL)来完成;当Client不再需要某一组件时,组件的“析构”也是在组件服务器内部进行的(其实是组件的自我销毁,见下一部分内容)。同时,组件服务器必须为Client提供可调用的组件实例的创建方法:
       // Interface.h
       extern "C" IUnknown *CreateInstance( void );
 
       // Component.cpp
       IUnknown *CreateInstance( void )
{
       ComponentA *pComponent = new ComponentA();
       pComponent->AddRef();
 
       return static_cast<IUnknown *>( static_cast<InterfaceX *>( pComponent ) );
}
       为了导出可供Client调用的方法,还需要一个DEF文件:
// Component.def
 
LIBRARY                      Server.dll
DESCRIPTION             '(C) 2006-2008 Eric Chi'
EXPORTS
                            CreateInstance @1 PRIVATE
 
3、  组件生命期的管理
你可能已经注意到,在组件创建、接口查询时,调用了AddRef()函数,而在接口使用完后,调用了Release()函数。我们正是采用“对组件指针的引用”进行计数的方法,来管理组件生命期的。当有组件被创建或有新的指针指向该组件时,调用AddRef()对计数器加一;在指向该组件的指针不再活动之前(如:退出其作用域),调用Release()对计数器减一,并判断计数值是否为0,如果是,则组件进行自我销毁(delete this;)。佯细情况可参阅原代码文件Component.cpp中AddRef()、Release()函数。
另外,需要说明的是,这两个函数不是“线程安全”的。你可以调用InterLockedIncreament( &ref )或InterLockedDecreament( &ref )对计数器进行“线程安全”的递增或递减。但是,这需要包含windows.h头文件,而该文件中定义的IUnknown、IID等会与我们自定义的这些类型发生冲突,所以我们没有调用这两个“线程安全”的函数,而采用简单的 ++ 及 ―― 对计数器进行操作,因为我们的目标只是为了把COM组件的运作机制说明清楚。
4、  客户对组件接口的调用
// Client.cpp
#include “DllHelper.h”
typedef IUnknown *( *CREATE_FUNCTION )( void );
 
void *hDll = DllLoad( argv[1] );                    // 装载组件服务器
CREATE_FUNCTION CreateInstance          /* 获取“创建组件方法”的指针 */
= (CREATE_FUNCTION)DllGetFunction( hDll, "CreateInstance" );
              IUnknown *pComponent = CreateInstance();      // 创建组件
             
              // Query & call interfaces.
       InterfaceX *pIX = NULL;
       if ( pComponent->QueryInterface( IID_IX, (void **)&pIX ) )
       {
              pIX->XShow();
              pIX->Release();
       }
       pComponent->Release();       // 释放组件
       DllFree( hDll );                     // 释放组件服务器
 
三、关于原代码
 
图1-4 原代码文件的组织
DllHelper包含了与DLL相关的一些工具函数,如:DLL的装载和释放以及DLL中函数指针的获取。除了DllHelper,所有其他的原代码都是纯C++的,即:不包含任何windows的头文件。
 
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值