COM编程入门



1. COM组件介绍

COM组件是什么?COM是一种规范,而不是实现。但是当使用C++来实现时,COM组件就是一个C++类,而COM接口就是继承至IUnknown的纯虚类,COM组件就是实现相应COM接口的C++类。
COM规范规定,任何组件或接口都必须从IUnknown接口中继承而来。IUnknown定义了3个重要函数,分别是:

  • QueryInterface
  • AddRef
  • Release

其中,QueryInterface负责组件对象上的接口查询,AddRef用于增加引用计数,Release用于减少引用计数。引用计数是COM中的一个非常重要的概念,它很好地解决了组件对象地生命周期问题,即COM组件何时被销毁,以及谁来销毁地问题。
除了IUnknown接口外,还有另外一个重要地接口,即IClassFactory。COM组件实际上是一个C++类,对于组件地外部使用者来说,这个类名一般不可知,那么如何创建这个类地实例?由谁来创建?COM规范规定,每个组件都必须实现一个与之对应的类工厂(Class Factory)。类工厂也是一个COM组件,它实现了IClassFactory接口。在IClassFactory的接口函数CreateInstance中,才能使用new操作生成一个COM组件类对象实例。
COM组件主要有3种类型:

  • 进程内组件(CLSCTX_INPROC_SERVER)
  • 本地进程组件(CLSCTX_LOCAL_SERVER)
  • 远程组件(CLSCTX_REMOTE_SERVER)

2. COM组件开发

实现一个COM组件,需要完成以下工作:

  1. COM组件接口
  2. COM组件实现类
  3. COM组件创建工厂
  4. COM组件注册与取消注册

本文以一个例子作为说明,COM组件提供了一个SayHello的接口函数,将“Hello COM”打印输出。

2.1 编写COM组件接口

COM组件接口是一个继承IUnknown的纯虚函数:

// IComTest.h
#pragma once

/*
COM 组件接口
*/

#include <Unknwn.h>

// interface id,COM组件接口唯一标识
static const WCHAR* IID_IComTestStr = L"{213D1B15-9BBA-414A-BAB6-CA5B6CEF0006}";
static const GUID IID_IComTest = { 0x213D1B15, 0x9BBA, 0x414A, { 0xBA, 0xB6, 0xCA, 0x5B, 0x6C, 0xEF, 0x00, 0x06 } };

// 纯虚类作为接口
class IComTest :public IUnknown
{
public:
	virtual int _stdcall SayHello() = 0;
};

2.2 编写COM组件实现类

COM组件类是一个实现了相应COM组件接口的C++类,注意:一个COM组件可以同时实现多个COM接口。

// ComTest.h
#pragma once
#include "IComTest.h"
// class id,COM组件唯一标识
static const WCHAR* CLSID_CComTestStr = L"{4046FA83-57F0-4475-9381-8818BFC50DDF}";
static const GUID CLSID_CComTest = { 0x4046FA83, 0x57F0, 0x4475, { 0x93, 0x81, 0x88, 0x18, 0xBF, 0xC5, 0x0D, 0xDF } };

class CComTest :public IComTest
{
public:
	CComTest();
	~CComTest();

	// 实现IUnknown接口
	
	// 查找接口
	// riid : 输入参数,接口id
	// ppvObject : 输出参数,返回相应的接口
	virtual HRESULT _stdcall QueryInterface(const IID &riid, void ** ppvObject);
	// 增加引用计数
	virtual ULONG _stdcall AddRef();
	// 减少引用计数
	virtual ULONG _stdcall Release();

	virtual int _stdcall SayHello();

protected:
	// 引用计数
	ULONG m_RefCount;

	// 全局创建对象个数
	static ULONG g_ObjNum;
};


// ComTest.cpp
#include "ComTest.h"
#include <stdio.h>

ULONG CComTest::g_ObjNum = 0;

CComTest::CComTest()
{
	m_RefCount = 0;
	g_ObjNum++;
}


CComTest::~CComTest()
{
	g_ObjNum--;
}

HRESULT _stdcall CComTest::QueryInterface(const IID &riid, void **ppvObject)
{
	// 通过接口id判断返回的接口类型
	if (IID_IUnknown == riid){
		*ppvObject = this;
		((IUnknown*)(*ppvObject))->AddRef();
	}
	else if (IID_IComTest == riid){
		*ppvObject = (IComTest*)this;
		((IComTest*)(*ppvObject))->AddRef();
	}
	else{
		*ppvObject = NULL;
		return E_NOINTERFACE;
	}
	return S_OK;
}

ULONG _stdcall CComTest::AddRef()
{
	m_RefCount++;
	return m_RefCount;
}

ULONG _stdcall CComTest::Release()
{
	m_RefCount--;
	if (0 == m_RefCount){
		delete this;
		return 0;
	}
	return m_RefCount;
}

int _stdcall CComTest::SayHello()
{
	printf("hello COM\r\n");
	return 666;
}

2.3 编写COM组件对象创建工厂

对于组件地外部使用者来说,这个COM组件的类名一般不可知,那么如何创建这个类地实例?由谁来创建?COM规范规定,每个组件都必须实现一个与之对应的类工厂(Class Factory)。类工厂也是一个COM组件,它实现了IClassFactory接口。在IClassFactory的接口函数CreateInstance中,才能使用new操作生成一个COM组件类对象实例。

// ComTestFactory.h
#pragma once
#include <Unknwn.h>

class CComTestFactory : public IClassFactory
{
public:
	CComTestFactory();
	~CComTestFactory();

	// 实现IUnknown接口  
	virtual HRESULT _stdcall QueryInterface(const IID& riid, void** ppvObject);
	virtual ULONG _stdcall AddRef();
	virtual ULONG _stdcall Release();

	// 实现IClassFactory接口  
	virtual HRESULT _stdcall CreateInstance(IUnknown *pUnkOuter, const IID& riid, void **ppvObject);
	virtual HRESULT _stdcall LockServer(BOOL fLock);

protected:
	ULONG m_RefCount;

	static ULONG g_ObjNum;
};

// ComTestFactory.cpp
#include "ComTestFactory.h"
#include "ComTest.h"

ULONG CComTestFactory::g_ObjNum = 0;

CComTestFactory::CComTestFactory()
{
	m_RefCount = 0;
	g_ObjNum++;
}

CComTestFactory::~CComTestFactory()
{
	g_ObjNum--;
}

// 查询指定接口
HRESULT _stdcall CComTestFactory::QueryInterface(const IID &riid, void **ppvObject)
{
	if (IID_IUnknown == riid){
		*ppvObject = (IUnknown*)this;
		((IUnknown*)(*ppvObject))->AddRef();
	}
	else if (IID_IClassFactory == riid){
		*ppvObject = (IClassFactory*)this;
		((IClassFactory*)(*ppvObject))->AddRef();
	}
	else{
		*ppvObject = NULL;
		return E_NOINTERFACE;
	}
	return S_OK;
}

ULONG _stdcall CComTestFactory::AddRef()
{
	m_RefCount++;
	return m_RefCount;
}

ULONG _stdcall CComTestFactory::Release()
{
	m_RefCount--;
	if (0 == m_RefCount){
		delete this;
		return 0;
	}
	return m_RefCount;
}

// 创建COM对象,并返回指定接口
HRESULT _stdcall CComTestFactory::CreateInstance(IUnknown *pUnkOuter, const IID &riid, void **ppvObject)
{
	if (NULL != pUnkOuter){
		return CLASS_E_NOAGGREGATION;
	}
	HRESULT hr = E_OUTOFMEMORY;
	//ComClass::Init();
	CComTest* pObj = new CComTest();
	if (NULL == pObj){
		return hr;
	}

	hr = pObj->QueryInterface(riid, ppvObject);
	if (S_OK != hr){
		delete pObj;
	}
	return hr;
}

HRESULT _stdcall CComTestFactory::LockServer(BOOL fLock)
{
	return NOERROR;
}

2.4 COM组件的注册

COM组件需要使用regsvr32工具注册到系统才能被调用,然而COM组件是如何被regsvr32注册的?一个典型的自注册COM组件需要提供4个必需的导出函数:

  • DllGetClassObject:用于获得类工厂指针
  • DllCanUnloadNow:系统空闲时会调用这个函数,以确定是否可以卸载COM组件
  • DllRegisterServer:将COM组件注册到注册表中
  • DllUnregisterServer:删除注册表中的COM组件的注册信息

DLL还有一个可选的入口函数DllMain,可用于初始化和释放全局变量

  • DllMain:DLL的入口函数,在LoadLibrary和FreeLibrary时都会调用
// ComTestExport.h
#include <windows.h> 

extern "C" HRESULT _stdcall DllRegisterServer();
extern "C" HRESULT _stdcall DllUnregisterServer();
extern "C" HRESULT _stdcall DllCanUnloadNow();
extern "C" HRESULT _stdcall DllGetClassObject(__in REFCLSID rclsid, __in REFIID riid, LPVOID FAR* ppv);
// ComTestExport.cpp
#include "ComTestExport.h"
#include "ComTestFactory.h"
#include "ComTest.h"

#include <iostream>

HMODULE g_hModule;  //dll进程实例句柄  
ULONG g_num;        //组件中ComTest对象的个数,用于判断是否可以卸载本组建,如值为0则可以卸载  

int myReg(LPCWSTR lpPath)   //将本组件的信息写入注册表,包括CLSID、所在路径lpPath、ProgID  
{
	HKEY thk, tclsidk;

	//打开键HKEY_CLASSES_ROOT\CLSID,创建新键为ComTest的CLSID,  
	//在该键下创建键InprocServer32,并将本组件(dll)所在路径lpPath写为该键的默认值  
	if (ERROR_SUCCESS == RegOpenKey(HKEY_CLASSES_ROOT, L"CLSID", &thk)){

		printf("RegOpenKey ok\r\n");

		if (ERROR_SUCCESS == RegCreateKey(thk, CLSID_CComTestStr, &tclsidk)){

			wprintf(L"RegCreateKey %s ok\r\n", CLSID_CComTestStr);

			HKEY tinps32k, tprogidk;
			if (ERROR_SUCCESS == RegCreateKey(tclsidk, L"InprocServer32", &tinps32k)){

				printf("RegCreateKey InprocServer32 ok\r\n");

				if (ERROR_SUCCESS == RegSetValue(tinps32k, NULL, REG_SZ, lpPath, wcslen(lpPath) * 2)){
				}
				RegCloseKey(tinps32k);
			}
			RegCloseKey(tclsidk);
		}
		RegCloseKey(thk);
	}
	//在键HKEY_CLASSES_ROOT下创建新键为COMCTL.CComTest,  
	//在该键下创建子键,并将CCompTest的CLSID写为该键的默认值  
	if (ERROR_SUCCESS == RegCreateKey(HKEY_CLASSES_ROOT, L"COMCTL.CComTest", &thk)){
		if (ERROR_SUCCESS == RegCreateKey(thk, L"CLSID", &tclsidk)){
			if (ERROR_SUCCESS == RegSetValue(tclsidk,
				NULL,
				REG_SZ,
				CLSID_CComTestStr,
				wcslen(CLSID_CComTestStr) * 2)){
			}
		}
	}
	//这样的话一个客户端程序如果想要使用本组件,首先可以以COMCTL.CComTest为参数调用CLSIDFromProgID函数  
	//来获取CCompTest的CLSID,再以这个CLSID为参数调用CoCreateInstance创建COM对象  
	return 0;
}

extern "C" HRESULT _stdcall DllRegisterServer()
{
	WCHAR szModule[1024];
	//获取本组件(dll)所在路径  
	DWORD dwResult = GetModuleFileName(g_hModule, szModule, 1024); 
	if (0 == dwResult){
		return -1;
	}
	MessageBox(NULL, szModule, L"", MB_OK);
	//将路径等信息写入注册表  
	myReg(szModule);
	return 0;
}

int myDelKey(HKEY hk, LPCWSTR lp)
{
	if (ERROR_SUCCESS == RegDeleteKey(hk, lp)){
	}
	return 0;
}

//删除注册时写入注册表的信息  
int myDel() 
{
	HKEY thk;
	if (ERROR_SUCCESS == RegOpenKey(HKEY_CLASSES_ROOT, L"CLSID", &thk)){
		myDelKey(thk, L"{4046FA83-57F0-4475-9381-8818BFC50DDF}\\InprocServer32");
		myDelKey(thk, CLSID_CComTestStr);

		RegCloseKey(thk);
	}
	if (ERROR_SUCCESS == RegOpenKey(HKEY_CLASSES_ROOT, L"COMCTL.CComTest", &thk)){
		myDelKey(thk, L"CLSID");
	}
	myDelKey(HKEY_CLASSES_ROOT, L"COMCTL.CComTest");
	return 0;
}

extern "C" HRESULT _stdcall DllUnregisterServer()
{
	//删除注册时写入注册表的信息  
	myDel();
	return 0;
}

// 用于判断是否可以卸载本组建, 由CoFreeUnusedLibraries函数调用  
extern "C" HRESULT _stdcall DllCanUnloadNow()
{
	//如果对象个数为0,则可以卸载  
	if (0 == g_num){
		return S_OK;
	}
	else{
		return S_FALSE;
	}
}

//用于创建类厂并返回所需接口,由CoGetClassObject函数调用  
extern "C" HRESULT _stdcall DllGetClassObject(__in REFCLSID rclsid, __in REFIID riid, LPVOID FAR* ppv)
{
	LPOLESTR szCLSID;
	StringFromCLSID(rclsid, &szCLSID);     //将其转化为字符串形式用来输出  
	wprintf(L"rclsid CLSID \"%s\"\n", szCLSID);

	szCLSID;
	StringFromCLSID(riid, &szCLSID);     //将其转化为字符串形式用来输出  
	wprintf(L"riid CLSID \"%s\"\n", szCLSID);

	if (CLSID_CComTest == rclsid){
		CComTestFactory* pFactory = new CComTestFactory();//创建类厂对象  
		if (NULL == pFactory){
			return E_OUTOFMEMORY;
		}
		HRESULT result = pFactory->QueryInterface(riid, ppv);//获取所需接口  
		return result;
	}
	else{
		return CLASS_E_CLASSNOTAVAILABLE;
	}
}

// 提供DLL入口;对于动态链接库,DllMain是一个可选的入口函数,在COM组件中是必须有的
BOOL APIENTRY DllMain(HMODULE hModule,
	DWORD  ul_reason_for_call,
	LPVOID lpReserved
	)
{
	//获取进程实例句柄,用于获取本组件(dll)路径  
	g_hModule = hModule;
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
		break;
	}
	return TRUE;
}

导出文件(Source.def):

LIBRARY "ComTest_Server"
EXPORTS
DllCanUnloadNow
DllGetClassObject
DllUnregisterServer
DllRegisterServer

生成完后,使用regsvr32注册到系统中:

> regsvr32 ComTest_Server.dll

3. COM组件使用

COM组件的使用包括:

  1. 创建COM组件实例对象
  2. 获取组件对象上的接口以及调用接口方法
  3. 管理组件对象(需熟悉COM的引用计数机制)

下面的代码是最一般的步骤:

CoInitialize(NULL);	// COM库初始化
// ...
IUnknow *pUnk = NULL;
IObject *pObj = NULL;
// 创建组件对象,CLSID_XXX为COM组件类的GUID(class id),返回默认IID_IUnknown接口
HRESULT hr = CoCreateInstance(CLSID_XXX,NULL,CLSCTX_INPROC_SERVER,NULL,IID_IUnknown,(void **)&pUnk);
if(S_OK == hr)
{
	// 获取接口,IID_XXX为组件接口的GUID(interface id)
	hr = pUnk->QueryInterface(IID_XXX,(void **)&pObj);
	if(S_OK == hr)
	{
		// 调用接口方法
		pObj->DoXXX();
	}
	// 释放组件对象
	pUnk->Release();
}
//...
// 释放COM库
CoUninitialize();

下面我们编写一个客户端,调用之前写的COM组件服务:

#include "IComTest.h"
#include "ComTest.h"
#include <stdio.h>

int main()
{
	// 初始化COM库
	CoInitialize(NULL);

	IComTest *pComTest = NULL;
	HRESULT hResult;

	// 创建进程内COM组件,返回指定接口
	hResult = CoCreateInstance(CLSID_CComTest, NULL, CLSCTX_INPROC_SERVER, IID_IComTest, (void **)&pComTest);
	if (S_OK == hResult)
	{
		// 调用接口方法
		printf("%d\r\n", pComTest->SayHello());
		// 释放组件
		pComTest->Release();
	}
	// 释放COM库
	CoUninitialize();

	return 0;
}

上面的例子和一般步骤不一致,少了QueryInterface,是因为默认返回的就是指定的接口,下面按一般步骤再实现一次:

#include "IComTest.h"
#include "ComTest.h"
#include <stdio.h>

int main()
{
	// 初始化COM库
	CoInitialize(NULL);

	IUnknown *pUnk = NULL;
	IComTest *pComTest = NULL;
	HRESULT hResult;

	// 创建COM组件,返回默认接口
	hResult = CoCreateInstance(CLSID_CComTest, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void **)&pUnk);
	if (S_OK == hResult)
	{
		// 查询接口
		hResult = pUnk->QueryInterface(IID_IComTest, (void **)&pComTest);
		if (S_OK == hResult)
		{
			// 调用接口方法
			printf("%d\r\n", pComTest->SayHello());
		}
		// 释放组件
		pComTest->Release();
	}

	// 释放COM库
	CoUninitialize();
	return 0;
}

是直接创建COM组件并获取接口,还是先创建COM组件得到默认接口再查询其他的接口,需要具体问题具体分析。

4.0 COM组件运行机制

一个COM组件从编写到最终可以被调用,整个运行流程是怎样的?或者我们再考虑简单一点,COM组件是如何被调用的?

参考:C++ COM组件编写初探

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页