BTW: 从今天开始,慢慢将自己的东西放到blog上来,由于之前写的技术文档都是doc,并且嵌入了viso等东西,可能网页无法显示。声明这些文档仅是私人,并参考了一些前辈的东西。
Com实例
Document Information:
Owner: | Li Zhang |
Date: | 2009.02.23 |
Version: | 0.01 |
Version History:
Version | Date | Author | Summary of Changes | Details of Changes |
0.01 | 2009.02.23 | Li Zhang | First Draft | |
参考书籍:
|
- C/C++ 虚函数(讨论虚函数,继承,引用等C++如何实现)
实验1: C对象布局
C++中,一个对象在内存中只存放其成员变量,所有的对象共享类函数。那么C++中类函数如何实现,及虚函数如何实现呢?首先我们看C++对象的内存布局。Let’s start from the simplest!
C++
对象的内存布局已了解了,那么
C++
中虚函数是什么呢?你方才大谈设置虚函数表这些东西,不会你是个老学究吧?是怎样实现的呢?
其实
C++
虚函数只是一个小花招而已,也是一个平淡无奇的函数。如果了解了虚函数,其实对
C++
是一个安全漏洞。如果拥有
C++
一个函数对象,其实就可以对整个程序为所欲为
(这个漏洞利用本文不会涉及,如果你对本文说的汇编这些东西了解,有学过编译原理,操作系统,那么如果你得到一个函数地址,知道这个函数存在
.rdata
区,你就会很
Happy
了!)
。
Let’s start at the begin!
源码:
执行结果: 执行解释: God! Base不是只有一个成员变量 int m_integer;吗,他的对象大小应该是4字节呀? Let’s do like hacker does! 源码:
执行结果: 执行解释:
怎么对象b的地址与其成员变量 m_nInteger地址不一致?不是对象仅是存储成员变量吗? Sorry,it’s my fault! 对象除了存储成员变量外,还存储一个神秘的东西,这个神秘的东西的值为 0x40A1E8. 是不是觉得这个值如此的眼熟呢?在VC编译器中,一般默认的main函数加载的进程虚拟地址为0x401000. 0x40AlE8 是不是也是函数地址呢? **C语言者最爱: 在Windows中,VC编译器一般默认的将main函数在进程中的虚拟地址设为0x00401000,如果您有汇编的经验,想想.COM这种汇编代码中,有个很有趣的0x100之类的数字,这个道理相似,不过时间有限,不深入了,HA HA ! 对于一个windows进程来说他有几个区(section)。Let’s have a look! 源码:
执行结果: 执行解释: 由结果可知 全局变量,全局static变量,函数static变量,类static变量地址都在0x40cf6*处,而函数局部变量,类成员变量都是在0x12ff6*左右。如何解释呢? 全局变量,static变量(函数也好,类成员变量也罢)都会在.exe中分配一个地址,他在编译器生成的汇编代码中,存储在数据区,而局部变量是不存储执行文件中的,它是动态的在函数堆栈中分配的。我们看下windows中执行文件(PE)的代码区与数据区。 命令行:dumpbin test.exe 执行结果: Test.exe有三个区.data , .rdata, .text 。 稍微说明一下。
可以看到main函数起始于0X00401000,结束于: 可以粗糙的认为.text区起始于0x00401000,结束于0x40A000; 在windows中,其内存一个page是4kb,及 0x1000. .text区可以看成是9个page大小. 2) .data; 存放数据。我们看下其起始及结束地址。 Dumpbin /rawdata:1 /section:.data test.exe|more 结束于: 可以粗糙的认为.data 区范围为 0x40C000 – 0X40D000, 相当于1个page大小
这三个区是exe文件中数据存放格式,与进程有何关系呢? 由于windows exe loader,在启动一个exe进程时,他会将exe的数据存放到进程的虚拟地址空间,怎样来确定这些数据的存放地址呢?就是按照他在exe中说明的位置。如:main函数在exe中规定为0x401000,那么exe被load到操作系统中时,main函数在虚拟地址空间对应的地址就是0x401000. 进程虚拟地址空间布局大致如下: 再回到上次的执行结果解释: 可见全局变量, staic变量 存储在.data区。关于new malloc 的地址分配在什么地方,堆栈在进程中什么位置,限于篇幅,点到为止,请采用上文中类似的方法,Try it and have fun! 让我们回到0x40A1E8这个奇怪的地址,由上文可知,这个存放在只读数据区.rdata. 所以是一个只读数据。让我们看一下test2.exe的0x40A1E8处有什么神秘的东西。 执行命令: dumpbin /rawdata:bytes /section:.rdata test2.exe|more God! 如此熟悉的数值 0x401070, 这个是一个代码区.text中地址。Let’s have a look! 见鬼了!如此熟悉的代码:我们看下test2.obj中的代码: 命令: objdump -d test2.obj 完全一致,如果从数值来看,因为0xffffffffc的值是-4. 而这该死的代码是Base::Show(); 所以Base b对象第一个字节存储的是一个指针,这个指针指向.data区某值,而这个值是一个函数指针指向Base某个类函数。 那么如何来解释这个问题呢?为什么Base对象的前四个字节,指向一块内存,而这块内存的值又指向Base的虚函数Show呢? 让我们看下test2.exe 执行的汇编代码: dumpbin /disasm /section:.text test2.exe|more b.show()的执行代码 对应 lea ecx,[ebp-8] ; // 存储this指针 call 00401070; //调用00401070处show代码。 ** C语言者最爱: E8 ->对应call 命令; 0x000005A是偏移值。是相对于当前命令代码的最后一个字节即 0x00401016;所以真正的函数调用地址为0x00401016+0x5A = 0X00401070; 这个0x00401070与Base b; 对象的第一个字节(0x0040A1E8)又有何关系呢? 其实这是该死的C++编译器做的优化,编译器生成的代码首先获取类对象的虚函数表指针,这个指针值为0x0040A1E8,及虚函数表位置在0X0040A1E8;然后调用虚函数表中对应的项。如果在编译期间能够确定指向的是基类还是派生类,这些代码就被编译器优化为指向的对应函数地址;如果只在运行期间确定,编译器并不做优化。 对于本例:编译器知道这个是基类,真正调用对应的函数指针 0X0040A1E8->0X00401070. 编译器把这段寻址代码优化为 call 0x00401070。 我们看下动态未经优化能够真实说明虚函数的例子:
看下能够透视虚函数机制的check函数汇编代码。 执行解释: ebp+8 存储的是Extend * b; mov eax,dword ptr[ebp+8]; // 将指针b值存入eax mov edx,dword ptr[eax]; // 装入b对象,b对象内存中的布局 vtbl指针+成员变量 //edx 中此时存放的vtbl指针。 mov eax,dword ptr[edx]; //将vtbl虚函数表的第一项虚函数指针装载到eax call eax; // 此时eax中是vtbl第一项函数指针 对于Hello函数: mov eax,dword ptr[edx+4]; call eax; // 装入虚函数表中第二项函数指针,调用第二项函数。 那么虚函数表中是如何布局的? 当编译器编译一个类时,它会按照这个类虚函数在源码中的声明的先后来决定其在虚函数表中的位置。如在此例中,虚函数声明次序为Show,Hello.所以其虚函数中其位置是: 当指向某个对象的虚函数时,会查找其对象的虚函数表,然后更具函数在虚函数表中的位置,调用对应的函数。 如 b->Hello(); 由于在源码中Hello是第二个虚函数,所以其在虚函数表中第二个位置。程序执行b->Hello(); 时,程序查找对象的指向的虚函数表二项,然后调用此函数。 在C++中相同的类 其对象共享虚函数表。 |
C++
具有多态的性质,那么它是如何利用虚函数实现的呢?
每个
C++
对象,其内存布局如上所叙,如果其有虚函数,则第一个四字节指向其类的虚函数表,其后才存储其成员变量。如果无虚函数,则只存储其成员变量。如果一个基类有虚函数,其继承类其虚函数表在对象初始化时
C++
编译器做了一个小花招来完成其
多态
。
C++
对象初始化时,如果其有父类,首先初始化父类;如果有虚函数,则修正其虚函数表;如果有成员变量,初始化成员变量;调用对象构造函数。那么在这初始化过程中,其虚函数表有产生生么魔力呢?
假设:
class A { public: virtual void A1(); vitual void A2(); }; class B : public A { public: Virtual void B1(); Virtual void B2(); }; class C: public B { public: virtual void C1(); virtual void C2(); }; 如果代码: C* test = new C();
如上图的虚函数表的分布,应该能够很清晰的了解多态性质?当一个子类被当成父类对象时,其虚函数表指针仍指向子类的虚函数表指针,且子类虚函数表的前几项与其父类,完全一致。关于多重继承,其虚函数表的变化请参照后文。 |
实验
2
:(编译并运行
test3.cpp
)让我们融合一下上文所有的知识。
//Test3.cpp #include <iostream> class BaseFlag { public: BaseFlag() { std::cout<<"BaseFlag::BaseFlag()"<<std::endl; } ~BaseFlag() { std::cout<<"BaseFlag::~BaseFlag()"<<std::endl; } }; class ExtendFlag { public: ExtendFlag() { std::cout<<"ExtendFlag::ExtendFlag()"<<std::endl; } ~ExtendFlag() { std::cout<<"ExtendFlag::~ExtendFlag()"<<std::endl; } }; class Base { public: Base() { std::cout<<"Base::Base()"<<std::endl; } ~Base() { std::cout<<"Base::~Base()"<<std::endl; } virtual void Show() { std::cout<<"Base::Show()"<<std::endl; } void Hello() { std::cout<<"Base::Hello()"<<std::endl; } BaseFlag bf; }; class Extend : public Base { public: Extend() { std::cout<<"Extend::Extend()"<<std::endl; } ~Extend() { std::cout<<"Extend::~Extend()"<<std::endl; } virtual void Show() { std::cout<<"Extend::Show()"<<std::endl; } void Hello() { std::cout<<"Extend::Hello()"<<std::endl; } ExtendFlag ef; }; void Check(Base base) { base.Show(); base.Hello(); } int main() { Extend e; Check(e); } |
运行结果
:
E:/com/2.24/vt>test1 BaseFlag::BaseFlag() ---(1) Base::Base() ---(2) ExtendFlag::ExtendFlag() ---(3) Extend::Extend() ---(4) Base::Show() ---(5) Base::Hello() ---(6) Base::~Base() ---(7) BaseFlag::~BaseFlag() ---(8) Extend::~Extend() ---(9) ExtendFlag::~ExtendFlag() ---(10) Base::~Base() ---(11) BaseFlag::~BaseFlag() ---(12) |
|
执行解释:
2) Check(e); Check函数原型为: void Check(Base base);
check中没有打印Extend方法,那C++多态不是骗人吗?
其实主要原因在于check中采用的是对象值传递,这就导致调用了Base的拷贝构造函数,而这个天杀的拷贝构造函数会重新设置对象的vtbl指针。如果采用对象指针传递,一且就可以工作了: 在C/C++函数参数传递是按值传递。如果传递一个类对象时,会传递一个类对象的副本,通过类的拷贝构造函数来创建这个类的副本。拷贝构造函数会重新设置对象的vtbl指针。但传递对象指针时,值传递的是指针,指针复制过去,其所指向的对象的vtbl指针是不会改变的,所以保持了子类的vtbl,所以就可调用子类的方法。 ** 小知识: 当一个子类覆盖父类的方法时: 如下图: 设B继承自A, A有虚函数(A1,A2, Show), B重写了(show,并增加B1)
|
啰啰嗦嗦的说了半天,这个文档不是关于
COM
的吗?怎么还没说到
COM
呢?
对
C++
对象及其虚函数有个深入的了解,对
COM
的了解就会水到渠来。所以还要试验一下虚函数的另一个性质:
多重继承。这个对
COM
也很重要。当然也要实验下虚拟继承。
上文只讨论了但继承虚函数是如何实现,及运作的。这个对于
Java
这种单继承(
Object
)已经足够了。但
C++
是一个能容忍多继承的一种语言。
Let’s start at the
Begin!
执行结果: 执行解释: 由于class A,B 都没有成员变量唯有虚函数,并且没有多重父类。所以其内存进存储虚函数标识指针,所以其大小都为四字节。
|
C++对象中如果有复杂的对象关系,如果类D继承自类B,C,同时类B,C又继承自A(如图): 如果按照一般方式的继承,那么D中有两份A,所以可以采取virtual public继承,当采用此种方式,对象继承中不存储多份父类对象。不过COM中不能采取这种虚拟继承。虚拟继承其实现及规则,鉴于篇幅,略去。 |
- COM 简介。(以下内容参考自<<COM技术内幕>>)
COM
不是一种计算机语言,仅是说明如何创建组件的规范。
COM
规范中应用程序可以看成若干组件构成:
程序员可以编写自己定制的组件,注册到
windows
注册表中,
windows
会把这个组件形象的容纳如操作系统的组件库。当某个程序员需要这个组件的功能,可以在程序中直接调用这个组件的函数接口。这种形式类似如调用
C
库函数,但与之不同的是这种调用是动态的,如果
C
库函数更新,那么应用程序需要重新编译。但如果组件更新了,应用程序无需重新编译,这是因为组件会保持且调用接口不变,虽然可能其内部实现有所更改。那么这种组件形式不就是
DLL
吗?组件的形式可以看成
DLL,
但与
DLL
是不同的。
COM
是用
DLL
来给组件提供动态连接,
COM
可以形象的看成是物理,而
DLL
可以看成微积分,微积分的方法可以解决物理中的方程问题。
DLL
是实现
COM
动态连接的一种方法,当然还也别的实现形式。
COM
组件是
WIN32
动态链接库(
DLL
)或可执行文件
(EXE)
可执行代码组成成的。由于
COM
组件以二进制形式发布的,任何用户调用
COM
组件都是要被加载到
Windows
操作系统中,都是以二进制形式调用,也就是函数调用的时候可以看成是基于机器码的调用,再高级一点可以看成汇编代码的调用,任何语言在汇编语言都是无差的,所以
COM
与编程语言无关的。
COM
具有一个被称为
COM
库的
API
,它提供给
Client
与组件组件管理服务。
COM
(进程内组件)服务原理(宏观):
-
- COM组件将自己注册到Windows注册表中。此组件有唯一标示的GUID, Windows注册表将此GUID与注册的COM组件相映射,(确切的说注册表中将CLSD_Hello这个GUID与server.dll所在文件路径练习起来。可以形象的看成CLSID_Hello与E:/InProc/server/server.dll是一个pair关系,Windows可以通过CLSID_Hello找到server.dll,找到server.dll,windows就可以加载)。
- 应用程序当需要调用COM组件时,只需要调用COM库中用于创建组件的函数,如CoCreateInstance,CoGetClassObject等,而这些函数需要所要调用的COM组件的GUID。如CoCreateInstance(.. CLSID_Hello…),CoCreateInstance会在注册表中查找CLSID_Hello,从而找到E:/InProc/Server/Server.dll这个路径,并将server.dll加载到应用程序中,从而应用程序可以调用server.dll提供的功能。
COM
组件是一个接口(
interface
)集,
Client
可以向
COM
组件查询接口,
COM
每个接口都是一种类似
C++:
Interface IX { Public: Virtual void show() = 0; Virtual void Hello() = 0; }; |
如果
COM
组件支持此接口,那么会返回给
Client
一个此接口的实现对象的指针,
Client
可以通过这个返回的接口指针,调用这个接口提供的功能。
Interface IX
并不是一个
COM
接口,所有的
COM
接口必须继承自一个名为
IUnknown
的
interface. IUnknown
定义于
WIN32 SDK Unknwn.h
中:
Interface IUnknown { Virtual HRESULT __stdcall QueryInterface(const IID& iid,void** ppv) = 0; Virtual HRESULT __stdcall AddRef() = 0; Virtual HRESULT __stdcall Release() = 0; }; |
- COM库头文件中的interface与C++中interface 关键词是有不同意义的,在objbase.h中COM库重新定义了interface关键词:
#define __STRUCT__ struct #define interface __STRUCT__ |
在COM库中将interface 定义成了struct。 由于struct 与class之间差别在于: class成员默认属性是private, 而struct是 public。所以所有直接include COM库头文件objbase.h或间接inlcude objbase.h,其中 interface的定义都会是一个struct关键词。
- Client与组件的交互都是通过COM接口完成的,组件不仅包含一个接口集,组件本身也是COM接口。
- 所有的COM接口都继承自IUnknown, 而IUnknown定义了三个纯虚函数,所以每个COM接口的vtbl前三个函数都是QueryInterface, AddRef, Release. 这样每个COM接口都可以作为IUnknown接口来操作。如果的到一个IUnknown接口指针,就可以通过IUnknwon::QueryInterface来查询此接口所支持的其他COM接口。
- Client可以调用COM组件的QueryInterface函数,以判断组件是否支持某个特定接口。如果支持则返回一个指向此接口的指针,否则返回一个错误代码。其函数原型:
HRESULT __stdcall QueryInterface(const IID& iid,void** ppv); |
- HRESULT: (Here is the result); 它是一个特定接口的32bit.其结构:
QueryInterface可以返回S_OK, E_NOINTERFACE. 可以使用SUCCEEDED, FAILED宏来判断这个返回值,以确定调用是否成功。判断是否调用成功主要参考31bit,如果是1则表示失败,是0则成功。SUUCCEEDED 与FAILED宏就是利用31bit判断是否成功的。
- __stdcall;
C/C++全局函数默认调用方式是__cdecl( c language default call),而C++类函数是__thiscall标示有this指针的传递。
__cdecl: 函数参数自右至左入栈,函数调用这负责清除栈中的参数。如:
int add(int x,int y) {
return x+y;
}
int main()
{
int a = 2;
int b = 3;
int c = add(a,b);
}
首先看下汇编的函数调用,假设A调用B, 当调用某个函数时,A首先将参数入栈,然后存放当前代码的执行地址入栈,然后调用B,B首先保存A函数的栈基址,这样当B返回时,可以恢复函数A的堆栈。如果B的原型为: void B(int x,int y);则栈的形式如下:所以如果B函数要引用参数必须是 0x8+ebp;同理y: 0xd+ebp
add汇编代码如下:
add: Push %ebp; // 调用add的函数栈基址入栈
Mov %esp, %ebp; // ebp指向add函数的栈基址
Mov 0x8(%ebp),eax; // 将x参数取出,存入eax中。
Add 0xc(%ebp),eax; // 将 y参数取出,并入eax中x相加。
-
-
-
// 函数调用返回都将返回参数入eax寄存// 器中,现在返回值已在eax中。
-
-
Pop %ebp //恢复调用add的函数的栈基址。
Ret //add函数返回
Main汇编代码如下:
Main: Push %ebp; // 将调用main的函数的C/C++启动函数栈基址入栈
Mov %esp,%ebp; // ebp指向main函数的栈基址
-
-
Sub %0xc, %esp; // 由于main函数中有三个int局部变量,所以要// 在堆栈中分配12个字节,%esp-0xc,在栈中分
-
Movl $0x2,-4(%ebp); // ebp-0x4,a的存储地址。给a赋值2;
Movl $0x3,-8(%ebp); // ebp-0x8, b地址,b赋值为3
Mov -8(%ebp), %eax; //取出b值
Push %eax; //将b值入栈 (add参数y)
Mov -4(%ebp), %ecx; //取出a值
Push %ecx; //将a值入栈 (add参数x)
Call add ; //调用add函数
Add $0x8,%esp; // 清除调用add main的入栈参数x,y。
Mov %eax, -0xc(%ebp); // 将add返回值存入到c中
Xor %eax,%eax ; //main函数执行成功,返回0,置eax 0;
Mov %ebp,%esp; // esp栈指针恢复到main函数起始处
Pop %ebp //恢复调用main函数的栈基址
Ret; //main函数返回
可见__cdecl方式是参数由右至左入栈,且调用函数main清除因参数入栈所带来的栈分配(push eax; push ecx;(相当如 sup 0x8,%esp)在栈中存储了x,y参数。必须清除,所以当add返回时,main调用add 0x8,%esp;清除参数).
-
__stdcall
,与
__cdecl
略有不同,参数也是自右至左入栈,但其参数清除却是由
被调用函数清除。即如上例如果
add
是
__stdcall,
则
add
在返回时必须要清除参数
x,y
在栈中的分配的地址。
Int __stdcall add(int x,int y) {
return x+y;
}
int main()
{
int a = 2;
int b = 3;
int c = add(a,b);
}
add汇编代码如下:add: Push %ebp; // 调用add的函数栈基址入栈
Mov %esp, %ebp; // ebp指向add函数的栈基址
Mov 0x8(%ebp),eax; // 将x参数取出,存入eax中。
Add 0xc(%ebp),eax; // 将 y参数取出,并入eax中x相加。
-
-
-
// 函数调用返回都将返回参数入eax寄存// 器中,现在返回值已在eax中。
-
-
Pop %ebp //恢复调用add的函数的栈基址。
-
-
-
Ret $0x8 //add函数返回,同时esp+0x8.清除堆栈中//add的参数。
-
-
Main汇编代码如下:Main: Push %ebp; // 将调用main的函数的C/C++启动函数栈基址入栈
Mov %esp,%ebp; // ebp指向main函数的栈基址
-
-
Sub %0xc, %esp; // 由于main函数中有三个int局部变量,所以要// 在堆栈中分配12个字节,%esp-0xc,在栈中分
-
-
-
//配了12个字节。
-
Movl $0x2,-4(%ebp); // ebp-0x4,a的存储地址。给a赋值2;
Movl $0x3,-8(%ebp); // ebp-0x8, b地址,b赋值为3
Mov -8(%ebp), %eax; //取出b值
Push %eax; //将b值入栈 (add参数y)
Mov -4(%ebp), %ecx; //取出a值
Push %ecx; //将a值入栈 (add参数x)
-
-
-
Call add ; //调用add函数,调用返回后main没有清除//add参数,因为add已经清理。
-
-
Mov %eax, -0xc(%ebp); // 将add返回值存入到c中
Xor %eax,%eax ; //main函数执行成功,返回0,置eax 0;
Mov %ebp,%esp; // esp栈指针恢复到main函数起始处
Pop %ebp //恢复调用main函数的栈基址
Ret; //main函数返回
-
__stdcall由被调用函数清理堆栈参数,__cdecl有调用函数清理堆栈,这样如果有多个函数调用__cdecl函数,那么多个函数都必须要一行代码执行清理堆栈的动作。如果__stdcall,程序中只有被调用函数执行清理动作,只有一行。所以__stdcall会减小汇编代码量。但__stdcall这种方式,会导致不能利用C中有趣的可变参数这个特性。
- QueryInterface第一个参数const IID& iid是一个接口标示符,IID是一个16字节的GUID. 用于唯一标示一个接口,类似与IP地址唯一标示一台计算机一样。(可以利用VC自带工具uuidgen, guidgen来生成guid,命令行键入uuidgen,guidgen,try it and have fun!)
- QueryInterface第二个参数void** ppv,用于存放请求的接口指针的地址。
看下如何QueryInterface的使用:(参考《com技术内幕 》 chap3)
Interface IX
{
void fx();
};
void foo(IUnknown* pI)
{
IX* pIX = NULL;
HRESULT hr = pI->QueryInterface(IID_IX,(void**)&pIX);
if(SUCCEECED(hr))
{
pIX->fx();
}
}
Foo查询pI是否支持IID_IX标示的接口。IID_IX定义在头文件中,foo与组件都知道这个值。
QueryInterface的实现:假设CA实现了两个COM接口IX, IY
interface IX: IUnknown
{
Virtual void fx() = 0 ;
};
interface IY: IUnknonw
{
Virtual void fy() = 0;
};
class CA:public IX,publci IY{ };
则CA的QueryInterface形式如下:
HRESULT __stdcall CA::QueryInterface(const IID& iid,void** ppv)
{
if(iid == IID_IUnknown)
{
*ppv = static_cast<IX*>(this);
}else if( iid == IID_IX)
{
*ppv = static_cast<IX*>(this);
}else if(iid == IID_IY)
{
*ppv = static_cast<IY*>(this);
}else
{
*ppv = NULL;
return E_NOINTERFACE;
}
static_cast<IUnknown*>(*ppv)->AddRef();
return S_OK;
}
当client查询IID_IUnknown时,由于COM接口都继承自IUnknown,所以支持IUnknown接口。由于CA继承自IX,IY, IX,IY都继承自IUnknown,这是多重继承,CA的虚函数表如下图:
由于IX, IY中IUnknown纯虚函数都是指向同一个实现,所以在此多重继承中,如果要返回IUnknown接口,可以执行static_cast<IX*>(this),或者static_cast<IY*>(this),或者static_cast<IUnknown*>(this). 由于这两个vtbl表前三项都完全一致。
当向QueryInterface查询IID_IX标示的IX接口必须*ppv = static_cast<IX*>(this),同理IY接口。
- AddRef() ; Release ;
由于组件使自己维护自己的生存周期,当发现系统中无人需要自己时,组件自动销毁。为完成这种功能,组件提供了引用计数的功能。AddRef 与Release用于操作引用计数。引用计数维护此接口对象有几个client在访问。引用计数用于维护接口而不是维护组件。当client向一个组件查询并返回一个接口时,此接口的引用计数递增(这个是由组件实现着递增);如果client在此接口上调用Release,此接口的引用计数递减(这个client操作)。引用计数的使用:
void foo(IUnknown *pI)
{
IX* pIX = NULL;
HRESULT hr = pI->QueryInterface(IID_IX,(void**)&pIX);
if(SUCCEEDED(hr))
{
pIX->Fx();
pIX->Release();
}
} |
pI->QueryInterface(IID_IX,(void**)&pIX); 在COM接口中查询IX接口,如果pI支持此接口,返回这个COM接口对象。当foo调用pIX->F(); 结束后,pIX接口不再需要,则调用Release释放此接口。
AddRef,Releae实现:
class CA
{
...
CA():m_cRef(0){...}
public:
long m_cRef;
};
ULONG __stdcall CA::AddRef()
{
return InterlockedIncrement(&m_cRef);
}
ULONG __stdcall CA::Release()
{
if(InterlockedDecrement(&m_cRef) == 0)
{
delete this;
return 0;
}
return m_cRef;
} |
当向COM接口调用QueryInterface查询一个接口,如果支持此接口,则返回一个此接口对象,由于增加了一个对象的引用,则必须调用AddRef。所以当QueryInterface返回接口,此接口的引用计数已经递增了。当此接口不再需要,调用此接口的Release递减此接口的引用。如果引用计数为0,则组件没有提供接口对象,则删除此组件(delete this;)。
- Windows注册表
COM只使用了注册表的一个分支HKEY_CLASSES_ROOT. 查看注册表,命令行运行Regedit命令。
在HKEY_CLASSES_ROOT,在CLSID子目录下,存储有操作系统中安装的所有组件的CLSID, 每个CLSID有个缺省的名字,如:
CLSID目录的一般结构如下:
-
- InprocServer32, 此关键字的缺省值为组件的DLL文件的名称。
-
使用InprocServer32作为关键字是标示这个dll是一个进程中的服务器,他被加载到CLIENT的进程中。
-
- ProgID: 程序员给某个CLSID指定的一个程序员易记的名称:
-
PogID的值一般采用如下格式:
-
Progarm + . + Component+ . + Version.
-
- VersionIndependentProgID: 与版本号无关的ProgID:
-
与版本号无关的ProgID,映射到当前最新版本的组件。其格式一般为: Program+ . + Component
-
- 在HKEY_CLASSES_ROOT下面也直接列出了ProgID, VersionIndependentProgID.这是方便查询
DLL如何注册到注册表中呢?
由于DLL知道它所包含的组件,因此dll可以完成这些信息的注册,由于DLL不能做任何事情,所以输出如下两个函数:
extern "C" HRESULT __stdcall DllRegisterServer();
extern "C" HRESULT __stdcall DllUnregisterServer();
COM组件在DllRegisterServer 和DllUnreisterServer函数中操作注册表:
注册:
- 在HKEY_CLASSES_ROOT//clsid下注册组件CLSID.
- 在新注册的CLSID目录下,填充InProcServer32, ProgID, VersionIndependentProgID三个关键字。
- 在HKEY_CLASES_ROOT//下注册versionIndependentProgID,并在注册后的VersionIndePendentProgID下填充CLSID, CurVer两个关键字。
- 在HKEY_CLASSES_ROOT下注册ProgID,并在此ProgID下填充CLSID关键字。
卸载:
清除组件注册是创建的所有内容。(请参考Registry.cpp 及Server.cpp的相关代码)。
- 创建组件对象
组件已经注册到注册表中,可以调用COM库函数创建组件对象。相关函数:
- CoCreateInstance.
HRESULT __stdcall CoCreateInstance(const IID& clsid, IUnknown* pIUnknownOuter , DWORD dwClsContext, const IID& iid, void** ppv); |
- clsid 待创建组件的clsid。这个clsid就是HKEY_CLASSES_ROOT//clsid下的组件GUID。
- pIUnknownOuter, 用于多个组件共同协作时。(暂略)。
- dwClsContext,限定创建组件的执行上下文。本文的实例是CLSCTX_INPROC_SERVER, 进程内组件服务器。
- iid , 组件所支持的COM接口IID.
- ppv , 返回此接口指针。
CoCreateInstance 实际并没有直接创建COM组件,而是创建了一个称作类厂的组件,而所需的组件则是此类厂所创建。类厂组件的功能就是创建其他组件。确切地说:某个特定的类厂只创建只和某个特定的CLSID相应的组件。创建组件的标准接口是IClassFactory,CoCreateInstance创建组件实际上是通过IClassFactory创建的。
客户是如何通过一个类厂直接创建所需组件?
为创建某个CLSID组件对应的类厂, COM库提供了一个函数:
CoGetClassObject 原型:
HRESULT __stdcall CoGetClassObject(
|
- clsid , 带创建组件的CLSID。
- dwClsContext, 组件执行上下文环境。
- pServerInfo , 用于DCOM.
- Iid , 支持的COM接口IID
- ppv, 返回接口对象的指针。
CoGetClassObject返回的是一个指向所需组件的类厂,而不是指向组件本身的一个指针。Client可以通过CoGetClassObject返回的指针来创建组件,这个指针就是IClassFactory。
- IClassFactory:
interface IClassFactory: IUnknown
{
HRESULT __stdcall CreateInstance(IUnknown* pUnknownOuter,
const IID& iid,
void** ppv);
HRESULT __stdcall LockServer(BOOL bLock);
}; |
IClassFactory有两个成员函数:
- CreateInstance;第一个参数用于多种组件聚合(暂略);iid,类厂能创建的com接口;ppv,返回接口指针。
创建组件时,首先创建组件的类厂,然后用所获取的IClassFactory指针创建所需的COM接口。CoCreateInstance实际上是通过CoGetClassObject实现的。如下:
HRESULT CoCreateInstance(const CLSID& clsid,IUnknown* pOuter,
DWORD dwClsContext, const IID& iid,
void** ppv)
{
*ppv = NULL;
IClassFactory* pIFactory = NULL;
HRESULT hr = CoGetClassObject(clsid,dwclsContext,NULL,
IID_IClassFactory,(void**)&pIFactory);
if(SUCCEEDED(hr))
{
hr = pIFactory->CreateInstance(pOuter,iid,ppv);
pIFactory->Release();
}
return hr;
}
CoCreateInstance首先调用CoGetClassObject来获取类厂中的IClassFactory接口的指针,然后用此指针来调用IClassFactory::CreateInstance完成新组件的创建。
- 类厂的实现:
当COM组件是dll时,CoGetClassObject调用dll中的DllGetClassObject创建类厂:
extern "C" HRESULT __stdcall DllGetClassObject(
const CLSID& clsid,
const IID& iid,
void **ppv); |
-
- clsid, 类厂要创建的组件的CLSID.
- Iid, client希望通过类厂来创建的com接口iid.
- ppv返回接口的指针。
组件的创建过程类似下图:
5 In-Process-Server(com dll)
(为减小代码量,方便阅读,程序一些检查错误的代码略去,如GetModuleFileName,必须检查其返回值是否失败等,程序中都未检查其返回值,以确保这个例子短小易懂。)
- 快速编译运行一个Hello,world COM组件。(源码见dll-src.rar)
Step 1: 定义COM组件接口(IX)。
运行cmd,建立一个目录,编辑Server.h头文件。如:
E:/> mkdir InProc
E:/> cd InPro
E:/InProc> mkdir server
E:/InProc> cd server
E:/InProc/server> notepad server.h.
输入 server.h 中的内容。 |
//server.h
#ifndef __Server_H__
#define __Server_H__
#include <objbase.h>
interface IX : IClassFactory
{
virtual void Hello() = 0;
};
extern "C" const IID IID_IX ;
extern "C" const CLSID CLSID_Hello;
#endif; |
声明COM接口IX, IX同时是继承自类厂。声明一个COM接口IID_IX, 与组件CLSID CLSID_Hello.
Step2: 赋值组件GUID。
编辑uuids.cpp.
E:/InProc/server> notepad uuids.cpp
输入 uuids.h 中的内容。 |
//uuids.cpp
#include <objbase.h>
extern "C" const IID IID_IX = {0xe9181cb2, 0xc42e, 0x431a, 0x88, 0x14, 0x9, 0x6b, 0xad, 0xf0, 0xd9, 0xb0};
extern "C" const CLSID CLSID_Hello = {0x80188242, 0xa104, 0x4b90, 0xb9, 0xd5, 0xbf, 0xcb, 0x55, 0x63, 0xc6, 0x82}; |
定义IID_IX, CLSID_Hello的值
Step3: 撰写COM组件(Server,实现IX接口)。
编辑Server.cpp. Server对象既是类厂也是接口。
E:/InProc/server> notepad server.cpp
输入 server.cpp 中内容。 |
//server.cpp
#include <stdio.h>
#include "Server.h"
#include "Registry.h"
static long g_components = 0;
static long g_serverLocks = 0;
static HMODULE g_module = NULL;
char* szInfo = "Say Hello to client";
char* szProgID = "Hello.1";
char* szIndProgID = "Hello";
void ShowIID(IID iid)
{
int* p = (int*)&iid;
for(int i=0; i<4;i++)
{
printf("%08x ",p[i]);
}
printf("/n");
}
class Server : public IX
{
public:
virtual HRESULT __stdcall QueryInterface(const IID& iid,void** ppv);
virtual ULONG __stdcall AddRef();
virtual ULONG __stdcall Release();
virtual HRESULT __stdcall CreateInstance(IUnknown* pOut,const IID& iid,void** ppv);
virtual HRESULT __stdcall LockServer(BOOL bLock );
virtual void Hello(void);
Server():m_cRef(1)
{
printf("Server: Server()/n");
g_components++;
}
~Server()
{
printf("Server: ~Server()/n");
g_components--;
}
private:
long m_cRef;
};
HRESULT __stdcall Server::LockServer(BOOL bLock )
{
printf("Server: LockServer/n");
if(bLock)
{
InterlockedIncrement(&g_serverLocks);
}else
{
InterlockedDecrement(&g_serverLocks);
}
return S_OK;
}
HRESULT __stdcall Server::CreateInstance(IUnknown* pOut,const IID& iid,void** ppv)
{
printf("Server: CreateInstance ");
ShowIID(iid);
if(pOut != NULL)
{
return CLASS_E_NOAGGREGATION;
}
*ppv = static_cast<IX*>(this);
return S_OK;
}
void Server::Hello()
{
HANDLE hstdOut = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO info;
GetConsoleScreenBufferInfo(hstdOut,&info);
SetConsoleTextAttribute(hstdOut,FOREGROUND_RED);
printf("/n*********************************************/n|/n");
printf("| Server Say: Hello world!/n|/n");
printf("*********************************************/n");
SetConsoleTextAttribute(hstdOut,info.wAttributes);
}
HRESULT __stdcall Server::QueryInterface(const IID& iid,void** ppv)
{
printf("Server: QueryInterface ");
ShowIID(iid);
if(iid == IID_IUnknown || iid == IID_IClassFactory || iid == IID_IX)
{
*ppv = static_cast<IX*>(this);
}else
{
*ppv = NULL;
printf(" No interface/n");
return E_NOINTERFACE;
}
reinterpret_cast<IUnknown*>(*ppv)->AddRef();
return S_OK;
}
ULONG __stdcall Server::AddRef()
{
printf("Server: AddRef %x/n",m_cRef+1);
return InterlockedIncrement(&m_cRef);
}
ULONG __stdcall Server::Release()
{
printf("Server: Release %x/n",m_cRef-1);
if (InterlockedDecrement(&m_cRef) == 0)
{
delete this ;
return 0 ;
}
return m_cRef ;
}
extern "C" HRESULT __stdcall DllCanUnloadNow()
{
printf("Server: Unload the server dll ");
if((g_components == 0) && (g_serverLocks == 0))
{
printf("true/n");
return S_OK;
}else
{
printf("false/n");
return S_FALSE;
}
}
extern "C" HRESULT __stdcall DllGetClassObject(const CLSID& clsid,const IID& iid,void **ppv)
{
printf("Server: DllGetClassObject() ");
ShowIID(iid);
if(clsid != CLSID_Hello)
{
return CLASS_E_CLASSNOTAVAILABLE;
}
Server* pNew = new Server();
if(pNew == NULL)
{
return E_OUTOFMEMORY;
}
HRESULT hr = pNew->QueryInterface(iid,ppv);
printf("End of DllGetClassObject/n");
return hr;
}
BOOL WINAPI DllMain(HANDLE hModule,DWORD dwReason,void* lpReserved)
{
if(dwReason == DLL_PROCESS_ATTACH)
{
g_module = (HMODULE) hModule;
}
return TRUE;
}
extern "C" HRESULT __stdcall DllRegisterServer()
{
const int len = 1024;
char szFileName[len];
::GetModuleFileName(g_module,szFileName,len);
char szCLSID[39];
CLSIDtoChar(CLSID_Hello, szCLSID, 39) ;
char szKey[64];
strcpy(szKey,"CLSID//");
strcat(szKey,szCLSID);
SetKeyAndValue(szKey,NULL,szInfo);
SetKeyAndValue(szKey,"InprocServer32",szFileName);
SetKeyAndValue(szKey,"ProgID",szProgID);
SetKeyAndValue(szKey,"VersionIndependentProtID",szIndProgID);
SetKeyAndValue(szIndProgID,NULL,szInfo);
SetKeyAndValue(szIndProgID,"CLSID",szCLSID);
SetKeyAndValue(szIndProgID,"CurVer",szProgID);
SetKeyAndValue(szProgID,NULL,szInfo);
SetKeyAndValue(szProgID,"CLSID",szCLSID);
return S_OK;
}
extern "C" HRESULT __stdcall DllUnregisterServer()
{
char szCLSID[39];
CLSIDtoChar(CLSID_Hello,szCLSID,39);
char szKey[64];
strcpy(szKey,"CLSID//");
strcat(szKey,szCLSID);
DeleteKey(HKEY_CLASSES_ROOT,szKey);
DeleteKey(HKEY_CLASSES_ROOT,szIndProgID);
DeleteKey(HKEY_CLASSES_ROOT,szProgID);
return S_OK;
} |
Server类即是组件类厂,也是组件。当Client调用CoGetClassObject创建类厂:调用Server.cpp中DllGetClassObject.
- 创建一个Server对象 ,此时Server对象的引用计数初始化为1,标示有一个引用。
- 调用Server QueryInterface查询iid标示的COM接口,由于Server继承自IX,Server是一个IUnknown接口,是一个IClassFactory,也是一个COM接口IX, 所以只支持IID_IUnknown , IID_IClassFactory ,IID_IX. 其QueryInterface为:
HRESULT __stdcall Server::QueryInterface(const IID& iid,void** ppv)
{
printf("Server: QueryInterface ");
ShowIID(iid);
if(iid == IID_IUnknown || iid == IID_IClassFactory || iid == IID_IX)
{
*ppv = static_cast<IX*>(this);
}else
{
*ppv = NULL;
printf(" No interface/n");
return E_NOINTERFACE;
}
reinterpret_cast<IUnknown*>(*ppv)->AddRef();
return S_OK;
} |
当查询iid 为 IID_IUnknown 或 IID_IClassFactory或 IID_IX, 返回IX* 指针。并增加引用计数。如果iid是非以上IID, 则返回错误代码。ShowIID是Server.cpp定义的一个打印iid值的辅助调试函数,请参看源码
Client的到了类厂,,则可以调用IClassFactory::CreateInstance函数创建组件。
HRESULT __stdcall Server::CreateInstance(
IUnknown* pOut,const IID& iid,void** ppv)
{
printf("Server: CreateInstance ");
ShowIID(iid);
if(pOut != NULL)
{
return CLASS_E_NOAGGREGATION;
}
*ppv = static_cast<IX*>(this);
return S_OK;
} |
这个CreateInstance很简单,忽略iid,不管怎样,都创建一个IX指针给客户。这是为了方便实例。
当Client得到了IX指针,就可以调用IX::Hello函数在控制台打印一段红色的语句“Server Say: Hello world!”.
Step4: 撰写注册表辅助函数代码。
编辑Registry.h, Registry.cpp.
E:/InProc/server> notepad Registry.h
输入 registry.h 中内容。
E:/InProc/server> notepad Registry.cpp
输入 registry.cpp 中的内容 |
//Registry.h
#ifndef __Registry_H__
#define __Registry_H__
void CLSIDtoChar(const CLSID& clsid, char* szCLSID, int length);
BOOL SetKeyAndValue(const char* szKey, const char* szSubKey, const char* szValue);
LONG DeleteKey(HKEY hKeyParent,const char* lpszKeyChild);
#endif |
#include <windows.h>
#include "Registry.h"
void CLSIDtoChar(const CLSID& clsid,char* szCLSID,int length)
{
if(length < 39)
{
return;
}
wchar_t* wszCLSID = NULL;
HRESULT hr = StringFromCLSID(clsid,&wszCLSID);
wcstombs(szCLSID,wszCLSID,length);
::CoTaskMemFree(wszCLSID);
}
BOOL SetKeyAndValue(const char* szKey,const char* szSubKey,const char* szValue)
{
HKEY hKey;
char szKeyBuf[1024];
strcpy(szKeyBuf,szKey);
if(szSubKey != NULL)
{
strcat(szKeyBuf,"//");
strcat(szKeyBuf,szSubKey);
}
LONG lResult = RegCreateKeyEx(HKEY_CLASSES_ROOT,szKeyBuf,0,NULL,REG_OPTION_NON_VOLATILE,
KEY_ALL_ACCESS,NULL,&hKey,NULL);
if(lResult != ERROR_SUCCESS)
{
return FALSE;
}
if(szValue != NULL)
{
RegSetValueEx(hKey,NULL,0,REG_SZ,(BYTE*) szValue,strlen(szValue)+1);
}
RegCloseKey(hKey);
return TRUE;
}
LONG DeleteKey(HKEY hKeyParent,const char* lpszKeyChild)
{
HKEY hKeyChild;
LONG lRes = RegOpenKeyEx(hKeyParent,lpszKeyChild,0,KEY_ALL_ACCESS,&hKeyChild);
if( lRes != ERROR_SUCCESS)
{
return lRes;
}
FILETIME time;
char szBuffer[256];
DWORD dwSize = 256;
while(RegEnumKeyEx(hKeyChild,0,szBuffer,&dwSize,NULL,NULL,NULL,&time) == S_OK)
{
lRes = DeleteKey(hKeyChild,szBuffer);
if (lRes != ERROR_SUCCESS)
{
RegCloseKey(hKeyChild);
return lRes;
}
dwSize = 256;
}
RegCloseKey(hKeyChild);
return RegDeleteKey(hKeyParent,lpszKeyChild);
} |
Step5: 编译并注册COM组件。
定义dll 导出函数。
E:/InProc/server/notepad server.def
输入 server.def 中内容。 |
//server.def
LIBRARY server.dll
DESCRIPTION 'Example DLL'
EXPORTS
DllGetClassObject @1 PRIVATE
DllCanUnloadNow @2 PRIVATE
DllRegisterServer @4 PRIVATE
DllUnregisterServer @5 PRIVATE |
Server.def是为了指定组件dll的导出函数。
编译com组件。
E:/InProc/server> cl /c uuids.cpp
编译 uuids.cpp, 生成存有 IID_IX, CLSID_Hello 的 uuids.obj.
E:/InProc/server> cl /c Registry.cpp
编译 Registry.cpp, 生成存有注册辅助函数代码的 registry.obj.
E:/InProc/server> cl /c Server.cpp
编译 Server.cpp, 生成 server.obj.
E:/InProc/server> link /def:server.def /dll /out:server.dll server.obj registry.obj uuids.obj ole32.lib advapi32.lib
根据 server.def 定义的导出函数生成 server.dll, 由于源码调用了 COM 库函数所以需要导入 ole32.lib, 且必须要操作 Windows 注册表,需要导入 advapi32.lib.
E:/InProc/server> regsvr32 server.dll
将 server.dll 注册到注册表中。至此 COM 组件创建完成 . |
Step6: 创建客户端并测试。
编辑client.cpp.
E:/InProc/server> cd ..
E:/InProc> mkdir client
E:/InProc> cd client
E:/InProc/client> notepad Client.cpp
输入 client.cpp 中内容。 |
//client.cpp
#include <unknwn.h>
#include <stdio.h>
#include "server.h"
int main()
{
CoInitialize(NULL);
printf("/n**************************/n");
printf("Client: Create Class Factory: /n");
IClassFactory* pIFactory = NULL;
HRESULT hr = CoGetClassObject(CLSID_Hello,CLSCTX_INPROC_SERVER,
NULL,IID_IClassFactory,(void**)&pIFactory);
if(FAILED(hr))
{
printf("Client: Can not create IX class factory(Error: %x)!/n",hr);
return -1;
}
printf("Client: Create Class Factory End/n");
printf("***************************/n/n");
printf("/n*************************/n");
printf("Client: Create interface IX/n");
IX* pIX = NULL;
hr = pIFactory->CreateInstance(NULL,IID_IX,(void**)&pIX);
if(FAILED(hr))
{
printf("Client: Can not create IX component(Error: %x)!/n",hr);
pIFactory->Release();
return -1;
}
printf("Client: End of CreateInstance/n");
printf("*****************************/n/n");
printf("/n*************************/n");
printf("Client: Call IX->Hello()/n");
pIX->Hello();
printf("Client: end of call Hello/n");
printf("*************************/n/n");
printf("/n*************************/n");
printf("Client: Release/n");
pIX->Release();
pIFactory->Release();
CoUninitialize();
printf("**************************/n");
printf("Client: exit/n");
return 0;
} |
Step7: 编译并运行客户端。
客户端需要组件的定义的讯息,如组件的接口及对应的GUID,需将COM组件Server中定义的server.h与uuids.cpp拷贝至client目录中。
E:/InProc/client> copy ../server/uuids.cpp .
E:/InProc/client> copy ../server/server.h .
E:/InProc/client> cl /c uuids.cpp
E:/InProc/client> cl client.cpp uuids.obj ole32.lib
由于 client.cpp 调用 COM 库函数,所以需要导入 ole32.lib. |
客户端运行结果:
Well Done,您已经完成并成功调用了一个COM组件!那么COM组件是如何在Windows中运行的呢?
- Client.exe 执行流程:
- CoInitialize(NULL) 初始化Client线程为单元线程, 此线程自此进入STA(single thread apartment)执行。调用CoUninitialize() 退去单元线程。
所有的COM组件和client都需要完成一些相同的操作,为保证
CoInitialize(NULL); 初始化COM 库和DLL文件,从而程序可以调用COM库函数,并标示当前线程为Single Thread Apartment. 调用CoUninitialize是为了释放对COM库和DLL的使用。STA是建立在windows 窗口线程基础上的(窗口线程有个消息队列,并有一个窗口句柄,通过CreateWindow建立窗口线程),STA有一个隐藏的的窗口用于窗口消息队列上的COM调用同步。CoInitialize(NULL)其实内部调用的是CoInitializeEx(NULL, COINIT_APARTMENTTHREADED )。CoIinitializeEx功能更强大,请参阅MSDN。
- HRESULT hr = CoGetClassObject
(CLSID_Hello,
CLSCTX_INPROC_SERVER,
NULL,
IID_IClassFactory,
(void**)&pIFactory);
创建并返回CLSID_Hello的类厂。这个函数的执行结果:
其实现:
extern "C" HRESULT __stdcall DllGetClassObject(
const CLSID& clsid,const IID& iid,void **ppv)
{
printf("Server: DllGetClassObject() ");
ShowIID(iid);
if(clsid != CLSID_Hello)
{
return CLASS_E_CLASSNOTAVAILABLE;
}
Server* pNew = new Server();
if(pNew == NULL)
{
return E_OUTOFMEMORY;
}
HRESULT hr = pNew->QueryInterface(iid,ppv);
printf("End of DllGetClassObject/n");
return hr;
} |
其调用次序:
-
- CoGetClassObject 查找CSLID_Hello组件,如果内存中没有CLSID_Hello组件,从注册表中获取组件程序的路径名,加载server.dll。调用server.dll中的DllGetClassObject.
- DllGetClassObject 执行次序:
- 进入函数,打印调试讯息” Server: DllGetClassObject()”,后面还打印了一串数字“00000001 00000000 000000c0 46000000“,这是IID_IClassFactory的值,标示CoGetClassObject调用DllGetClassObject的iid为IID_IClassFactory,要创建类厂。所以打印
(1)。
-
- new 一个Server对象,Server构造函数会打印一个调试讯息,所以打印
(2)。
-
- 调用新对象的Queryinterface,所以打印
(3), 由于QueryInterface内部调用AddRef,所以打印(4).
-
- DllGetClassObject退去,打印
(5).
-
- DllGetClaaObject执行完毕返回到CoGetClassObject函数中。(6)-(10)打印是由于CoGetClassObject从DllGetClassObject对类厂做了一次QueryInterface做了一次查询,查询的iid是CoGetClassObject的查询的iid,目的是确保类厂支持此接口。
- pIFactory->CreateInstance(NULL,IID_IX,(void**)&pIX);
如果类厂支持IID_IX 接口。则pIX指向IX接口。
- pIX->Hello();
void Server::Hello()
{
HANDLE hstdOut = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO info;
GetConsoleScreenBufferInfo(hstdOut,&info);
SetConsoleTextAttribute(hstdOut,FOREGROUND_RED);
printf("/n*********************************************/n|/n");
printf("| Server Say: Hello world!/n|/n");
printf("*********************************************/n");
SetConsoleTextAttribute(hstdOut,info.wAttributes);
}
Hello函数在Client Console打印“Server Say….”等。
- Pix->Release(); pIFactory->Release();
由于程序将退去,不再需要组件对象,递减其引用技术。引用计数为0.
ULONG __stdcall Server::Release()
{
if (InterlockedDecrement(&m_cRef) == 0)
{
delete this ;
return 0 ;
}
return m_cRef ;
}
由于引用计数为0, 所以delete this;会删除这个Server对象,释放其内存。
Server():m_cRef(1)
{
printf("Server: Server()/n");
g_components++;
}
~Server()
{
printf("Server: ~Server()/n");
g_components--;
}
当创建一个Server对象时,递增组件的对象计数,当销毁一个Server对象时,递减组件对象计数。组件对象计数用于卸载组件dll. 当程序计数为0时,用户调用CoFreeUnusedLibraries查询组件是否无人引用可被卸载,将调用组件的DllCanUnloadNow:
extern "C" HRESULT __stdcall DllCanUnloadNow()
{
if((g_components == 0) && (g_serverLocks == 0))
{
return S_OK;
}else
{
return S_FALSE;
}
}
当组件对象计数为0并且无client LockServer时,返回S_OK,标示可以写在组件服务器(dll).如果S_FALSE,则不能卸载。
- CoUninitialize();
Client.cpp 调用CoUnitialize() 关闭当前线程的COM库,释放无引用的DLL,此时会调用CoFreeUnusedLibraries 。由于组件对象引用计数为0,client没有锁定server ,所以DllCanUnloadNow返回S_OK,从而会将server.dll卸载。
浏览下调用流程:
- COM库在Windows注册表中查找CLSID_Hello组件,当发现server.dll没有装载在client.exe的进程中,windows会将server.dll load到client.exe进程中。
- 加载server.dll后,COM库调用server.dll的DllGetClassObject创建组件类厂,,在此类厂中查询IID_IX, 并返回组件对象。程序退出时调用CoUninitialize().释放dll。组件对象维护自己的引用的计数,当发现无人引用时,销毁自己。
- 组件注册到windows注册表中:
组件通过CoCreateInstance,可以在Windows注册表中查找组件。那么组件如很将其注册到注册表中呢?组件server.dll提供了两个导出函数DllRegisterServer() , DllUnregisterServer(); DllRegisterServer函数可以将组件自注册到Windows注册表中。(可以调用cmd命令: regsvr32 server.dll; regsvr32会调用server.dll的DllRegisterServer将其注册到注册表中;当解除注册可以: regsvr32 –u server.dll ; 调用DllUnregServer解除注册。)
参阅<<COM 技术内幕>> CHAP6
extern "C" HRESULT __stdcall DllRegisterServer() {
const int len = 1024;
char szFileName[len];
::GetModuleFileName(g_module,szFileName,len);
char szCLSID[39];
CLSIDtoChar(CLSID_Hello, szCLSID, 39) ;
char szKey[64];
strcpy(szKey,"CLSID//");
strcat(szKey,szCLSID);
SetKeyAndValue(szKey,NULL,szInfo);
SetKeyAndValue(szKey,"InprocServer32",szFileName);
SetKeyAndValue(szKey,"ProgID",szProgID);
SetKeyAndValue(szKey,"VersionIndependentProtID",szIndProgID);
SetKeyAndValue(szIndProgID,NULL,szInfo);
SetKeyAndValue(szIndProgID,"CLSID",szCLSID);
SetKeyAndValue(szIndProgID,"CurVer",szProgID);
SetKeyAndValue(szProgID,NULL,szInfo);
SetKeyAndValue(szProgID,"CLSID",szCLSID);
return S_OK;
}
extern "C" HRESULT __stdcall DllUnregisterServer()
{
char szCLSID[39];
CLSIDtoChar(CLSID_Hello,szCLSID,39);
char szKey[64];
strcpy(szKey,"CLSID//");
strcat(szKey,szCLSID);
DeleteKey(HKEY_CLASSES_ROOT,szKey);
DeleteKey(HKEY_CLASSES_ROOT,szIndProgID);
DeleteKey(HKEY_CLASSES_ROOT,szProgID);
return S_OK;
}