用C实现的一个基本COM接口IFoo(来自COM Programmer's Cookbook)——C实现COM接口系列2

用C实现的一个基本COM接口IFoo(来自COM Programmer's Cookbook)——C实现COM接口系列2

在C实现COM接口系列1中实现的com接口IFoo与使用它的客户耦合在一起,没有实现在各自分离的模块,因此不符合模块化编程思想。本期添加类厂支持,以使接口的实现与接口的使用相分离。

---------------------------------------------------
类厂的作用到底是什么?
将接口的实现与客户使用分离开来吗?

不尽然。使用CoCreateInstance,客户可以完全不必知道类厂的存在,而创建组件,获取组件实现的接口并使用。

即COM库可以完全抛开类厂的概念,而是提供一个这样的函数原型:
CoCreateObject(REFID rclsid,...,REFID riid,void **ppItf);
用户在调用的时候可以对riid提供IID_Unknown或者特定于该对象的一个接口,直接获取该对象的IUnknown或特定的接口指针。

可以看到,这正是CoCreateInstance所作的事情。

1 类厂提供了间接创建类对象的方式:用户可以先获取并持有类厂接口指针,通过该指针所指向的类厂接口创建类对象。适用于需要创建多个(或重复创建)类对象的地方,减少了每次都要定位对象库并把对象库装入内存的开销。
2 类厂提供了保证组件库留在内存不被卸载出去的另一种方法:类厂接口函数LockServer。组件库维护一个库范围计数器,只有该计数器为0时,组件库才允许自己被卸载出内存。(与此相对,引用计数是类对象范围的,通过该类实现的各个接口来维护。如果一个类对象的引用计数达到0,那么该对象占有的内存就被释放,该对象上的接口指针也不再有效。)
除了调用LockServer锁定组件库以外,当创建的组件个数大于0时,组件库也不能被卸载。也可以说,调用一次LockServer()的作用相当于创建了一个组件。

-----------------------------------------------------------------------
客户一侧:
1 使用一个接口需要知道哪些信息?
备选:
接口IID
类对象(类厂)CLSID(或ProgID)
接口函数原型(参数个数,类型,返回值)
实现接口组件的线程模型(进程内、进程外、远程)?
类型库typelib信息?

服务一侧:
2 实现一个组件和接口以供客户调用,需要提供哪些东西?
备选:
所有客户使用组件和接口所需的内容
额外的还有:


--------------------------------------------------------------------
为dll添加.def文件与直接在需要导出的函数定义处指定_declspec( dllexport )有区别吗?如果有是什么区别?

我发现在outdll.c中这样指定:
__declspec( dllexport ) HRESULT DllGetClassObject (REFCLSID rclsid, REFIID riid, void **ppv)
会产生编译错误:
1>------ Build started: Project: outside, Configuration: Debug Win32 ------
1>Compiling...
1>outdll.c
1>d:\outside-cf\outside\outdll.c(19) : error C2375: 'DllGetClassObject' : redefinition; different linkage
1>        c:\program files\microsoft visual studio 8\vc\platformsdk\include\objbase.h(833) : see declaration of 'DllGetClassObject'
1>Build log was saved at "file://d:\outside-cf\outside\Debug\BuildLog.htm"
1>outside - 1 error(s), 0 warning(s)
========== Build: 0 succeeded, 1 failed, 1 up-to-date, 0 skipped ==========

c2375的解释意思是出错的函数使用的链接指示符与之前声明的不同。
Compiler Error C2375
'function' : redefinition; different linkage

The function is already declared with a different linkage specifier.

objbase.h中声明了DllGetClassObject()函数:
STDAPI  DllGetClassObject(IN REFCLSID rclsid, IN REFIID riid, OUT LPVOID FAR* ppv);

而使用.def文件就没有问题。

-----------------------------------------------------------------------------
初次执行结果:

问题就是总有一个分配的内存没有释放:

用C实现的一个基本COM接口IFoo(来自COM Programmer's Cookbook)——C实现COM接口系列2_第1张图片

 


根据打印出来的内存地址可以判断,应该是先创建的类厂对象的内存没有释放。
检查代码,main()中并没有忘记调用Release(pCF)释放类厂对象。打印Release(pCF)的返回值,发现是1,即在类厂接口指针上少调用了一次Release,那么,究竟是哪里少的呢?

main()函数中有关类厂对象引用计数的地方就是CoGetClassObject和Release(CreateInstance跟类厂自己的引用计数无关),这是一对增加引用计数和减少引用计数的对应操作,所以,main()中应该没有问题。

那么,就只有创建类厂对象的时候了。下面看一下类厂对象是如何创建的。
首先,main调用CoGetClassObject,该函数就调用dll中的DllGetClassObject。由于是第一次调用(不考虑其他客户使用该dll的情况),程序执行到CreateClassFactory(...),该函数执行完后,类厂对象的引用计数是1。

由于创建成功,因此继续向下执行到QueryInterface,此时,类厂对象的引用计数变成了2。然后,DllGetClassObject返回,com库函数CoGetClassObject也应该返回。注意,此时的类厂对象引用计数已经是2了!

因此,问题就出在这里。main调用一次CoGetClassObject后,类厂对象的引用计数是2,而不是我想向中的1。于是,后面调用一次Release也就当然无法释放掉类场对象了。

 

 

 1  HRESULT DllGetClassObject (REFCLSID rclsid, REFIID riid,  void   ** ppv)
 2  {
 3       * ppv  =   0 ;
 4       if  (IsEqualCLSID (rclsid,  & CLSID_Outside))
 5      {
 6 
 7           if  ( ! vpcfOutside)
 8 
 9          {
10 
11              HRESULT hr  =  CreateClassFactory ( & CLSID_Outside, CreateOutside,
12                                                & IID_IClassFactory,  & vpcfOutside);
13 
14                   if  (hr  !=  NOERROR)
15 
16                       return  hr;
17          }
18 
19           return  QueryInterface (vpcfOutside, riid, ppv);
20 
21      }
22 
23       return  E_FAIL;
24  }

 

 


找到了原因,改正就很容易了。这里我觉得需要把DllGetClassObject作如下修改:

 1  HRESULT DllGetClassObject (REFCLSID rclsid, REFIID riid,  void   ** ppv)
 2  {
 3       * ppv  =   0 ;
 4       if  (IsEqualCLSID (rclsid,  & CLSID_Outside))
 5      {
 6 
 7           if  ( ! vpcfOutside)
 8 
 9          {
10 
11              HRESULT hr  =  CreateClassFactory ( & CLSID_Outside, CreateOutside,
12                                                & IID_IClassFactory,  & vpcfOutside);
13 
14                   if  (hr  !=  NOERROR)
15 
16                       return  hr;
17 
18       if (IsEqualIID(riid, & IID_IClassFactory))
19      {
20        * ppv  =  vpcfOutside; //  Set *ppv to vpcfOutside directly instead of QueryInterface if first time creation
21        return  NOERROR;
22      }
23       else
24      {
25       Release(vpcfOutside); //  Any interface requested (riid) other than IID_ClassFactory and IID_Unknown not support by class factory,
26                             //  call Release to free the memory.
27        return  E_FAIL;
28      }
29 
30          }
31 
32           return  QueryInterface (vpcfOutside, riid, ppv);
33 
34      }
35 
36       return  E_FAIL;
37  }

 

修改后在执行,内存都正常释放了。

用C实现的一个基本COM接口IFoo(来自COM Programmer's Cookbook)——C实现COM接口系列2_第2张图片

-------------------------------------------------------------------------------------------
CreateClassFactory代码说明

 1  HRESULT CreateClassFactory (REFCLSID rclsid,
 2      HRESULT ( * pfnCreate)(IUnknown  * , REFIID,  void   ** ), 
 3      REFIID riid,  void   ** ppv)
 4  {
 5      ClassFactory  * this ;
 6      HRESULT hr;
 7      
 8       * ppv  =   0 ;
 9       if  (hr  =  Alloc ( sizeof  (ClassFactory),  & this ))
10       return  hr;
11 
12       this -> icf.lpVtbl  =   & vtblClassFactory;
13       this -> cRef  =   1 ;   //  After this call, cRef==1
14 
15       this -> pfnCreate  =  pfnCreate;
16      
17      hr  =  QueryInterface ( & this -> icf, riid, ppv);   //  After this call, cRef==2
18      Release ( & this -> icf);   //  Corresponds to "this->cRef = 1", ater this call, cRef==1
19 
20       return  hr;
21  }

 

可以看到,两行代码的效果是对引用计数增1及减1,这两行代码执行后,对引用计数的影响互相抵消,等于没有改变引用计数。那么,把这两行同时注释掉,是不是可以呢?
我的回答是:在本例中可以。因为这两行代码之间的QueryInterface总是可以执行成功的(因为是用IDD_ClassFactory来调用该函数的)。所以,即便把这两行代码同时注释掉,CreateClassFactory执行结束后,类厂对象的引用计数也增了1,以后调用Release就可以释放掉类厂对象占用的内存。
但是,如果CFQueryInterface的代码编写中除了错误,比如,像这样写:

 1  static  HRESULT CFQueryInterface (IClassFactory  * pcf, REFIID riid,  void   ** ppv)
 2  {
 3      ClassFactory  * this   =  IMPL (ClassFactory, icf, pcf);
 4 
 5       if  (IsEqualIID (riid,  & IID_IUnknown)  ||
 6  //             IsEqualIID (riid, &IID_IClassFactory))    //  Comment out this condition to create an error
 7           * ppv  =   & this -> icf;
 8       else
 9      {
10           * ppv  =   0 ;
11           return  E_NOINTERFACE;
12      }
13 
14      AddRef ((IClassFactory  * ) * ppv);
15 
16       return  NOERROR;
17  }


那么,这两行代码之间的QueryInterface就会执行出错,那么类厂对象占用的内存就永远没有机会释放了。
也就是说,AddRef和Release虽然在作用上对引用计数来说相互抵消,但Release函数提供了释放对象内存的机会(当引用计数为0时),如果不成对的调用他们,也就失去了管理对象内存(释放对象占用的内存)的机会。

---------------------------------------------------------------------------
组件库outside文件说明:
 IFoo.h      IFoo接口声明
 outside.c   组件对象、IFoo接口实现
 cf.c        类厂对象、IClassFactory接口实现
 outdll.c    组件库导出函数实现
 outside.def 组件库模块定义文件,导出函数声明
 outside.reg 组件库注册文件

----------------------------------------------------------------------------
源码: outside-cf

你可能感兴趣的:(用C实现的一个基本COM接口IFoo(来自COM Programmer's Cookbook)——C实现COM接口系列2)