业精于勤,荒于嬉
全部博文(763)
分类: WINDOWS
2008-05-16 17:28:37
《com 技术内幕》
第1章 组件
1、COM,即组件对象模型,是关于如何建立组件以及如何通过组件建构应用程序的一个规范。
2、组件的优点:应用程序可随时间的流逝而发展变化;定制应用程序;组件库;分布式组件。
3、对组件的需求:组件必须动态连接;必须隐藏其内部实现细节。
4、COM组件是以Win32动态链接库(DLLs)或可执行文件(EXEs)的形式发布的可执行代码组成
的。遵循COM规范编写的组件将能够满足对组件家够的所有需求。COM组件是动态链接的,COM使
用DLL将组件动态链接起来。对于COM组件的封装是很容易的。COM组件按照一种标准的方式来宣
布他们的存在。COM组件是一种给其他应用程序提供面向对象的API或服务的极好方法。
5、COM并不是一种计算机语言。
6、将COM同DLL相提并论是不合适的。实际上COM使用了DLL来给组件提供动态链接的能力。
7、COM并不是像Win32API那样的函数集,它更主要的是一种编写能够按面向对象API形式提供服
务的组件的方法。
8、COM并不是类似于MFC这样的C++类库。COM给开发人员提供的是一种开发与语言无关的组件库
的方法,但COM本身并没有提供任何实现。
9、COM具有一个被称作是COM库的API,它提供的是对所有客户及组件都非常有用的组件管理服务
。
////////////////////////////////////////////////////////////////////
第2章 接口
1、在COM中接口就是一切。
(1)接口可以保护系统免首外界变化的影响。
(2)接口可以使客户用同样的方式来处理不同的组件。
2、(1)COM接口在C++中是用纯抽象基类实现的。
(2)一个COM组件可以提供多个接口。
(3)一个C++类可以使用多继承来实现一个可以提供多个接口的组件。
3、类并非组件。
4、接口并非总是继承的。对接口的继承只不过是一种实现细节而已。除了可以使用一个类来实
现几个不同的接口外,还可以用单个的类来实现每一个接口再使用指向这些类的指针。
5、组件可以支持任意数目的接口。为支持多重接口,可以使用多重继承。支持多重接口的组件
可以被看作是接口的集合。
6、COM接口的不变性、多态以及接口继承。
(1)一旦公布了一个接口,那么它将永远保持不变。当对组件进行升级时,一般不会修改已有
的接口,而是加入一些新的接口。
(2)多态指的是可以按同一种方式来处理不同的对象。
7、虚拟函数表(vtbl):包含一组指向虚拟函数实现的指针。
定义一个纯抽象基类也就是定义了相应的内存结构。但此内存只是在派生类中实现此抽象基类时
才会被分配。当派生类继承一个抽象基类时,它将继承此内存结构。
8、在COM中,对一个组件的访问只能通过函数完成,而绝不能直接通过变量。
9、接口的真正的威力在于继承此接口的所有类均可以被客户按同一方式进行处理。
////////////////////////////////////////////////////////////
第3章 QueryInterface函数
1、接口查询:
客户同组件的交互都是通过一个接口完成的。在客户查询组件的其他接口时,也是通过接口完成
的。这个接口就是IUnknown。
IUnknown接口的定义包含在Win32 SDK中的UNKNOWN.H头文件中。
interface IUnknown
{
virtual HRESULT _stdcall QueryInterface(const IID& iid,void **ppv) = 0;
virtual ULONG _stdcall AddRef() = 0;
virtual ULONG _stdcall Release() = 0;
}
在IUnknown中定义了一个名为QueryInterface的函数。客户可以调用QueryInterface来决定组件
是否支持某个特定的接口。
2、所有的COM接口都需要继承IUnknown。
3、由于所有的COM接口都继承了IUnknown,每个接口的vtbl中的前三个函数都是
QueryInterface,AddRef和Release。若某个接口的vtbl中的前三个函数不是这三个,那么它将不
是一个COM接口。由于所有的接口都是从IUnknown 继承的,因此所有的接口都支持
QueryInterface.因此组件的任何一个接口都可以被客户用来获取它所支持的其他接口。
4、非虚拟继承:注意IUnknown并不是虚拟基类,所以COM接口并不能按虚拟方式继承IUnknown,
这是由于会导致与COM不兼容的vtbl。若COM接口按虚拟方式继承IUnknown,那么COM接口的vtbl
中的头三个函数指向的将不是IUnknown的三个成员函数。
5、一个QuertyInterface可以用一个简单的if-then-else语句实现,但case语句是无法用的,因
为接口标识符是一个结构而不是一个数。
6、多重类型及类型转换
7、QueryInterface的规则
(1)QueryInterface返回的总是同一IUnknown指针。
(2)若客户曾经获取过某个接口,那么它将总能获取此接口。
(3)客户可以再次获取已经拥有的接口。
(4)客户可以从任何接口返回到起始接口。
(5)若能够从某个借口获取某特定接口,那么可以从任意接口都将可以获取此接口。
8、接口的IID决定了它的版本。当改变了下列条件中的任何一个时,就应给新接口指定新的ID:
(1)接口中函数的数目。
(2)接口中函数的是顺序。
(3)某个函数的参数。
(4)某个函数参数的顺序。
(5)某个函数参数的类型。
(6)函数可能的返回值。
(7)函数参数的含义。
(8)接口中函数的含义。
9、避免违反隐含和约:
(1)使接口不论在其成员函数怎么被调用都能正常工作。
(2)强制客户按一定的方式来使用此接口并在文档中将这一点说明清楚。
//////////////////////////////////////////////////////////////////
第4章 引用计数
1、生命期控制
IUnknown的另外两个成员函数AddRef和Release的作用就是给客户提供一种让它指示何时处理完
一个接口的手段。
2、AddRef和Release实现的是一种名为引用计数的内存管理技术。
引用计数是使组件能够自己将自己删除的最简单同时也是效率最高的方法。
COM组件将维护一个称做是引用计数的数值。当客户从组件取得一个接口时,此引用计数值将增1
。当客户使用完某个接口后,组件的引用计数值将减1。当引用计数值为0时,组件即可将自己从
内存中删除。
3、正确使用引用计数规则:
(1)在返回之前调用AddRef。对于那些返回接口指针的函数,在返回之前应用相应的指针调用
AddRef。这些函数包括QueryInterface及CreateInstance。这样当客户从这种函数得到一个接口
后,它将无需调用AddRef。
(2)在使用完接口之后调用Release。在使用完某个接口之后应调用此接口的Release函数。
(3)在赋值之后调用AddRef。在将一个接口指针赋给另外一个接口指针时,应调用AddRef。换
句话说,在建立接口的另外一个引用之后应增加相应组件的引用计数。
4、在客户看来,引用计数是处于接口级上而不是组件级上的。
5、为什么选择为每一个接口单独维护一个引用计数而不是针对整个组件维护引用计数?(1)使
程序调试更为方便;(2)支持资源的按需获取。
6、AddRef&Release的例子
ULONG _stdcall AddRef()
{
return InterlockedIncrement(&m_cRef);
}
ULONG _stdcall Release()
{
if(InterlockedDecrement(&m_cRef)
{
delete this;
return 0;
}
return m_cRef;
}
7、当建立一个新组件时,应建立一个对此组件的引用。因此创建组件时,在将指针返回给客户
之前,应该增大组件的引用计数值。这使程序员可以不必在调用CreateInstance 或
QueryInterface之后记着去调用AddRef。
8、引用计数规则优化:
(1)输出参数规则:任何在输出参数中或作诶返回值返回一个新的接口指针的函数必须对此接
口指针调用AddRef。
(2)输入参数规则:对传入函数的接口指针,无需调用AddRef和Release,这是因为函数的生命
期嵌套在调用者的生命周期内。
(3)输入-输出函数规则:对于用输入-输出参数传递进来的接口指针,必须在给它赋另外一个
接口指针之前调用其Release。在函数返回之前,还必须对输出参数中所保存的接口指针调用
AddRef。如:
void ExchangeForCachedPtr( int i, IX **ppIX)
{
(*ppIX)->Fx(); //Do something with in-parameter.
(*ppIX)->Release();//Release in parameter.
*ppIX = g_Cache[i];//Get cached pointer.
(*ppIX)->AddRef();//AddRef pointer.
(*ppIX)->Fx();//Do something with out-parameter.
}
(4)局部变量规则:对于局部复制的接口指针,由于它们只是在函数的生命周期内才存在,因
此无需调用AddRef和Release。
(5)全局变量规则:对于保存在全局变量中的接口指针,在将其传递给另外一个函数之前,必
须调用其AddRef。由于此变量是全局的,因此任何函数都可以通过调用其Release来终止其生命
期。对于保存在成员变量中的接口指针,也应按此种方式进行处理。因为类中的任何成员函数都
可以改变次中接口指针的状态。
(6)不能确定时的规则:对于任何不能确定的情形,都应调用AddRef和Release对。
////////////////////////////////////////////////////////////////////
第5章 动态链接
1、从DLL中输出函数:用extern "c"标记。
2、在使用VC时,可以用DUMPBIN。EXE来得到某个DLL中所输出的符号的清单。如下面的命令:
dumpbin -exports Cmpnt1.dll
3、装载DLL:LoadLibrary以被装载的DLL的名称作为参数并返回一个指向所装载的DLL的句柄。
win32的GetProcAddress函数可以使用此句柄以及待用的函数的名称,然后返回一个指向次函数
的指针。
4、使用DLL实现组件的原因:DLL可以共享它们所链入的应用程序的地址空间。
//////////////////////////////////////////////////////////////////////
第6章 关于HRESULT、GUID、注册表及其他细节
1、HRESULT值的结构:
_________________________________________________________
| | |
|
|
|
15bits设备代码
|
16bits返回代码
|
|__|_________________________|______________________________|
31
30 16 15
0
2、常用的HRESULT值:
3、一般不能直接将HRESULT值同某个成功代码(如S_OK)进行比较以决定某个函数是否成功也不
能直接将其同某个失败代码(如E_FAIL)进行比较以决定函数调用是否失败。应该使用
SECCEEDED和FAILED宏。
HRESULT hr = CoCreateInstance(...);
if(FAILED(hr))
return ;
hr = pI->QueryInterface(...);
if(SUCCEEDED(hr))
{
pIX->Fx();
pIX->Release();
}
pI->Release();
4、当前所定义的设备代码:
——————————————————————————————————
FACILITY_WINDOWS 8
FACILITY_STORAGE 3
FACILITY_SSPI 9
FACILITY_RPC 1
FACILITY_WIN32 7
FACILITY_CONTROL 10
FACILITY_NULL 0
FACILITY_ITF 4
FACILITY_DISPATCH 2
FACILITY_CERT 11
——————————————————————————————————
5、关于定义自己的HRESULT的一些一般性规则:
(1)不要将0X0000及IX01FF范围内的值作为返回代码。这些值是为COM所定义的FACILITY_ITF代
码而保留的。只有遵循这一规则,才不致使用户自己定义的代码同COM所定义的代码相混淆。
(2)不要传播FACILITY_ITF错误代码。
(3)尽可能使用通用的COM成功及失败代码。
(4)避免定义自己的HRESULT,而可以在函数中使用一个输出参数。
6、用MAKE_HRESULT宏来定义一个HRESULT值,此宏可根据所提供的严重级别、设备代码及返回代
码生成一个HRESULT值。如:
MAKE_HRESULT(SEVERITY_ERROR,FACILITY_ITF,100);
7、GUID是英文Globally Unique Identifier(全局唯一标识符)的首字母缩写。IID是一个128比
特(16)字节的一个GUID结构。
8、生成GUID :UUIDGEN.EXE和GUIDGEN.EXE
9、GUID的比较:操作符==;等价函数IsEqualGUID,IsEqualIID,IsEqualCLSID。
10、将GUID作为组件标识符
11、由于一个GUID值占用了16个字节,因此一般不用值传递GUID参数。而大量使用的是按引用传
递。
12、COM只使用了注册表的一个分支:HKEY_CLASSES_ROOT。
13、注册表CLSID是一个具有如下格式的串:
{********-****-****-****-************}
14、CLSID关键字的子关键字InprocServer32关键字的缺省值是组件所在的DLL文件名称。
15、一些特殊关键字:
(1)AppID:此关键字下的子关键字的作用是将某个APPID(应用程序ID)隐射成某个远程服务
器名称。分布式COM将用到此关键字。
(2)组见类别:注册表的这一分支可以将CATID(组件类别ID)映射成某个特定的组件类别。
(3)Interface:用于将IID映射成与某个接口相关的信息。
(4)Licenses:保存授权使用COM组件的一些许可信息。
(5)TypeLib:类型库关键字所保存的是关于接口成员函数所用参数的信息等。
16、ProgID命名约定:
17、从ProgID到CLSID的转换:COM库函数:CLSIDFromProgID和ProgIdFromCLSID:
CLSID clsid;
CLSIDFromProgID("****.****.****",&clsid);
18、自注册:DLL一定要输出下边两个函数:
STDAPI DllRegisterServer();
STDAPI DllUnregisterServer();
用户可以使用程序REGSVR32.EXE来注册某个组件,它实际上是通过上述函数来完成组件的注册的
。
19、组件类别使开发人员能够使开发人员无需创建组件实例就能决定它是否特工所需接口。一个
组件类别实际上就是一个接口集合,该集合将被分配给一个GUID,此GUID此时被称做是CATID。
对于某个组件而言,若它实现了某个组件类别的所有接口,那么它可以将其注册成该组件类别的
一个成员。这样,客户就能够通过从注册表中选择只属于某个特定组件类别的组件而准确找到它
所需的组件。
20、组件类别的用途:指定某个组件必须实现的接口集合;用于指定组件需要其客户提供的接口
集合。
22、在使用COM库中的其他函数(除CoBuildVersion外,此函数将返回COM库的版本号)之前,进
程必须先调用CoInitialize来初始化COM库函数。当进程不再需要使用COM库函数时,必须调用
CoUninitialize。对每一个进程,COM库函数只需初始化一次。这并不是说不能多次调用
CoInitailize,但需保证每一个CoInitialize都有一个相应的CoUnoinitialize调用。当进程已
经调用过CoInitialize后,再次调用此函数所得到的返回值将是S_FALSE而不再是S_OK.
23、OLE是建立在COM基础之上的,它增加了对类型库、剪贴板、拖放、ActiveX文档、自动化以
及ActiveX控件的支持。在OLE库中包含对这些特性的额外的支持。在需要使用这些特性时,应调
用OleInitailize及OleUninitialize,而不是CoInitailize和 CoUninitialize。Ole*函数将调用
Co*函数。但若程序中没有用到那些额外的功能,使用Ole*将会造成资源的浪费。
24、COM中分配和释放内存的标准方法:任务内存分配器。使用此分配器,组件可以给客户提供
一块可以由客户删除的内存。可在多线程应用程序中使用。
一些方便的函数:
void *CoTaskMemAlloc(
ULONG cb //size in bytes of block to be allocated
);
void CoTaskMemFree(
void *pv //pointer to memory block to be freed
);
25、StringFromGUID2可以将某个GUID转换成一个字符串:
wchar_t szCLSID[39];
int r = ::StringFromGUID2(CLSID_Component1,szCLSID,39);
传给StringFromGUID2的参数是一个Unicode串(即一个宽字符wchar_t类型的数组而不是char类
型的字符数组)。在非Unicode的系统中,需要将结果转化为单字节字符(char)。为此,可以使
用ANSI的wcstombs函数如下:
#ifndef _UNICODE
char szCLSID_single[39];
wcstombs(szCLSID_single,szCLSID,39);
#end if
26、
----------------------------------------------------------------------------
函数 用途
-----------------------------------------------------------------------------
StringFromCLSID 将CLSID转化成文本串
-----------------------------------------------------------------------------
StringFromIID 将IID转化成文本串
------------------------------------------------------------------------------
StringFromGUID2 将GUID转化成文本串。此串将被存放在调用者所分配的缓冲区中
------------------------------------------------------------------------------
CLSIDFromString 将一个文本串转化成CLSID
------------------------------------------------------------------------------
IIDFromString 将一个文本串转化成IID