COM 实例初探

 

BTW: 从今天开始,慢慢将自己的东西放到blog上来,由于之前写的技术文档都是doc,并且嵌入了viso等东西,可能网页无法显示。声明这些文档仅是私人,并参考了一些前辈的东西。

 

 

Com实例


Document Information:

Owner:Li Zhang
Date:2009.02.23
Version:0.01

Version History:

VersionDate AuthorSummary of ChangesDetails of Changes
0.012009.02.23Li ZhangFirst Draft

参考书籍:
  1. COM 技术内幕 (若干代码,修改自此书源码)
  2. COM 本质论
  3. MSDN

 

 

  1. C/C++ 虚函数(讨论虚函数,继承,引用等C++如何实现)

实验1 C对象布局

C++中,一个对象在内存中只存放其成员变量,所有的对象共享类函数。那么C++中类函数如何实现,及虚函数如何实现呢?首先我们看C++对象的内存布局。Let’s start from the simplest!


源码:
//Test1.cpp

#include <stdio.h>

class Base

{

public:

Base()

{

printf("Base::Base()/n");

}

~Base()

{

printf("Base::~Base()/n");

}

void Hello()

{

printf("Base::Hello()/n");

}

int m_integer;

char m_char;

};

int main()

{

Base b;

b.Hello();

printf("sizeof Class Base is %x!/n",sizeof(b));

}




运行结果:

您的浏览器可能不支持显示此图像。

执行解释:

为什么Base对象的大小为8字节呢?

由于Base对象只存储成员变量m_integerm_char,所以其内存大小为其成员变量所占有的内存。

如果存储m_integer,m_char,其内存大小应该为5字节,但为什么是8字节呢?

这是由于字节对齐的原因。内存中数据结构会采用4字节对齐,m_char虽只占有一字节,但操作系统会分配四字节,第一个字节赋给m_char,余下三字节用于对齐.字节对齐的原因是增加内存存储速度(详细解释,此处限于篇幅与时间,不加解释,请随意参考一本操作系统的书籍,或请参考《深入理解计算机系统》3.10,其中有精彩的解释)。

那么,对象中函数如Base(),~Base(),Hello()又存储在什么地方呢?

计算机中不管是什么面向对象的语言,最后翻译成二进制,面向对象仅是包装而已。最后.exe的形式形象一点都是类似如C中的函数形式。

命令行运行: objdump -r test1.obj

会发现有趣的东西:

您的浏览器可能不支持显示此图像。


??0Base@@QAE@XZ ---对应---- Base::Base()

?Hello@Base@@QAEXXZ ---对应-- Base::Hello()

??1Base@@AQE@XZ ---对应-- Base::~Base();




所有的类函数都被翻译成这种有着奇怪名字的C函数。

那么编译器是怎样转化类函数名字的呢?

不同版本的C++编译器采取的转化格式都略微不同。此版本编译器为(VS2005 C++)将C++函数翻译成底层函数时,其C++函数名转换C函数名采用如下格式(构造函数与析构函数比较特殊,不采用这种格式)

+函数名+@+类名+@@QAE+返回值类型+参数类型+参数结束字符;

C++由于支持函数重载,所以二进制中函数形式采用函数名+参数的形式来命名函数;转换后的函数名由@@QAE标示其后字符标示返回类型和参数类型。其中类型字符转换表为:

X--void
D--char
E--unsigned char
F--short
H--int
I--unsigned int
J--long
K--unsigned long
DWORD
M--float
N--double
_N--bool
U—struct

– Z

参数结束字符:

参数表如果有参数以’@Z’标示参数名结束,如果函数无参数则以’Z’结束。


所以:

Void Base:: Hello(void) ---- ?+Hello+@+Base+@@QAE+X(无返回值)+X(无参数)+Z

--- ?Hello@Base@@QAEXXZ

如果:

Int Base::Hello(char,int) --- ?+Hello+@+Base+@@QAE+H+DH+@Z

构造函数与析构函数名比较特殊:

构造函数: ??0+类名+@@QAE+@+参数类型+参数结束符

所以: Base::Base() ---- ??0+Base+@@QAE+@+X+Z ??0Base@@QAE@XZ

如果: Base::Base(char,int) --- ??0+Base+@@QAE+@+DH+@Z ??0Base@@QAE@DH@Z

析构函数: ??1+类名+@@QAE+@XZ

所以: Base::Base() ---- ??1+Base+@@QAE+@XZ ??0Base@@QAE@XZ

为什么析构函数会这样呢?

因为析构函数无参。

(限于篇幅,点到为止了)

那么C++是如何执行类函数呢?

b.Hello();

C++编译器在编译这行代码时做了动作:其test1.obj反汇编如下:

objdump -d test1.obj

// main 函数反汇编

您的浏览器可能不支持显示此图像。

// Base::Hello()反汇编

您的浏览器可能不支持显示此图像。

C++类函数中一般有一个this指针,这个指针就在这汇编代码中。在VC编译器中默认的是以ecx寄存器存储this指针,这是一个约定,所有的类函数都明白自己的this指针存在什么地方,如果需要就回去ecx寄存器存取。

b.Hello();

代码在编译时,先把this指针存入ecx寄存器,由于Hello函数无参数,所以不用压栈。

直接调用其函数就可以了。其exe反汇编如下:

dumpbin /disasm /section:.text test1.exe|more

您的浏览器可能不支持显示此图像。

** C语言者最爱:

上图是main函数的反汇编代码,稍微解释一下:

  •  
    •  
      • push ebp; /* ebp 寄存器存储的是一个函数其栈地址的起始位置,栈就是函数分配局部变量的地方。main函数又C/C++运行期启动函数调用,这个知识点的详细解释,Sorry,篇幅所限。请参阅《Windows核心编程》Programming Applications for Microsoft Windows, Chap4 Processes 其中Writing Your First Windows Application节,有精辟的解释)。C/C++ startup funtions调用main函数时,由于从一个函数进入一个函数,而ebp的作用是记录对应的当前函数的基地址,所以ebp必须要改变其值,所以push ebp ebp入栈,为什么要入栈呢?因为startup funtions调用main函数,main函数返回,ebp要恢复为startup 的栈基址,所以将startup栈基址保存在堆栈中,当main返回时,再从堆栈中重新将栈基址读入到ebp中,这样函数的栈基址随着函数的调用有序的更改。调用函数的栈基址已经存储,那么ebp要设置为被调用函数的栈基址,而此时的的堆栈指针记录在esp中,所以mov ebp,esp 那么ebp就指向新的函数堆栈。所有被调用函数的局部变量将在ebp指向的新的栈中分配。

  •  
    •  
      • */

mov ebp,esp

  •  
    •  
      • sub esp,8; /* 还记得Base对象的大小吗? 8 字节!此处将站指针减小8,就是分配8字节的栈单元,这8个字节存储的就是Base对象,如下图

  •  
    •  
      • 您的浏览器可能不支持显示此图像。

  •  
    •  
      • lea ecx, [ebp-8];

  •  
    •  
      • call 0x00401040; /*分配了Base对象后,要调用其构造函数来初始化这段地址.C++类函数中除static函数都要默认传递一个参数,对象的this指针,即对象的内存初始地址。构造函数也不例外,也需要这个对象的内存初始地址,ebp-8对应的就是Base对象的地址,见上图将ebp-8字节就可以了。将Base对象的地址放入ecx寄存器,这时就可以调用Base构造函数了。Call 0x00401040就是调用其构造函数。

  •  
    •  
      • */

** 小秘密:

构造函数都在做些什么呢?

C++类构造函数其实是一个很复杂的函数,虽然代码撰写者只需在构造函数中撰写代码。如:

Base::Base()

{

Your code;

}




括号内撰写代码就可以了,其实C++编译器会做一些神秘的事情。他会在这个构造函数的起始处添加若干很重要的代码,形象点说他在Base构造函数的括号外又添加了若干代码。这样构造函数就如下的格式:

Base::Base()

C++编译器 write some code

{

….

Your code;

}




C++编译器在里面写了什么代码,限于篇幅,大略的写一下了。他会做如下的事情如果有父类,调用父类的构造函数;如果有虚函数,设置虚函数表指针(将类对象的第一个四字节的值设为指向此对象的虚函数表的位置。);如果有成员变量,初始化成员变量。类似如下:(假设B类有父类A,且有虚函数,有成员变量)。严格按照这个顺序!

A::A()

call B::B() // 调用父类构造函数;

set vtbl // 设置虚函数表,其实就是 *((int*)this) = addres of vtl;

Constructor member // 初始化成员变量

{

Your Code

}



让我们回到主题上来!

Base b;

b.Hello();

执行流程:

  1. 分配Base对象的地址空间。
  2. 调用Base::Base()构造函数初始化此地址空间。
  3. 调用Hello函数。

0x00401080开始的是Base::Hello()的代码:(有兴趣可以看一下他是如何处理this指针的,类函数执行时一般都会将ecx中的this指针取出来,暂存在栈中,其代码如下:

Push ecx;

Mov dword prt [ebp-4],ecx;

//其实这两行汇编代码干的都是同一件事在ebp-4这个地方存储this指针,重复的设置了两次。Ha Ha! 这是编译器没有优化的结果!

您的浏览器可能不支持显示此图像。

Test1.obj test1.exe代码不同的地方就是test1.exetest1.Obj中重定向代码都重新赋了地址值。(请参考编译原理书籍)。

总结:

  1. C++类函数被编译成一般函数,并按照一般函数来调用,但其函数名按照特定规定与源码的函数名不同。
  2. C++对象存储成员变量,而共享函数地址。类函数公有,成员变量私有。
  3. C++类构造函数其实机制复杂。代码撰写者写的构造函数代码外,编译器还增加了若干代码,用于确保继承,多态这些面向对象的思想。重复一次构造函数的机制:
  1. 有父类,调用父类的构造函数。
  1. 有虚函数,设置虚函数表指针
  2. 有成员变量,初始化成员变量
  3. 调用代码撰写者撰写的构造函数主体代码。

** 还有一个小秘密,为什么用printf, 而不用std::cout呢?是不是作者对C++一窍不通,还是是一个顽固的C语言爱好者呢?请尝试使用objdump等工具,你会找到答案。Try it and have fun!

 

C++

对象的内存布局已了解了,那么

C++

中虚函数是什么呢?你方才大谈设置虚函数表这些东西,不会你是个老学究吧?是怎样实现的呢?

 

其实

C++

虚函数只是一个小花招而已,也是一个平淡无奇的函数。如果了解了虚函数,其实对

C++

是一个安全漏洞。如果拥有

C++

一个函数对象,其实就可以对整个程序为所欲为

(这个漏洞利用本文不会涉及,如果你对本文说的汇编这些东西了解,有学过编译原理,操作系统,那么如果你得到一个函数地址,知道这个函数存在

.rdata

区,你就会很

Happy

了!)

Let’s start at the begin!

 

源码:
//Test2.cpp

#include <stdio.h>

class Base

{

public:

virtual void Show()

{

printf("Base::Show()/n");

}

int m_integer;

};

int main()

{

Base b;

b.Show();

printf("sizeof b is %d!/n",sizeof(b));

return 0;

}



执行结果

您的浏览器可能不支持显示此图像。

执行解释:

God! Base不是只有一个成员变量 int m_integer;吗,他的对象大小应该是4字节呀?

Let’s do like hacker does!

源码:

//Test2.cpp

#include <stdio.h>

class Base

{

public:

virtual void Show()

{

printf("Base::Show()/n");

}

int m_integer;

};

int main()

{

Base b;

b.Show();

printf("sizeof b is %d/n",sizeof(b));

printf("b address is %#x/n",&(b));

printf("b.m_ninteger address is %#x/n",&(b.m_integer));

printf("the x value is %#x/n",b);

return 0;

}




执行结果:

您的浏览器可能不支持显示此图像。

执行解释:

怎么对象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!

源码:

//Test.cpp

#include <stdio.h>

int g_int = 0;

static int g_static = 0;

void fun()

{

static int l_static =0;

int i =0;

printf("Local static %x!/n",&l_static);

printf("Fuction local %x!/n",&i);

}

class A

{

public:

void show();

static int c_static;

int c_local;

};

int A::c_static = 0;

int main()

{

printf("global %x!/n",&g_int);

printf("global static %x!/n",&g_static);

fun();

A a;

printf("Class %x!/n",&a.c_local);

printf("class static %x!/n",&a.c_static);

return 0;

}


执行结果:

您的浏览器可能不支持显示此图像。

执行解释:

由结果可知 全局变量,全局static变量,函数static变量,类static变量地址都在0x40cf6*处,而函数局部变量,类成员变量都是在0x12ff6*左右。如何解释呢?

全局变量,static变量(函数也好,类成员变量也罢)都会在.exe中分配一个地址,他在编译器生成的汇编代码中,存储在数据区,而局部变量是不存储执行文件中的,它是动态的在函数堆栈中分配的。我们看下windows中执行文件(PE)的代码区与数据区。

命令行:dumpbin test.exe

执行结果:

您的浏览器可能不支持显示此图像。

Test.exe有三个区.data , .rdata, .text 稍微说明一下。

  1. .text 这个是exe的代码区。用于存放函数代码如main函数等。运行命令
  • Dumpbin /disasm /section:.text test.exe|more

  • 你可以反汇编到.text区的汇编代码。

  • 您的浏览器可能不支持显示此图像。

可以看到main函数起始于0X00401000,结束于:

您的浏览器可能不支持显示此图像。

可以粗糙的认为.text区起始于0x00401000,结束于0x40A000; windows中,其内存一个page4kb, 0x1000. .text区可以看成是9page大小.

2) .data 存放数据。我们看下其起始及结束地址。

Dumpbin /rawdata:1 /section:.data test.exe|more

您的浏览器可能不支持显示此图像。

结束于:

您的浏览器可能不支持显示此图像。

可以粗糙的认为.data 区范围为 0x40C000 – 0X40D000 相当于1page大小

  1. .rdata 只读数据存储区,其起始结束地址为 0X40A000 -- 0x40C000.(截图略,采用上文的dumpbin相似的命令就可以查看到)。相当于2page大小。只读存储区顾名思义是存储只读数据。什么是只读数据呢?
  • Char* s = “Hello,world!”;

  • “Hello,world!” 就是只读数据。Literl constant var!

这三个区是exe文件中数据存放格式,与进程有何关系呢?

由于windows exe loader,在启动一个exe进程时,他会将exe的数据存放到进程的虚拟地址空间,怎样来确定这些数据的存放地址呢?就是按照他在exe中说明的位置。如:main函数在exe中规定为0x401000,那么exeload到操作系统中时,main函数在虚拟地址空间对应的地址就是0x401000. 进程虚拟地址空间布局大致如下:

您的浏览器可能不支持显示此图像。

再回到上次的执行结果解释:

您的浏览器可能不支持显示此图像。

可见全局变量, staic变量 存储在.data区。关于new malloc 的地址分配在什么地方,堆栈在进程中什么位置,限于篇幅,点到为止,请采用上文中类似的方法,Try it and have fun!

让我们回到0x40A1E8这个奇怪的地址,由上文可知,这个存放在只读数据区.rdata. 所以是一个只读数据。让我们看一下test2.exe0x40A1E8处有什么神秘的东西。

执行命令: 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; //调用00401070show代码。

** C语言者最爱:

E8 ->对应call 命令; 0x000005A是偏移值。是相对于当前命令代码的最后一个字节即

0x00401016;所以真正的函数调用地址为0x00401016+0x5A = 0X00401070;

这个0x00401070Base b; 对象的第一个字节(0x0040A1E8)又有何关系呢?

其实这是该死的C++编译器做的优化,编译器生成的代码首先获取类对象的虚函数表指针,这个指针值为0x0040A1E8,及虚函数表位置在0X0040A1E8;然后调用虚函数表中对应的项。如果在编译期间能够确定指向的是基类还是派生类,这些代码就被编译器优化为指向的对应函数地址;如果只在运行期间确定,编译器并不做优化。

对于本例:编译器知道这个是基类,真正调用对应的函数指针

0X0040A1E8->0X00401070.

编译器把这段寻址代码优化为 call 0x00401070

我们看下动态未经优化能够真实说明虚函数的例子:

//Test2.cpp

#include <stdio.h>

class Base

{

public:

virtual void Show()

{

printf("Base::Show()/n");

}

virtual void Hello()

{

printf("Base::Hello()/n");

}

};

class Extend : public Base

{

public:

virtual void Show()

{

printf("Extend::Show()/n");

}

virtual void Hello()

{

printf("Extend::Hello()/n");

}

};

void check(Base* b)

{

b->Show();

b->Hello();

int** p = reinterpret_cast<int**> (b);

printf("Base Show address %#x/n",**p);

printf("Base Hello address %#x/n",*(*p+1));

}

int main()

{

Extend* b= new Extend();

check(b);

int** p = reinterpret_cast<int**> (b);

printf("Base Show address %#x/n",**p);

printf("Base Hello address %#x/n",*(*p+1));

delete b;

return 0;

}



看下能够透视虚函数机制的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();

  1. 由于C其有父类B,所以先初始化父类B, 由于B又有父类A,所以初始化A,由于A有虚函数,则置对象的虚函数表指针指向A的虚函数表(A虚函数表第一项A1,第二项A2);调用A构造函数初始化A
  2. 初始化A完毕,返回到初始化B,修正对象的虚函数表的指针指向B的虚函数表(B虚函数表依次: A1 –A2—B1—B2; 调用B构造函数,初始化B完毕。
  3. 初始化B完毕,返回到初始化C, 修正对象的虚函数表的指针指向C的虚函数表(C虚函数表一次: A1-A2-B1-B2-C1-C2;调用C构造函数,初始化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)


 

 

执行解释:

  1. main函数中:
  • Extend e;
  • C++class对象初始化,如果对象是个继承类,首先初始化父类,然后初始化成员变量,然后再调用对象构造函数重新赋值成员变量等。所以Extend e;

  1. 初始化父类,由于父类有成员变量: BaseFlag bf; 所有初始化父类成员变量bf,所以打印1BaseFlag:: BaseFlag(); 由于有虚函数,所以设置虚函数标表指针;初始化成员变量后,调用父类构造函数2
  2. 初始化对象本身,由于Extend有成员变量ExtendFlag ef; 所以首先修正虚函数表指针,使其指向Extend虚函数表;初始化成员变量ef,打印3;成员变量初始化完毕,调用Extend构造函数所以答应4
  • 至此Extend e; 这行代码执行完毕。

2 Check(e);

Check函数原型为:

void Check(Base base);

  • 当向Check中传递Base子类Extend对象时,编译器按照C赋值的方法,把e的值按二进制转换到Base类型。然后执行:

  • base.show();

  • base.Hello();

  • 执行结果为5)(6.

  • C++不是支持多态吗?为什么此时打印的是父类的Hello,Show呢?

  • 如果了解拷贝构造函数,那就会了解了。

  • Extend e;

  • Check(e) ; // 调用默认的Base拷贝函数,如这个拷贝函数是bit copy 成员变量,

  • 但也会做一件秘密的事。就是把临时的Base的对象的指针指向Base虚函数表。

  • 您的浏览器可能不支持显示此图像。

  • 所以在Check函数中对象为Base对象,其vtblBase虚函数表。Check函数执行完毕,这个临时对象要销毁,所以调用Base析构函数。所以打印7。由于有成员变量,所以也要析构其成员变量,所以打印8)。

  1. Check函数返回,main函数也要退去,所以按照析构顺序销毁Extend对象。

check中没有打印Extend方法,那C++多态不是骗人吗?

//Test3.cpp

#include <iostream>

void Check(Base* base)

{

Base->Show();

Base->Hello();

}

int main()

{

Extend e;

Check(&e);

}


其实主要原因在于check中采用的是对象值传递,这就导致调用了Base的拷贝构造函数,而这个天杀的拷贝构造函数会重新设置对象的vtbl指针。如果采用对象指针传递,一且就可以工作了:

您的浏览器可能不支持显示此图像。

C/C++函数参数传递是按值传递。如果传递一个类对象时,会传递一个类对象的副本,通过类的拷贝构造函数来创建这个类的副本。拷贝构造函数会重新设置对象的vtbl指针。但传递对象指针时,值传递的是指针,指针复制过去,其所指向的对象的vtbl指针是不会改变的,所以保持了子类的vtbl,所以就可调用子类的方法。

** 小知识:

当一个子类覆盖父类的方法时:

如下图: B继承自A, A有虚函数(A1,A2, Show, B重写了(show,并增加B1

class A

{

public:

Virtual void A1();

Virtual void A2();

Virtual void Show();

};

Class B : public A

{

Public:

Virtual void B1();

Virtual void Show();

} ;

您的浏览器可能不支持显示此图像。

执行解释:

B b;

初始化BB有父类,所以首先初始化父类A,由于父类有虚函数,所以将b对象的第一个四字节(对象vtbl指针)指向Avtbl。初始化A完毕。初始化B本身,重新设置虚函数, B对象的第一个四字节指向B 类的vtbl.由于B vtbl的前三项而A vtbl前三项完全一致。所以当B对象被解释成A对象后:

B* b = new B();

A* a = b;

当调用a.Show();时程序会查找a对象的虚函数表及B vtbl第三项,所以运行的函数是B::Show();

不过不同的编译器实现的方法不同,有些编译器所有的类对向共享一个类的虚函数表。有的编译器动态的改写虚函数表。我曾经用过一个编译器,忘记版本了。它是这样实现VTBL的。

对象的vtbl指针指向内存一段地址。这段地址存放虚函数表,但虚函数表在初始化时一步一步填充完毕。由于对象的初始化顺序,先初始化父类,在子类。所以填充虚函数表时,肯定是先填充父类的虚函数,如

B b;

由于父类A中有A1 () , A2(), Show () 3个虚函数,所以程序将此对象的vtbl的前三项设成指向类A的三个函数。初始化B本身时,由于B重写了Show这个虚函数,所以将此对象的vtbl第三项改为指向类BShow函数。又发现B还有一个虚函数,然后再在vtbl中加入第四项,第四项指向B1().

所以这种方式导致所有的类对象各自都有一个虚函数表。这个编译器大概是2000年发布的一个编译器,不记得了。但大概流程是这样。

现在的编译器一般是一个类所有的对象共享虚函数表。明显这个节省了若干字节内存。在.rdata只读区存放了各个类的VTBL, 当一个对象初始化时,只需将其vtbl指针指向其类对象的虚函数表既可。一方面节省了代码,一方面也节省了内存。


 

啰啰嗦嗦的说了半天,这个文档不是关于

COM

的吗?怎么还没说到

COM

呢?

 

C++

对象及其虚函数有个深入的了解,对

COM

的了解就会水到渠来。所以还要试验一下虚函数的另一个性质:

多重继承。这个对

COM

也很重要。当然也要实验下虚拟继承。

 

上文只讨论了但继承虚函数是如何实现,及运作的。这个对于

Java

这种单继承(

Object

)已经足够了。但

C++

是一个能容忍多继承的一种语言。

Let’s start at the

 

Begin!


#include <stdio.h>

class A

{

public:

virtual void A1()

{

printf("A::A1()/n");

}

};

class B

{

public:

virtual void B1()

{

printf("B::B1()/n");

}

};

class C : public A, public B

{

public:

virtual void C1()

{

printf("C::C1()/n");

}

};

void check(int** p)

{

printf("addres if vtbl: %X/n",*p);

int* temp = *p;

int i=1;

while(*temp >=0x401000 && *temp < 0x40A000)

{

printf("vitual funciton %d: %X/n",i++,*temp);

temp++;

}

}

int main()

{

A* a = new A();

printf("class A size: %X/n",sizeof(A));

check(reinterpret_cast<int**>(a));

B* b = new B();

printf("/n/nclass B size: %X/n",sizeof(B));

printf("class B vtbl funtion: %x/n",sizeof(B));

check(reinterpret_cast<int**>(b));

C* c = new C();

printf("/n/nclass C size: %X/n",sizeof(C));

check(reinterpret_cast<int**>(c));

check(reinterpret_cast<int**>(c)+1);

A* x = c;

printf("/n/nclass A size: %X/n",sizeof(x));

check(reinterpret_cast<int**>(x));

B* y = c;

printf("/n/nclass B size: %X/n",sizeof(y));

check(reinterpret_cast<int**>(y));

delete a;

delete b;

delete c;

}



执行结果:

您的浏览器可能不支持显示此图像。

执行解释:

由于class A,B 都没有成员变量唯有虚函数,并且没有多重父类。所以其内存进存储虚函数标识指针,所以其大小都为四字节。

  1. A只有一个虚函数A1(), 其虚函数表地址0x40A238, A1函数地址 0x401240.
  2. B只有一个虚函数B1(), 其虚函数表地址0x40A24C, B1 函数地址 0x401280.
  3. C,由于其多重继承,C++实现多重继承采取了一种很简单的做法:如果一个对象继承了N个父类,在对象中一次放置N个虚函数表指针,每个指针指向一个虚函数表,此虚函数表内容完全复制其对应父类的虚函数表项。如果子类也有虚函数,将子类虚函数放置在第一个虚函数表中。对于C.
  • 由于其继承了两个类A,B. 所以必须建立两个虚函数表指针,所以大小为8字节。由于在源码中声明第一个继承的父类为A, 所以第一个虚函数表指针所指向的虚函数表,会以类A的虚函数表内容来填充。第二个虚函数表指针所指向的虚函数表对应的以B的虚函数表中的内容来填充。由于C有虚函数,其虚函数填充在第一个虚函数表,对应于类A的那个虚函数表。

  • 对于类C对象其第一个虚函数表地址 0x40A268 ,其中填充了 0x401240, A1的函数地址。

  • 0X4012E0 是类C虚函数C1的函数地址。

  • C对象第二个虚函数表地址 0x40A260,其中填充了0x401280 ,这个是B1的函数地址。

  • 如果类C转化为A对象,只需屏蔽B的成员变量,对应类B的虚函数指针。同理转化为B对象时,只需提取B虚函数表指针,和其成员变量。类C对象图如下:

  • 您的浏览器可能不支持显示此图像。



C++对象中如果有复杂的对象关系,如果类D继承自类B,C,同时类B,C又继承自A(如图)

您的浏览器可能不支持显示此图像。

如果按照一般方式的继承,那么D中有两份A,所以可以采取virtual public继承,当采用此种方式,对象继承中不存储多份父类对象。不过COM中不能采取这种虚拟继承。虚拟继承其实现及规则,鉴于篇幅,略去。

  1. 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

(进程内组件)服务原理(宏观):

  •  
    1. COM组件将自己注册到Windows注册表中。此组件有唯一标示的GUID Windows注册表将此GUID与注册的COM组件相映射,(确切的说注册表中将CLSD_Hello这个GUIDserver.dll所在文件路径练习起来。可以形象的看成CLSID_HelloE:/InProc/server/server.dll是一个pair关系,Windows可以通过CLSID_Hello找到server.dll,找到server.dllwindows就可以加载)。
    2. 应用程序当需要调用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;

};

  1. COM库头文件中的interfaceC++interface 关键词是有不同意义的,在objbase.hCOM库重新定义了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关键词。

  1. Client与组件的交互都是通过COM接口完成的,组件不仅包含一个接口集,组件本身也是COM接口。
  2. 所有的COM接口都继承自IUnknown IUnknown定义了三个纯虚函数,所以每个COM接口的vtbl前三个函数都是QueryInterface, AddRef, Release. 这样每个COM接口都可以作为IUnknown接口来操作。如果的到一个IUnknown接口指针,就可以通过IUnknwon::QueryInterface来查询此接口所支持的其他COM接口。
  3. Client可以调用COM组件的QueryInterface函数,以判断组件是否支持某个特定接口。如果支持则返回一个指向此接口的指针,否则返回一个错误代码。其函数原型:
HRESULT __stdcall QueryInterface(const IID& iid,void** ppv);
  1. HRESULT: (Here is the result); 它是一个特定接口的32bit.其结构:
  • 您的浏览器可能不支持显示此图像。

  • QueryInterface可以返回S_OK, E_NOINTERFACE. 可以使用SUCCEEDED, FAILED宏来判断这个返回值,以确定调用是否成功。判断是否调用成功主要参考31bit,如果是1则表示失败,是0则成功。SUUCCEEDED FAILED宏就是利用31bit判断是否成功的。

  1. __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首先将参数入栈,然后存放当前代码的执行地址入栈,然后调用BB首先保存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参数取出,并入eaxx相加。

    •  
      •  
        •  
          • // 函数调用返回都将返回参数入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,在栈中分

    •  
      •  
        • //配了12个字节。您的浏览器可能不支持显示此图像。

    Movl $0x2,-4(%ebp); // ebp-0x4a的存储地址。给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参数取出,并入eaxx相加。

    •  
      •  
        •  
          • // 函数调用返回都将返回参数入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-0x4a的存储地址。给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中有趣的可变参数这个特性。

  1. QueryInterface第一个参数const IID& iid是一个接口标示符,IID是一个16字节的GUID. 用于唯一标示一个接口,类似与IP地址唯一标示一台计算机一样。(可以利用VC自带工具uuidgen, guidgen来生成guid,命令行键入uuidgenguidgen,try it and have fun!
  2. 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{ };

  • CAQueryInterface形式如下:

  • 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, IYIUnknown纯虚函数都是指向同一个实现,所以在此多重继承中,如果要返回IUnknown接口,可以执行static_cast<IX*>(this),或者static_cast<IY*>(this),或者static_cast<IUnknown*>(this). 由于这两个vtbl表前三项都完全一致。

  • 当向QueryInterface查询IID_IX标示的IX接口必须*ppv = static_cast<IX*>(this),同理IY接口。

  1. 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;)

  1. Windows注册表
  • COM只使用了注册表的一个分支HKEY_CLASSES_ROOT. 查看注册表,命令行运行Regedit命令。

HKEY_CLASSES_ROOT,CLSID子目录下,存储有操作系统中安装的所有组件的CLSID, 每个CLSID有个缺省的名字,如:

您的浏览器可能不支持显示此图像。

CLSID目录的一般结构如下:

您的浏览器可能不支持显示此图像。

  •  
    1. InprocServer32, 此关键字的缺省值为组件的DLL文件的名称。
  •  
    • 您的浏览器可能不支持显示此图像。

  •  
    • 使用InprocServer32作为关键字是标示这个dll是一个进程中的服务器,他被加载到CLIENT的进程中。

  •  
    1. ProgID: 程序员给某个CLSID指定的一个程序员易记的名称:
  •  
    • 您的浏览器可能不支持显示此图像。

  •  
    • PogID的值一般采用如下格式:

  •  
    • Progarm + . + Component+ . + Version.

  •  
    1. VersionIndependentProgID: 与版本号无关的ProgID:
  •  
    • 您的浏览器可能不支持显示此图像。

  •  
    • 与版本号无关的ProgID,映射到当前最新版本的组件。其格式一般为: Program+ . + Component

  •  
    1. HKEY_CLASSES_ROOT下面也直接列出了ProgID, VersionIndependentProgID.这是方便查询
  •  
    • 您的浏览器可能不支持显示此图像。

DLL如何注册到注册表中呢?

由于DLL知道它所包含的组件,因此dll可以完成这些信息的注册,由于DLL不能做任何事情,所以输出如下两个函数:

extern "C" HRESULT __stdcall DllRegisterServer();

extern "C" HRESULT __stdcall DllUnregisterServer();

COM组件在DllRegisterServer DllUnreisterServer函数中操作注册表:

注册:

  1. HKEY_CLASSES_ROOT//clsid下注册组件CLSID.
  2. 在新注册的CLSID目录下,填充InProcServer32 ProgID, VersionIndependentProgID三个关键字。
  3. HKEY_CLASES_ROOT//下注册versionIndependentProgID,并在注册后的VersionIndePendentProgID下填充CLSID, CurVer两个关键字。
  4. HKEY_CLASSES_ROOT下注册ProgID,并在此ProgID下填充CLSID关键字。

卸载:

清除组件注册是创建的所有内容。(请参考Registry.cpp Server.cpp的相关代码)。

  1. 创建组件对象
  • 组件已经注册到注册表中,可以调用COM库函数创建组件对象。相关函数:

  1. CoCreateInstance.

HRESULT __stdcall CoCreateInstance(const IID& clsid, IUnknown* pIUnknownOuter , DWORD dwClsContext, const IID& iid, void** ppv);


  1. clsid 待创建组件的clsid。这个clsid就是HKEY_CLASSES_ROOT//clsid下的组件GUID
  2. pIUnknownOuter, 用于多个组件共同协作时。(暂略)。
  3. dwClsContext,限定创建组件的执行上下文。本文的实例是CLSCTX_INPROC_SERVER, 进程内组件服务器。
  4. iid , 组件所支持的COM接口IID.
  5. ppv , 返回此接口指针。

  • CoCreateInstance 实际并没有直接创建COM组件,而是创建了一个称作类厂的组件,而所需的组件则是此类厂所创建。类厂组件的功能就是创建其他组件。确切地说:某个特定的类厂只创建只和某个特定的CLSID相应的组件。创建组件的标准接口是IClassFactory,CoCreateInstance创建组件实际上是通过IClassFactory创建的。

  • 客户是如何通过一个类厂直接创建所需组件?

  • 为创建某个CLSID组件对应的类厂, COM库提供了一个函数:

  • CoGetClassObject 原型:

HRESULT __stdcall CoGetClassObject(

  • const CLSID& clsid,

  • DWORD dwClsContext,

  • CONSERVERINFO* pServerInfo,

  • const IID& iid,

  • void** ppv

  • );

  1. clsid , 带创建组件的CLSID
  2. dwClsContext, 组件执行上下文环境。
  3. pServerInfo 用于DCOM.
  4. Iid , 支持的COM接口IID
  5. ppv, 返回接口对象的指针。

  • CoGetClassObject返回的是一个指向所需组件的类厂,而不是指向组件本身的一个指针。Client可以通过CoGetClassObject返回的指针来创建组件,这个指针就是IClassFactory

  1. IClassFactory:

interface IClassFactory: IUnknown

 

{

 

HRESULT __stdcall CreateInstance(IUnknown* pUnknownOuter,

 

const IID& iid,

 

void** ppv);

 

HRESULT __stdcall LockServer(BOOL bLock);

 

};

  • IClassFactory有两个成员函数:

  1. 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完成新组件的创建。

  1. 类厂的实现:
  • COM组件是dll时,CoGetClassObject调用dll中的DllGetClassObject创建类厂:

extern "C" HRESULT __stdcall DllGetClassObject(

 

const CLSID& clsid,

 

const IID& iid,

 

void **ppv);

  •  
    1. clsid, 类厂要创建的组件的CLSID.
    2. Iid, client希望通过类厂来创建的com接口iid.
    3. ppv返回接口的指针。

组件的创建过程类似下图:

您的浏览器可能不支持显示此图像。

5 In-Process-Server(com dll)

(为减小代码量,方便阅读,程序一些检查错误的代码略去,如GetModuleFileName,必须检查其返回值是否失败等,程序中都未检查其返回值,以确保这个例子短小易懂。)

  1. 快速编译运行一个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.cppDllGetClassObject.

  1. 创建一个Server对象 ,此时Server对象的引用计数初始化为1,标示有一个引用。
  2. 调用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, 则返回错误代码。ShowIIDServer.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.huuids.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中运行的呢?

  1. Client.exe 执行流程:
  1. 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

  1. 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;

 

}


其调用次序:

  •  
    1. CoGetClassObject 查找CSLID_Hello组件,如果内存中没有CLSID_Hello组件,从注册表中获取组件程序的路径名,加载server.dll。调用server.dll中的DllGetClassObject.
    2. DllGetClassObject 执行次序:
    1. 进入函数,打印调试讯息” Server: DllGetClassObject()”,后面还打印了一串数字“00000001 00000000 000000c0 46000000“,这是IID_IClassFactory的值,标示CoGetClassObject调用DllGetClassObjectiidIID_IClassFactory,要创建类厂。所以打印

(1)

  •  
    1. new 一个Server对象,Server构造函数会打印一个调试讯息,所以打印

(2)

  •  
    1. 调用新对象的Queryinterface,所以打印

(3), 由于QueryInterface内部调用AddRef,所以打印(4).

  •  
    1. DllGetClassObject退去,打印

(5).

  •  
    1. DllGetClaaObject执行完毕返回到CoGetClassObject函数中。(6)-(10)打印是由于CoGetClassObjectDllGetClassObject对类厂做了一次QueryInterface做了一次查询,查询的iidCoGetClassObject的查询的iid,目的是确保类厂支持此接口。

  1. pIFactory->CreateInstance(NULL,IID_IX,(void**)&pIX);
  • 如果类厂支持IID_IX 接口。则pIX指向IX接口。

  1. 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….”等。

  1. 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,则不能卸载。

  1. CoUninitialize();
  • Client.cpp 调用CoUnitialize() 关闭当前线程的COM库,释放无引用的DLL,此时会调用CoFreeUnusedLibraries 。由于组件对象引用计数为0client没有锁定server ,所以DllCanUnloadNow返回S_OK,从而会将server.dll卸载。

浏览下调用流程

  1. COM库在Windows注册表中查找CLSID_Hello组件,当发现server.dll没有装载在client.exe的进程中,windows会将server.dll loadclient.exe进程中。
  2. 加载server.dll后,COM库调用server.dllDllGetClassObject创建组件类厂,,在此类厂中查询IID_IX, 并返回组件对象。程序退出时调用CoUninitialize().释放dll。组件对象维护自己的引用的计数,当发现无人引用时,销毁自己。

  1. 组件注册到windows注册表中:
  • 组件通过CoCreateInstance,可以在Windows注册表中查找组件。那么组件如很将其注册到注册表中呢?组件server.dll提供了两个导出函数DllRegisterServer() , DllUnregisterServer(); DllRegisterServer函数可以将组件自注册到Windows注册表中。(可以调用cmd命令: regsvr32 server.dll regsvr32会调用server.dllDllRegisterServer将其注册到注册表中;当解除注册可以: 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;

    }

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值