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 | |
参考书籍:
|
实验1: C对象布局
C++中,一个对象在内存中只存放其成员变量,所有的对象共享类函数。那么C++中类函数如何实现,及虚函数如何实现呢?首先我们看C++对象的内存布局。Let’s start from the simplest!
源码:
运行结果:
执行解释: 为什么Base对象的大小为8字节呢? 由于Base对象只存储成员变量m_integer与m_char,所以其内存大小为其成员变量所占有的内存。 如果存储m_integer,m_char,其内存大小应该为5字节,但为什么是8字节呢? 这是由于字节对齐的原因。内存中数据结构会采用4字节对齐,m_char虽只占有一字节,但操作系统会分配四字节,第一个字节赋给m_char,余下三字节用于对齐.字节对齐的原因是增加内存存储速度(详细解释,此处限于篇幅与时间,不加解释,请随意参考一本操作系统的书籍,或请参考《深入理解计算机系统》3.10,其中有精彩的解释)。 那么,对象中函数如Base(),~Base(),Hello()又存储在什么地方呢? 计算机中不管是什么面向对象的语言,最后翻译成二进制,面向对象仅是包装而已。最后.exe的形式形象一点都是类似如C中的函数形式。 命令行运行: objdump -r test1.obj 会发现有趣的东西:
所有的类函数都被翻译成这种有着奇怪名字的C函数。 那么编译器是怎样转化类函数名字的呢? 不同版本的C++编译器采取的转化格式都略微不同。此版本编译器为(VS2005 C++)将C++函数翻译成底层函数时,其C++函数名转换C函数名采用如下格式(构造函数与析构函数比较特殊,不采用这种格式): ?+函数名+@+类名+@@QAE+返回值类型+参数类型+参数结束字符;
C++由于支持函数重载,所以二进制中函数形式采用函数名+参数的形式来命名函数;转换后的函数名由@@QAE标示其后字符标示返回类型和参数类型。其中类型字符转换表为:
所以: 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函数的反汇编代码,稍微解释一下:
mov ebp,esp;
** 小秘密: 构造函数都在做些什么呢? C++类构造函数其实是一个很复杂的函数,虽然代码撰写者只需在构造函数中撰写代码。如:
括号内撰写代码就可以了,其实C++编译器会做一些神秘的事情。他会在这个构造函数的起始处添加若干很重要的代码,形象点说他在Base构造函数的括号外又添加了若干代码。这样构造函数就如下的格式:
C++编译器在里面写了什么代码,限于篇幅,大略的写一下了。他会做如下的事情如果有父类,调用父类的构造函数;如果有虚函数,设置虚函数表指针(将类对象的第一个四字节的值设为指向此对象的虚函数表的位置。);如果有成员变量,初始化成员变量。类似如下:(假设B类有父类A,且有虚函数,有成员变量)。严格按照这个顺序!
让我们回到主题上来! Base b; b.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.exe将test1.Obj中重定向代码都重新赋了地址值。(请参考编译原理书籍)。 总结:
** 还有一个小秘密,为什么用printf, 而不用std::cout呢?是不是作者对C++一窍不通,还是是一个顽固的C语言爱好者呢?请尝试使用objdump等工具,你会找到答案。Try it and have fun! |
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 。 稍微说明一下。
Dumpbin /disasm /section:.text test.exe|more 你可以反汇编到.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大小
Char* s = “Hello,world!”; “Hello,world!” 就是只读数据。Literl constant var! 这三个区是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 class BaseFlag { public: BaseFlag() { std::cout<<"BaseFlag::BaseFlag()"< } ~BaseFlag() { std::cout<<"BaseFlag::~BaseFlag()"< } }; class ExtendFlag { public: ExtendFlag() { std::cout<<"ExtendFlag::ExtendFlag()"< } ~ExtendFlag() { std::cout<<"ExtendFlag::~ExtendFlag()"< } }; class Base { public: Base() { std::cout<<"Base::Base()"< } ~Base() { std::cout<<"Base::~Base()"< } virtual void Show() { std::cout<<"Base::Show()"< } void Hello() { std::cout<<"Base::Hello()"< } BaseFlag bf; }; class Extend : public Base { public: Extend() { std::cout<<"Extend::Extend()"< } ~Extend() { std::cout<<"Extend::~Extend()"< } virtual void Show() { std::cout<<"Extend::Show()"< } void Hello() { std::cout<<"Extend::Hello()"< } 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) |
|
执行解释:
C++中class对象初始化,如果对象是个继承类,首先初始化父类,然后初始化成员变量,然后再调用对象构造函数重新赋值成员变量等。所以Extend e;
至此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对象,其vtbl是Base虚函数表。Check函数执行完毕,这个临时对象要销毁,所以调用Base析构函数。所以打印(7)。由于有成员变量,所以也要析构其成员变量,所以打印(8)。
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 都没有成员变量唯有虚函数,并且没有多重父类。所以其内存进存储虚函数标识指针,所以其大小都为四字节。
由于其继承了两个类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中不能采取这种虚拟继承。虚拟继承其实现及规则,鉴于篇幅,略去。 |
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
组件是一个接口(
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; }; |
#define __STRUCT__ struct #define interface __STRUCT__ |
在COM库中将interface 定义成了struct。 由于struct 与class之间差别在于: class成员默认属性是private, 而struct是 public。所以所有直接include COM库头文件objbase.h或间接inlcude objbase.h,其中 interface的定义都会是一个struct关键词。
HRESULT __stdcall QueryInterface(const IID& iid,void** ppv); |
QueryInterface可以返回S_OK, E_NOINTERFACE. 可以使用SUCCEEDED, FAILED宏来判断这个返回值,以确定调用是否成功。判断是否调用成功主要参考31bit,如果是1则表示失败,是0则成功。SUUCCEEDED 与FAILED宏就是利用31bit判断是否成功的。
C/C++全局函数默认调用方式是__cdecl( c language default call),而C++类函数是__thiscall标示有this指针的传递。
__cdecl: 函数参数自右至左入栈,函数调用这负责清除栈中的参数。如:
首先看下汇编的函数调用,假设A调用B, 当调用某个函数时,A首先将参数入栈,然后存放当前代码的执行地址入栈,然后调用B,B首先保存A函数的栈基址,这样当B返回时,可以恢复函数A的堆栈。如果B的原型为: void B(int x,int y);则栈的形式如下:
所以如果B函数要引用参数必须是 0x8+ebp;同理y: 0xd+ebp add汇编代码如下:
Main汇编代码如下:
可见__cdecl方式是参数由右至左入栈,且调用函数main清除因参数入栈所带来的栈分配(push eax; push ecx;(相当如 sup 0x8,%esp)在栈中存储了x,y参数。必须清除,所以当add返回时,main调用add 0x8,%esp;清除参数). |
__stdcall
,与
__cdecl
略有不同,参数也是自右至左入栈,但其参数清除却是由
被调用函数清除。即如上例如果
add
是
__stdcall,
则
add
在返回时必须要清除参数
x,y
在栈中的分配的地址。
add汇编代码如下:
Main汇编代码如下:
|
__stdcall由被调用函数清理堆栈参数,__cdecl有调用函数清理堆栈,这样如果有多个函数调用__cdecl函数,那么多个函数都必须要一行代码执行清理堆栈的动作。如果__stdcall,程序中只有被调用函数执行清理动作,只有一行。所以__stdcall会减小汇编代码量。但__stdcall这种方式,会导致不能利用C中有趣的可变参数这个特性。
看下如何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 }else if( iid == IID_IX) { *ppv = static_cast }else if(iid == IID_IY) { *ppv = static_cast }else { *ppv = NULL; return E_NOINTERFACE; } static_cast return S_OK; } |
当client查询IID_IUnknown时,由于COM接口都继承自IUnknown,所以支持IUnknown接口。由于CA继承自IX,IY, IX,IY都继承自IUnknown,这是多重继承,CA的虚函数表如下图:
由于IX, IY中IUnknown纯虚函数都是指向同一个实现,所以在此多重继承中,如果要返回IUnknown接口,可以执行static_cast
当向QueryInterface查询IID_IX标示的IX接口必须*ppv = static_cast
由于组件使自己维护自己的生存周期,当发现系统中无人需要自己时,组件自动销毁。为完成这种功能,组件提供了引用计数的功能。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;)。
在HKEY_CLASSES_ROOT,在CLSID子目录下,存储有操作系统中安装的所有组件的CLSID, 每个CLSID有个缺省的名字,如:
CLSID目录的一般结构如下:
使用InprocServer32作为关键字是标示这个dll是一个进程中的服务器,他被加载到CLIENT的进程中。
PogID的值一般采用如下格式:
Progarm + . + Component+ . + Version.
与版本号无关的ProgID,映射到当前最新版本的组件。其格式一般为: Program+ . + Component
DLL如何注册到注册表中呢?
由于DLL知道它所包含的组件,因此dll可以完成这些信息的注册,由于DLL不能做任何事情,所以输出如下两个函数:
extern "C" HRESULT __stdcall DllRegisterServer();
extern "C" HRESULT __stdcall DllUnregisterServer();
COM组件在DllRegisterServer 和DllUnreisterServer函数中操作注册表:
注册:
卸载:
清除组件注册是创建的所有内容。(请参考Registry.cpp 及Server.cpp的相关代码)。
HRESULT __stdcall CoCreateInstance(const IID& clsid, IUnknown* pIUnknownOuter , DWORD dwClsContext, const IID& iid, void** 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 ); |
CoGetClassObject返回的是一个指向所需组件的类厂,而不是指向组件本身的一个指针。Client可以通过CoGetClassObject返回的指针来创建组件,这个指针就是IClassFactory。
interface IClassFactory: IUnknown
{
HRESULT __stdcall CreateInstance(IUnknown* pUnknownOuter,
const IID& iid,
void** ppv);
HRESULT __stdcall LockServer(BOOL bLock);
}; |
IClassFactory有两个成员函数:
创建组件时,首先创建组件的类厂,然后用所获取的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); |
组件的创建过程类似下图:
(为减小代码量,方便阅读,程序一些检查错误的代码略去,如GetModuleFileName,必须检查其返回值是否失败等,程序中都未检查其返回值,以确保这个例子短小易懂。)
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
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
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
#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
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
}else
{
*ppv = NULL;
printf(" No interface/n");
return E_NOINTERFACE;
}
reinterpret_cast
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.
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
}else
{
*ppv = NULL;
printf(" No interface/n");
return E_NOINTERFACE;
}
reinterpret_cast
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
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
#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
#include
#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中运行的呢?
所有的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。
(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)。
(2)。
(3), 由于QueryInterface内部调用AddRef,所以打印(4).
(5).
如果类厂支持IID_IX 接口。则pIX指向IX接口。
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….”等。
由于程序将退去,不再需要组件对象,递减其引用技术。引用计数为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,则不能卸载。
Client.cpp 调用CoUnitialize() 关闭当前线程的COM库,释放无引用的DLL,此时会调用CoFreeUnusedLibraries 。由于组件对象引用计数为0,client没有锁定server ,所以DllCanUnloadNow返回S_OK,从而会将server.dll卸载。
浏览下调用流程:
组件通过CoCreateInstance,可以在Windows注册表中查找组件。那么组件如很将其注册到注册表中呢?组件server.dll提供了两个导出函数DllRegisterServer() , DllUnregisterServer(); DllRegisterServer函数可以将组件自注册到Windows注册表中。(可以调用cmd命令: regsvr32 server.dll; regsvr32会调用server.dll的DllRegisterServer将其注册到注册表中;当解除注册可以: regsvr32 –u server.dll ; 调用DllUnregServer解除注册。)
参阅<
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; } |