2.实现一个最简单的COM

  • Post author:
  • Post category:其他


上一篇说了怎么设计一个伪COM,现在我们来看看真正的COM库是怎么实现模块化的。

先提一个问题,上一节我们实现的伪COM有没有什么令你不爽的地方?对的,相信很多人对于

需要自己加载指定位置的dll和导出接口

极为不爽,微软也很不爽,所以COM库是解决了这个问题的。

1.COM库-定位和加载、导出接口

a).定位和加载

自己加载dll时,需要显示指定对应的dll位置来加载。在COM中简化了这一查找过程,

CoCreateInstance(Ex)

创建COM对象时传入标记guid,此函数内部调用

CoGetClassObjecti

就会自动帮我们找到对应的dll位置和加载。

COM库怎么查找到对应的dll呢,显然它自己是没法知道的,还是得我们告诉它,我们只需要告诉COM一次,它就记住了,这一过程就叫

COM的注册

COM注册实现的方法很简单,将dll的位置写入注册表。如下是一个典型的COM注册表,


其中GUID字符串

{FFFD-…B52}

就是上节说的标明具体组件

对象的CLSID

看看InprocServer32内容如下


InprocServer32意为进程内组件,ThreadingModel意为线程模型,这些之后再讲,现在知道即可。

重点关注默认值是该组件dll的全路径

,对了,当我们传入CLSID指定导出对应对象的接口时,COM库会帮我们查询注册表找到和加载这个dll的。

那么这个注册表是谁写的呢,答案是dll自己。COM要求dll必须导出

注册函数DllUnregisterServer和取消注册函数DllUnregisterServer

,他们会完成对应的注册表写入和删除操作。使用命令

regsvr32 + dll路径

即可调用对应的dll注册接口。

另外,这里ProgID是对应CLSID的一刻可读的组件对象名称,如下


可以使用函数

CLSIDFromProgID完成ProgID到CLSID的转换

使用CLSID的好处在于满足COM标准要求的是

位置透明性

——不论dll在哪里都能发现和加载它,无论是进程内组件还是进程外组件都能使用同一方式来查找。

b.)导出接口


CoGetClassObject

内部找到dll并加载到进程地址空间后,会调用

DllGetClassObject

获得组件对象接口指针。

相比于直接创建组件对象,这里先获得一个工厂对象接口指针

IFactory*

,然后调用

IFactory*

的方法来创建对应的接口。


之所以引入工厂对象而不是直接导出,是因为使用工厂对象可以作中间过渡,如判断不同场景下创建不同的组件对象,


相当于一个中间代理,使得创建过程更加灵活,稍后介绍的包容和聚合都是依赖工厂对象来实现的

全过程引用《COM原理与应用》图如下:


简单来说调用关系如下:


客户

->CoCreateInstance


COM库

->CoGetClassObject->找到dll->DllGetClassObject->得到工厂对象->调用工厂对象的CreateInstance方法创建组件对象


组件dll

->实现DllGetClassObject导出函数,返回工厂对象

CoGetClassObject会帮我们完成根据CLSID找到对应DLL的工作,导出对应的接口并不是直接导出组件对象的接口,而是通过一个工厂对象来完成导出。

DllGetClassObject根据传入的CLSID返回对应的工厂对象,工厂对象再进一步创建组件对象。

c).组件卸载

当我们使用LoadLibrary加载dll时,对应需要使用FreeLibrary完成对应dll卸载。那么在COM库中,dll加载由COM库托管了,怎么才知道何时卸载DLL呢。很简单,DLL导出一个函数

DllCanUnloadNow

,当检测此函数返回值为TRUE的时候就可以FreeLibrary,类似java内存回收机制。

COM库提供CoFreeUnusedLibraries来检测当前进程中所有COM组件,发现某个组件的DllCanUnloadNow函数返回TRUE就调用FreeLibrary函数,

COM库不会主动调用CoFreeUnusedLibraries,推荐客户在空闲时刻调用(如单开一个线程处理)

2.COM库接口的实现

a).注册和撤销注册DLL

对应两个函数实现如下:

//组件注册函数
STDAPI DllRegisterServer(void)
{
	TCHAR szModule[MAX_PATH];
	DWORD dwResult = ::GetModuleFileName(g_hModule, szModule, MAX_PATH);
	if (0 == dwResult)
	{
		return SELFREG_E_CLASS;
	}

	return CToolHelper::RegisterServer( CLSID_EasyComPeople, 
										TEXT("EasyCom.Object"), 
										szModule,
										TEXT("EasyCom Component Description"))
		   ? S_OK : SELFREG_E_CLASS;
}

//组件取消注册函数
STDAPI DllUnregisterServer(void)
{
	return CToolHelper::UnRegisterServer(CLSID_EasyComPeople, TEXT("EasyCom.Object")) 
		   ? S_OK : SELFREG_E_CLASS;
}

注册时写入CLSID、ProgID、InprocServer32信息,反注册时删除对应CLSID项即可。

b).导出接口

//组件信息函数
STDAPI  DllGetClassObject(__in REFCLSID rclsid, __in REFIID riid, LPVOID FAR* ppv)
{
	if (rclsid == CLSID_EasyComPeople)
	{
		CPeopleFactory *pFactory = new CPeopleFactory;

		if (NULL == pFactory)
		{
			return E_FAIL;
		}

		HRESULT hr = pFactory->QueryInterface(riid, ppv);
		return hr;
	} 
	else
	{
		return CLASS_E_CLASSNOTAVAILABLE;
	}
}

这里创建返回指定CLSID的组件的工厂对象。

c).卸载组件

对应实现如下:

//组件卸载函数
STDAPI  DllCanUnloadNow(void)
{
	if(g_LockNumber == 0 && g_EasyComNumber==0)
	{
		return S_OK;
	}
	else
	{
		return S_FALSE;
	}
}

可以看到这里判断g_LockNumber和g_EasyComNumber均为0时才告诉COM库当前组件dll可卸载了。g_LockNumber是工厂对象的锁,在需要持续用到工厂对象创建组件对象时,可以

锁住工厂对象

从而不让dll组件卸载;g_EasyComNumber是

所有组件对象的引用计数和

,当所有组件对象的所有接口都不再被引用时为0,可被卸载。

3.COM对象的实现

a).组件接口定义

// {2F8C8811-1D6D-4e1b-ABD0-686F2641F1C3}
_declspec(selectany) GUID CLSID_EasyComPeople = 
{ 0x2f8c8811, 0x1d6d, 0x4e1b, { 0xab, 0xd0, 0x68, 0x6f, 0x26, 0x41, 0xf1, 0xc3 } };

// {F4D72691-1361-4ece-B550-7C753874B880}
_declspec(selectany) GUID IID_IAge = 
{ 0xf4d72691, 0x1361, 0x4ece, { 0xb5, 0x50, 0x7c, 0x75, 0x38, 0x74, 0xb8, 0x80 } };

// {FDFCA635-07F6-4ac0-9978-3B7BF1A4840C}
_declspec(selectany) GUID IID_IName = 
{ 0xfdfca635, 0x7f6, 0x4ac0, { 0x99, 0x78, 0x3b, 0x7b, 0xf1, 0xa4, 0x84, 0xc } };

class IAge : public IUnknown
{
public:
	virtual HRESULT STDMETHODCALLTYPE PrintAge(int nAge)=0;//必须为虚函数
};

class IName : public IUnknown
{
public:
	virtual HRESULT STDMETHODCALLTYPE PrintName(PWCHAR szName)=0;
};

这里我们实现一个打印个人信息的组件对象,有

两个接口,分别负责打印年龄和打印姓名

b).组件对象声明

class CPeople: IName, IAge
{
public:
	CPeople(void);
	~CPeople();

	//IUnknown
	HRESULT STDMETHODCALLTYPE QueryInterface( _In_ REFIID riid, _Out_ void **ppvObject);
	ULONG STDMETHODCALLTYPE AddRef( void);
	ULONG STDMETHODCALLTYPE Release( void);

	//IName
	HRESULT STDMETHODCALLTYPE PrintName(PWCHAR szName);

	//IAge
	HRESULT STDMETHODCALLTYPE PrintAge(int nAge);

private:
	ULONG m_nRef;
};

c).组件对象实现

分别为构造函数、析构函数、接口查询、声明周期管理和各个接口的实现。其它的基本和上篇文章一致,不同 的是需要注意的是

在构造和析构函数中增加和减少总的组件对象计数

CPeople::CPeople(void)
{
	g_EasyComNumber++;//组件引用计数
	this->m_nRef = 0;
}

CPeople::~CPeople()
{
	g_EasyComNumber--;//组件引用计数
}

//IUnknown
HRESULT STDMETHODCALLTYPE CPeople::QueryInterface( _In_ REFIID riid, _Out_ void **ppvObject )
{
	if (riid == IID_IUnknown)
	{
		*ppvObject = (IAge*)this;
		((IAge*)this)->AddRef();
	}
	else if (riid == IID_IAge)
	{
		*ppvObject = (IAge*)this;
		((IAge*)this)->AddRef();
	}
	else if (riid == IID_IName)
	{
		*ppvObject = (IName*)this;
		((IName*)this)->AddRef();
	}
	else
	{
		*ppvObject = NULL;
		return E_NOINTERFACE;
	}

	return S_OK;
}

ULONG STDMETHODCALLTYPE CPeople::AddRef( void )
{
	m_nRef++;

#ifdef _DEBUG
	cout << __FUNCTION__ << "\tDebugInfo-refcount:" << m_nRef << endl;
#endif

	return (ULONG)m_nRef;
}

ULONG STDMETHODCALLTYPE CPeople::Release( void )
{
	m_nRef--;

#ifdef _DEBUG
	cout << __FUNCTION__ << "\tDebugInfo-refcount:" << m_nRef << endl;
#endif

	if (m_nRef == 0)
	{
#ifdef _DEBUG
	cout << __FUNCTION__ << "\tRefcount=0=>Delete CPeople" << endl;
#endif

		delete this;
		return 0;
	}

	return (ULONG)m_nRef;
}

//IName
HRESULT STDMETHODCALLTYPE CPeople::PrintName( PWCHAR szName )
{
	wcout << L"CPeople->IName: My name is " << szName << endl;
	return S_OK;
}

//IAge
HRESULT STDMETHODCALLTYPE CPeople::PrintAge( int nAge )
{
	cout << "CPeople->IAge: My age is " << nAge << endl;
	return S_OK;
}

d).工厂对象的声明和实现


声明

class CPeopleFactory : public IClassFactory
{
public:
	CPeopleFactory(void);

	//IUnknown
	HRESULT STDMETHODCALLTYPE QueryInterface( _In_ REFIID riid, _Out_ void **ppvObject);
	ULONG STDMETHODCALLTYPE AddRef( void);
	ULONG STDMETHODCALLTYPE Release( void);

	//IClassFactory
	HRESULT STDMETHODCALLTYPE CreateInstance(_In_ IUnknown *pUnkOuter, _In_ REFIID riid, _Out_ void **ppvObject);
	HRESULT STDMETHODCALLTYPE LockServer(_In_ BOOL fLock);

protected:
	ULONG m_nRef;
};


实现

CPeopleFactory::CPeopleFactory(void)
{
	this->m_nRef = 0;
}

//IUnknown
HRESULT STDMETHODCALLTYPE CPeopleFactory::QueryInterface( _In_ REFIID riid, _Out_ void **ppvObject )
{
	if (riid == IID_IUnknown)
	{
		*ppvObject = (IUnknown*)this;
		((IUnknown*)this)->AddRef();
	}
	else if (riid == IID_IClassFactory)
	{
		*ppvObject = (IClassFactory*)this;
		((IClassFactory*)this)->AddRef();
	}
	else
	{
		*ppvObject = NULL;
		return E_NOINTERFACE;
	}

	return S_OK;
}

ULONG STDMETHODCALLTYPE CPeopleFactory::AddRef( void )
{
	m_nRef++;
	return (ULONG)m_nRef;
}

ULONG STDMETHODCALLTYPE CPeopleFactory::Release( void )
{
	m_nRef--;

	if (m_nRef == 0)
	{
		delete this;
		return 0;
	}

	return (ULONG)m_nRef;
}

//IClassFactory
HRESULT STDMETHODCALLTYPE CPeopleFactory::CreateInstance( _In_ IUnknown *pUnkOuter, _In_ REFIID riid, _Out_ void **ppvObject )
{
	CPeople *pObj = NULL;
	HRESULT hr = S_FALSE;

	*ppvObject = NULL;

	//创建组件对象
	pObj = new CPeople;
	if (pObj == NULL)
	{
		return hr;
	}

	//获得非托管第一个接口指针
	hr = pObj->QueryInterface(riid, ppvObject);
	if (S_OK != hr)
	{
		delete pObj;
	}

	return hr;
}

HRESULT STDMETHODCALLTYPE CPeopleFactory::LockServer( _In_ BOOL fLock )
{
	fLock ? g_LockNumber++ : g_LockNumber--;

	return S_OK;
}

可见,其实工厂对象也是一个COM对象,不同的只是他是

给COM库调用的,相当于一个标准对象

,是COM库和实际COM对象的桥梁。可以看工厂对象除了查询接口和声明周期管理外,还包含

CreateInstance和LockServer

函数,前者用于创建实际COM对象,后者传入参数TRUE时锁住组件dll,此时不会卸载。

还有一点是需要注意的,在工厂对象中是不需要操作全局组件对象计数g_EasyComNumber的,因为此时COM库正在加载dll导出接口是一定不会卸载dll的。

4.运行结果


如下调用

int _tmain(int argc, _TCHAR* argv[])
{
	HRESULT hr	 = S_FALSE;
	CLSID easycomCLSID;
	IUnknown *pUnknown	= NULL;
	IAge  *pAge			= NULL;
	IName *pName		= NULL;

	cout << "EasyCom Demo:" << endl;

	//初始化COM库
	if (CoInitialize(NULL) != S_OK)
	{
		cout << "Fail to Initialize COM" << endl;
		return -1;
	}

	//由已知的ProgID找对应CLSID
	hr = ::CLSIDFromProgID(L"EasyCom.Object", &easycomCLSID);
	if (hr != S_OK)
	{
		cout << "Fail to Find CLSID" << endl;
		return -2;
	}

	//创建对应的接口实例
	hr = CoCreateInstance(easycomCLSID, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void **)&pUnknown);
	if (hr != S_OK)
	{
		cout << "Fail to Create Object" << endl;
		return -2;
	}

	//查询接口
	hr = pUnknown->QueryInterface(IID_IName, (void **)&pName);
	if (hr != S_OK)
	{
		cout << "Fail to Create IName" << endl;
		return -2;
	}
	pName->PrintName(L"wenzhou(http://www.jimwen.net)");

	hr = pUnknown->QueryInterface(IID_IAge, (void **)&pAge);
	if (hr != S_OK)
	{
		cout << "Fail to Create IAge" << endl;
		return -2;
	}
	pAge->PrintAge(23);

	//清理工作
	pAge->Release();
	pName->Release();
	pUnknown->Release();

	CoUninitialize();

	return 0;
}

这里为了使COM库正常工作,需要调用CoInitialize初始化COM库,使用完了需要使用CoUninitialize卸载COM库。


结果显示如下


注意这里红框标明的调试信息。

先忽略红框内内容,CoCreateInstance导出接口IID_IUnknown时,引用计数为1,导出接口IID_IName时,引用计数为2,导出接口IID_IAge时,引用计数为3,符合逻辑。

那么这里红框的内容是怎么来的呢?

答案是这是

CoCreateInstance内部调用函数时使用的

,每次调用函数前先AddRef增加引用计数,传给函数,使用完再Release,这样可防止COM对象使用期间被卸载了。

本文完整演示代码

下载链接

原创,转载请注明来自

http://blog.csdn.net/wenzhou1219



版权声明:本文为wenzhou1219原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。