2.实现一个最简单的COM

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

先提一个问题,上一节我们实现的伪COM有没有什么令你不爽的地方?对的,相信很多人对于需要自己加载dll和导出接口极为不爽,微软也很不爽,所以COM库是解决了这个问题的。

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

a).加载

先看看COM库怎么解决加载dll的问题。要加载dll,首先要找到它,上篇文章我们是假定dll在当前调用程序目录中的,但是COM标准要求的是具有位置透明性——不论dll在哪里都能发现和加载它。

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的转换。

b).导出接口

找到了dll,接下来就是导出对应接口,这里不像伪COM那样直接导出对应接口,全过程引用《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,当COM库检测此函数返回值为TRUE的时候就FreeLibrary,类似java内存回收机制。

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

时间: 2024-10-18 20:08:01

2.实现一个最简单的COM的相关文章

一个极为简单的requirejs实现

require和 sea的源码分析,我之前的博客有写过, 今天我想分享的是一个很简单的核心代码(不带注释和空行大概60行), 没有容错判断. require.js require函数实现用一句话概括: 依次加载require的模块,然后监测script的onload事件,判断所有模块加载成功,执行require的callback, 如果只带一个参数且不是数组,就是加载成功后return 模块. 1 //标记已经加载成功的个数 2 var REQ_TOTAL = 0; 3 //模块导出 4 win

[编译] 1、第一个makefile简单例子

前言 本篇用一个最简单的例子引入makefile,教你编写第一个makefile 正文 在Download/aa文件夹下有a.c和makefile文件 1 [email protected]:~/Downloads/aa$ ls 2 a.c makefile 其中a.c为: 1 #include<stdio.h> 2 int main() 3 { 4 int i,j; 5 for(i=0;i<10;i++) 6 { 7 for(j=2*i+1;j>0;j--) 8 { 9 prin

打造支持apk下载和html5缓存的 IIS(配合一个超简单的android APP使用)具体解释

为什么要做这个看起来不靠谱的东西呢? 由于刚学android开发,还不能非常好的熟练控制android界面的编辑和操作,所以我的一个急着要的运用就改为html5版本号了,反正这个运用也是须要从server获取大量数据来展示在手机上面的,也就是说:必须联网,才干正常工作,于是想了一下,反正都要联网获取数据,为什么不直接用我相对熟悉一点的 html来做这个运用呢?省的花费不够用的时间去学习android界面的控制,于是就简单了:用蹩脚的手段做了一个android程序的启动欢迎界面,内页就是一个全屏的

基于php的一个最简单的memcache的分布式算法

首先,核心函数是这个 function mHash($key){ $md=substr(md5($key),0,8); $seed=31; $hash=0; for($i=0;$i<8;$i++){ $hash=$hash*$seed+ord($md5{$i}); } return $hash & 0x7FFFFFFF; } class HashServer{ private $serverlist; private $issorted=false; function addServer($

一个最简单的Socket通信例子

所谓socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄.应用程序通常通过"套接字"向网络发出请求或者应答网络请求.  Socket和ServerSocket类库位于java.net包中.ServerSocket用于服务器端,Socket是建立网络连接时使用的.在连接成功时,应用程序两端都会产生一个Socket实例,操作这个实例,完成所需的会话.对于一个网络连接来说,套接字是平等的,并没有差别,不因为在服务器端或在客户端而产生不同级别.不管是

[.NET] 打造一个很简单的文档转换器 - 使用组件 Spire.Office

打造一个很简单的文档转换器 - 使用组件 Spire.Office 目录 Spire.Office 介绍 库引用 界面预览 代码片段 Spire.Office 介绍 关于 Spire.Office,它是一个专门为开发人员创建,读取,写入设计的库,转换和从打印 word 文档文件.作为一个独立的 .NET组件,它不需要在机器上安装微软的 Word 等办公软件.然而,它可以将微软的“文档创建功能”集成到任何开发人员的网络应用程序中.它是一个可靠的 MS Word 的API,可以执行许多Word文档处

一个超简单的马里奥游戏

理论是需要通过实践来检验的,学了这么多,于是我就试了试采用面向对象的编程思想实现了一个超级简单的马里奥游戏,游戏感觉特傻! 准备素材(图片mario.jpg): 分析: 如何通过按钮控制图片的位置 设计相关的对象 要求:Mario碰到边界给一个提示.(其实还有一个要求就是Mario可以去找另一个物体,没有实现.) 以下为源码: 超级马里奥游戏.html: <!DOCTYPE html> <html> <head> <meta charset="UTF-8

Hello,Cardboard!!-如何开发一个最简单的Cardboard虚拟现实应用(一)

温馨提醒,本篇为介绍篇,如果只想看如何开发的具体步骤请参看<Hello,Cardboard!!-如何开发一个最简单的Cardboard虚拟现实应用(三)> 前述:恕我啰嗦一下,主要照顾对cardboard不太了解的朋在,Cardboard是由Google公司的两位巴黎办公室的员工利用业余时间创作出来的作品,它最大的特点就是将原来人们以为高大上的虚拟现实技术以廉价的方式带进了公众的视野,到目前为止,google已推出了改良版的cardboard 2代盒子,相比1代,2代改善了成像,增加了视野范围

JNI编程(一) —— 编写一个最简单的JNI程序

来自:http://chnic.iteye.com/blog/198745 忙了好一段时间,总算得了几天的空闲.貌似很久没更新blog了,实在罪过.其实之前一直想把JNI的相关东西整理一下的,就从今天开始吧.Here we go. JNI其实是Java Native Interface的简称,也就是java本地接口.它提供了若干的API实现了和Java和其他语言的通信(主要是C&C++).也许不少人觉得Java已经足够强大,为什么要需要JNI这种东西呢?我们知道Java是一种平台无关性的语言,平

用Python写一个最简单的网络爬虫

什么是网络爬虫?这是百度百科的解释: 网络爬虫(又被称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动的抓取万维网信息的程序或者脚本.另外一些不常使用的名字还有蚂蚁,自动索引,模拟程序或者蠕虫. 爬虫可以做什么?爬虫可以帮助我们在茫茫互联网中爬取我们需要的特定数据,这个特定数据可以是任何想获得的数据. 爬虫是一个让人热血的话题,因为当你在写爬虫的时候,你会感觉到自己是在做一件很NB的事,而每当写出一个爬虫,就会在此基础上不断尝试写出更NB的爬虫,有