零基础学习COM之COM库跟类厂介绍

零基础学习COM之COM库和类厂介绍

       在windows系统中,注册表是一个正式的共享系统数据库。注册表中包含关于系统软硬件以及配置和用户的各种信息。在COM技术中使用注册表存储关于组件的信息。客户可以再注册表中搜索它需要的组件。


    注册表有许多关键字构成的层次结构。每个关键字又可以有一些列子关键字、以及值。使用regedit.exe可以编辑、查看注册表。


    在注册表HKEY_CLASSES_ROOT分支下有一个CLSID关键字。CLSID关键字下列有系统安装的所有组件的CLSID。在CLSID下最重要的关键字是InprocServer32.此关键字的值是组件所在的DLL路径名称。由于使用CLSID来查看组件非常的麻烦,因此在每个CLSID关键字下都对应着一个ProgID。它是程序员给某个CLSID指定的一个易记的名称。但是ProgID不能保证唯一。


     ProgID主要作用是获得相应的CLSID。COM库提供了两个函数CLSIDFromProgID和ProgIDFromCLSID.来完成ProgID和CLSID之间的相互转换。


    客户可以在注册表中查询需要的组件。 但是注册表中怎么存储我们的组件信息呢?这是因为每个DLL都知道它所包含的组件。DLL可以将它所包含的组建信息注册到注册表中。因此在每个包含组件的DLL中,我们必须要输出以下两个函数:


    

    extern "C" HRESULT _stdcall DllRegisterServer()

    extern "C" HRESULT _stdcall DllUnregisterServer()。


 


    这两个函数并不需要客户直接调用,而是提供给COM库调用的。COM会自动搜索这两个函数并调用。在许多程序的安装过程中大多数安装程序都会调用DllRegisterServer完成组件的注册。用户也可以使用程序REGSVR32.exe来手动注册某个组件。RegSvr32实际上上是通过调用上述两个函数来完成组件注册的。


    DllRegisterServer和DllUnregister是组件的生产者提供的,通过调用注册表函数来在注册表中添加某些项目。


    有关的注册表函数为:
     

 RegOpenKeyEx,
      RegCreateKeyEx,
      RegSetValueEx,
      RegEnumKeyEx,
      RegDeleteKey,
      RegClosekey.



   组件类别实际上就是一个接口集合。每个组件类别都有一个GUID。此时的GUID被称为CATID(category ID)。对于某个组件,如果它实现了某个组件类别的所有接口,那么它就可以注册为该组件类别的一个成员。这样,客户就能够通过从注册表中选择只属于某个特定组件类别的组件中准确找到所需的组件。


    COM库函数提供一一组对COM对象操作的函数。它们是在OLE32.DLL中实现 。
首先介绍COM的初始化函数。因为在使用其他函数之前必须调用CoInitialize来初始化COM库。不再使用COM库时必须调用CoUninitialize。对每个COM库只需初始化一次。COM库的初始化一般在客户代码中进行。在提供组件的dll中则不需进行。

    COM库可以提供给用户一个内存分配器。使用此分配器组件可以给客户提供一块内存。该内存可以由用户删除。
CoGetMalloc返回一个内存分配器IMalloc。可以使用IMalloc::Alloc申请一块内存。使用IMalloc::Free释放。上述过程比较麻烦。因此COM库实现了一些方便的帮助函数如CoTaskMemAlloc和CoTaskMemFree.它们分别完成内存申请和释放的过程。不再需要先获得内存分配器在申请和释放内存。


    在注册表中包含的CLSID是以字符串形式表示的。因此需要一些函数完成CLSID与字符串之间的转换。如:

  

  

     StringFromGUID2,
     StringFromCLSID,

      StringFromIID,

     CLSIDFromString,IIDFromString。

 


     前面我们介绍了使用CreateInstance来创建组建对象的例子。但是那是我们自己定义的函数。实际上在COM库中也提供了专门创建COM对象的函数:CoCreateInstance。此函数也是创建COM对象最简单的方式。但是CoCreateInstance却不太灵活。在此情况下引入了类厂。

    所有的组件都是通过类厂来创建的,客户使用类厂来创建对象有很大的灵活性。

    CoCreateInstance函数:

 

HRESULT _stdcall CoCreateInstance(

CLSID &clsid,

IUnknown *pIUnknownOuter,

DWORD dwClsContext,

Const IID&iid,

Void **ppv);


 

    clsid标识一个想要创建的组件。

    iid标识此组件的一个接口,ppv返回接口指针。此函数可以在创建COM对象的同时返回该对象的相应接口指针。

    dwClsContext限定所创建的组件的执行上下文。

    pIUnknownOuter用于组件聚合,稍后会有介绍。

 

 HRESULT hr=CoCreateInstance(CLSID_CA,NULL,CLSCTX_INPROC_SERVER,IID_IUnknown,(void**)&pI);

 if(!SUCCEEDED(h))

 {

   pI->IY_Func();

   pI->Release();

 }


 

   CLSCTX_INPROC_SERVER告诉CoCreateInstance要加载的是进程中服务器或dll中的组件。

    CoCreateInstance的第三个参数控制所创建的组件是在与客户相同的进程中运行,还是在不同的进程中运行或者是在另一台机器上运行。

此参数可以是一下值:

     CLSCTX_INPROC_SERVER 客户希望在同一进程创建组建。此组件必须在dll中实现。

     CLSCTX_INPROC_HANDLER客户希望创建进程中处理器。所谓进程中处理器实际上是只实现了某个组件的一部分的进程中组件

     CLSCTX_LOCAL_SERVER 客户希望创建一个在同一机器的另外一个进程中运行的组件。

     CLSCTX_REMOTE_SERVER 客户希望创建一个在远程机器上运行的组件。

   客户可以在三种不同的进程上下文中使用某个组件:进程中、本地及远程。

    下面的例子创建了纯COM客户和组件

int main(int argc,char**argv)

{

CoInitialize();

IX*pIX=NULL;

HRESULT hr=CoCreateInstance(CLSID_CA,NULL,CLSCTX_INPROC_SERVER,IID_IX,(void**)&pIX);


    if(SUCCEEDED(hr)

   {

      pIX->IX_Func();


     IY*pIY=NULL;

     hr=pIX->QueryInterface(IID_IY,(void**)&pIY);

     pIX->Release();

     if(SUCCEEDED(hr))

     {

        pIY->IY_Func();

       pIY->Release();

     }

  }


  CoUninitialize();

  return 0;

}


 

    前面我们提到过使用CoCreateInstance创建对象不太灵活,那么接下来我们将介绍更加灵活的创建方式:使用类厂。

    首先介绍COM库的CoGetClassObject,它接受一个标识组件的CLSID,并返回相应类厂中某个接口指针函数。

HRESULT _stdcall CoGetClassObject(

     Const CLSID&clsid,

     DWORD dwClsContext,

     COSERVERINFO*pServerInfo,

     Const IID&iid,

     Void **ppv);  


 

    可以看到该函数与CoCreateInstance的参数很类似。它们的第一个参数都是待创建的组件的CLSID。第二个参数均为创建组件的上下文。pServerInfo将被DCOM用于对远程组件的访问。

     但最大的差别在于CoGetClassObject返回的是创建组件的类厂的接口指针,而不是指向组件的接口指针。

    客户可以调用CoGetClassObject的返回的指针来创建相应的组件。该指针通常是一个IClassFactory指针。

IClassFactory接口声明如下:

class IClassFactory:public IUnknown

{

public:

 HRESULT _stdcall CreateInstance(

        IUnknown*pUnknownOuter,

        Const IID&iid,

        Void **ppv);

 HRESULT _stdcall LockServer

};


 

它有两个成员函数:

    CreateInstance第一个参数为指向某个IUnknown接口的指针,同CoCreateInstanceIUnknown指针是相同的,也是用于组件聚合。最后两个的参数与QueryInterface的参数是相同的。

但是CreateInstance没有接受标识要创建组件的CLSID,那么它如何获得要创建组件的CLSID呢?大家可以看下CoGetClassObject。它接受一个标识组件的CLSID。哦,原来如此!!

    其实在前面介绍过的CoCreateInstance ,它在实现时使用了CoGetClassObjectCreateInstance。虽然使用CoCreateInstance函数灵活性不好,但是很简单。在满足需要的前提下此函数经常被使用。接下来看一下CoCreateInstance的实现:

HRESULT _stdcall CoCreateInstance(

                const CLSID&clsid,

               IUnknown*pUnknownOuter,

               DWORD dwClsContext,

               const IID&iid,

               void **ppv)

{

     IClassFactory pIFactroy=NULL;

    HRESULT hr=CoGetClassObject(clsid,dwClsContext,NULL,IID_CLASSFACTORY,(void**)&pIFactroy);

   if(SUCCEEDED(hr))

   {


      pIFactroy->CreateInstance(pUnknownOuter,IID_IX,ppv);

      pIFactroy->Release();

      pIX->IX_Func();

     pIX->Release();

   }


}


 

    大多数情况下组件的创建都是使用CoCreateInstance而不是CoGetClassObject。但是当创建一个组件的多个实例时使用CoGetClassObject具有很提高的效率。因为此时并不需要创建多个类厂。一个类厂完成所有组件实例的创建。

DllGetClassObject 完成类厂的创建。此函数被CoGetClassObject调用。

STDAPI  DllGetClassObject(

     Const CLSID&clsid,

     Const IID&iid,

     Void **ppv);


    此函数的三个参数与CoGetClassObject相同。

接下来我们完整的介绍下组件的创建过程:

     首先,客户调用COM库函数CoGetClassObject,此函数调用组件内提供的函数DllGetClassObject完成类厂的创建并返回类厂指针,然后使用类厂指针调用COMIClassFactory::CreateInstance创建组件对象并返回接口指针。

    可以注意到dll中导出四个函数。它们分别是:

     

     DllGetClassObject,

      DllCanUnloadNow,

     DllReigisterServer,

     DllUnregisterServer。


     前面提到过可以在一个dll中实现多个组件。之所以可以实现这一点就是因为导出函数:DllGetClassObject。它能够根据不同的CLSID,创建对应的类厂。一个DLL可以支持多个组件也从侧面说明了DLL并不等价于组件,而是相当于一个组件服务器。

    DLL的卸载

    DllCanUnloadNowLockServer

    DLL内可以有多个组件。为了使DLL在所有组件都不使用后被卸载,需要在dll内维护一个当前可用组件的计数g_NumOfCom;当此值为0时说明没有对象正在被使用,dll就可以被卸载了。g_NumCom会在组件的构造函数或IClassFactory::CreateInstance中被增加,在组件的析构函数中减小。

    LockServerIClassFactory的成员函数,它对锁计数器进行操作。当锁计数器大于0时,可以防止该类厂所占空间被释放掉。假设客户拥有一个指向某类厂的指针,在某个DLL被卸载后,该类厂对应的空间被释放掉,类厂指针就变成了一个野指针,使用时会造成违规访问。

    初始时锁计数器为0LockServer(true)会使锁计数器加一,此时类厂对象被锁住,保留在内存中不被释放,使用完毕后再次调用此函数解锁。

LockServer(false)会使锁计数器的值减一。

    DllCanUnloadNow可以返回该DLL是否可以被卸载。若现在没有组件在被使用,那么此时该DLL就可以从内存中撤销了。

    组件个数和锁计数器可以使用同一个数值g_NumOfCom,也可以分别使用。当使用不同的值时在DllCanUnloadNow中就必须对这两个值进行判断,只有当它们都为0时,dll才能被卸载。

     Win7下调用RegCreateKeyExHKEY_CLASSES_ROOT添加键,需要在程序运行时获得管理员权限。否则返回值为5,权限不足。

下面的代码综合了前面介绍的所有知识,是一个相对比较完整的例子:

IX.h
#include"objbase.h"
class IX:public IUnknown
{
public:
	virtual void IX_Func()=0;
};

//IY.h
#include"objbase.h"
class IY:public IUnknown
{
public:
	virtual void IY_Func()=0;
};

//CA.h
#ifndef CA_H
#define CA_H
#include<iostream>
#include"IX.h"
#include"IY.h"
extern UINT g_NumOfCom;
class CA:public IX,public IY
{
public:
	CA()
	{
		m_Ref=0;
		g_NumOfCom++;
		std::cout<<"CA构造函数被调用!!"<<std::endl;
	}
	~CA()
	{
		std::cout<<"CA析构函数被调用!!"<<std::endl;
		g_NumOfCom--;
	}
	HRESULT _stdcall QueryInterface(const IID&id,void **ppv);
	ULONG _stdcall AddRef();
	ULONG _stdcall Release();

	void IX_Func();
	void IY_Func();
public:
	ULONG m_Ref;
};
#endif

//CA.cpp
#include"StdAfx.h"
#include"CA.h"
extern IID IID_IX;
extern IID IID_IY;
// {AB4B7F96-B8A5-4BB3-BF44-8FB158ED36AD}

extern IID IID_CA;
HRESULT  CA::QueryInterface(const IID&id,void**ppv)
{
	std::cout<<"CA::QueryInterface被调用!!"<<std::endl;
	if(id==IID_IUnknown)
	{
		*ppv=static_cast<IX*>(this);
	}
	else if(id==IID_IX)
	{
		*ppv=static_cast<IX*>(this);
	}
	else if(id==IID_IY)
	{
		*ppv=static_cast<IY*>(this);
	}
	else
	{
		*ppv=NULL;
		return E_NOINTERFACE;
	}
	static_cast<IUnknown*>(*ppv)->AddRef();
	return S_OK;
};
ULONG CA::AddRef()
{
	std::cout<<"CA::Addref被调用!!"<<std::endl;
	m_Ref++;
	return m_Ref;
}
ULONG CA::Release()
{
	std::cout<<"CA:release被调用!!"<<std::endl;
	if(--m_Ref==0)
	{
		delete this;
	}
	return m_Ref;
}
void CA::IX_Func()
{
	std::cout<<"IX_Func被调用!!"<<std::endl;
}
void CA::IY_Func()
{
	std::cout<<"IY_Func被调用!!"<<std::endl;
}

//CFactory.h
#include<objbase.h>

class CFactory:public IClassFactory
{
public:
	CFactory();
	~CFactory();
	HRESULT _stdcall CreateInstance(IUnknown*pUnknownOuter,const IID&iid,void **ppv);
    HRESULT _stdcall LockServer( BOOL fLock);
	HRESULT _stdcall QueryInterface(const IID&iid,void**ppv);
	ULONG _stdcall AddRef();
	ULONG _stdcall Release();
private:
	LONG m_Ref;
};


//CFactory.cpp
#include"stdafx.h"
#include"CFactory.h"
#include"CA.h"
// {186E5F66-438C-49EF-B8E3-29BB2B8CC133}


extern UINT g_NumOfCom;
 extern GUID CLSID_FACTORY;
UINT g_ServerLocks=0;
HRESULT _stdcall CFactory::CreateInstance(IUnknown*pUnknownOuter,const IID&iid,void **ppv)
{
	std::cout<<"CFactory::CreateInstance被调用!!"<<std::endl;
	if(pUnknownOuter)
	{
		return CLASS_E_NOAGGREGATION;
	}
	CA*pCA=new CA;
	if(!pCA)
	{
		std::cout<<"new CA申请失败!!"<<std::endl;
		return E_OUTOFMEMORY;
	}
	std::cout<<"new CA创建成功!!!"<<std::endl;
	HRESULT hr=pCA->QueryInterface(iid,ppv);
	pCA->Release();
	return hr;

}
HRESULT _stdcall CFactory::QueryInterface( const IID&iid,void**ppv )
{
	std::cout<<"CFactory::QueryInterface被调用!!"<<std::endl;
	//if(iid==IID_IUnknown||iid==CLSID_FACTORY)
	//if()
	{
		*ppv=static_cast<IClassFactory*>(this);
	}
	//else
	{
		//*ppv=NULL;
		//std::cout<<"CFactory:接口查询失败!!"<<std::endl;
		//return E_NOINTERFACE;
	}
	static_cast<IUnknown*>(*ppv)->AddRef();
	return S_OK;
}

ULONG _stdcall CFactory::AddRef()
{
	std::cout<<" CFactory::AddRef被调用!!"<<std::endl;
	m_Ref++;
	return m_Ref;
}

ULONG _stdcall CFactory::Release()
{
	std::cout<<"CFactory::Release被调用!!"<<std::endl;
	if(--m_Ref==0)
	{
		delete this;
	}
	return m_Ref;
}

CFactory::CFactory()
{
	std::cout<<"CFactory构造函数被调用!!"<<std::endl;
	m_Ref=1;
	g_NumOfCom++;
}

HRESULT _stdcall CFactory::LockServer( BOOL fLock )
{
	if(fLock)
	{
		g_ServerLocks++;
	}
	else
	{
		g_ServerLocks--;
	}
	return S_OK;
}

CFactory::~CFactory()
{
	std::cout<<"CFactory析构函数被调用!!"<<std::endl;
	g_NumOfCom--;
}

//dll.DEF
;LIBRARY "dll.dll" 
EXPORTS
    DllGetClassObject  PRIVATE
	DllCanUnloadNow  PRIVATE

//dll.h
// 下列 ifdef 块是创建使从 DLL 导出更简单的
// 宏的标准方法。此 DLL 中的所有文件都是用命令行上定义的 DLL_EXPORTS
// 符号编译的。在使用此 DLL 的
// 任何其他项目上不应定义此符号。这样,源文件中包含此文件的任何其他项目都会将
// DLL_API 函数视为是从 DLL 导入的,而此 DLL 则将用此宏定义的
// 符号视为是被导出的。
#ifdef DLL_EXPORTS
#define DLL_API __declspec(dllexport)
#else
#define DLL_API __declspec(dllimport)
#endif
#include"objbase.h"
 DllCanUnloadNow(void);
 DLL_API STDAPI DllGetClassObject(const CLSID&clsid,const IID&iid,void**ppv);
 extern "C" DLL_API HRESULT DllRegisterServer();
 extern "C" DLL_API HRESULT  DllUnregisterServer(); 
// dllmain.cpp : 
定义 DLL 应用程序的入口点。
#include "stdafx.h"
extern HMODULE g_DLLModule;
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
					 )
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
		g_DLLModule=hModule;
	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
		break;
	}
	return TRUE;
}

//GUID.cpp
#include"StdAfx.h"
 IID IID_IUnknown = 
{ 0x49fa8f03, 0x1ab0, 0x4d75, { 0xb0, 0x23, 0x54, 0xb7, 0xf, 0xa7, 0x31, 0xc2 } };

IID IID_IX = 
{ 0x8af3709f, 0xa8eb, 0x46c4, { 0xb5, 0x1, 0xbc, 0xb6, 0x7d, 0x45, 0x9a, 0xfe } };
IID IID_IY = 
{ 0xc18d13a4, 0x57af, 0x41d7, { 0xb5, 0xf2, 0x46, 0xc1, 0xfe, 0xa6, 0xbc, 0x37 } };
CLSID CLSID_CA = 
{ 0xab4b7f96, 0xb8a5, 0x4bb3, { 0xbf, 0x44, 0x8f, 0xb1, 0x58, 0xed, 0x36, 0xad } };
CLSID CLSID_FACTORY = 
{ 0x186e5f66, 0x438c, 0x49ef, { 0xb8, 0xe3, 0x29, 0xbb, 0x2b, 0x8c, 0xc1, 0x33 } };