2.实现一个最简单的COM

COM教程 专栏收录该内容
23 篇文章 34 订阅

上一篇说了怎么设计一个伪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

  • 2
    点赞
  • 5
    评论
  • 3
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 撸撸猫 设计师:马嘣嘣 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值