COM编程基础示例

2022/08/20

Kkem Chen

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

  • COM组件接口
  • COM组件实现类
  • COM组件创建工厂
  • COM组件注册与反注册

本文以一个例子作为说明,COM组件实现了一个矩形类,提供了两个接口,IAbstructShape和IDisplay。 IAbstructShape接口提供了shapeType接口函数,返回矩形类的类型。IDisplay接口提供了draw接口函数,将在屏幕上绘制出矩形。

一、COM组件的实现

代码目录结构如下

|
|–include
    |–IAbstructShape.h
    |–IDisplay.h
|–CRectangle.h
|–CRectangle.cpp
|–CRectangleFactory.h
|–CRectangleFactory.cpp
|–CRectangleExport.h
|–CRectangleExport.cpp
|–CRectangle.def

1、IAbstructShape接口

COM的所有接口都应该继承自IUnknown,因此IAbstructShape接口也是如此。

// IAbstructShape.h
#pragma once
#include <Unknwn.h>
// interface id,COM组件接口唯一标识
// {455C1480-C5C7-4EEB-B88F-CC91D2606CE5}
static const WCHAR* IID_IAbstructShapeStr = L"{455C1480-C5C7-4EEB-B88F-CC91D2606CE5}";
static const GUID IID_IAbstructShape =
{ 0x455c1480, 0xc5c7, 0x4eeb, { 0xb8, 0x8f, 0xcc, 0x91, 0xd2, 0x60, 0x6c, 0xe5 } };
 
class IAbstructShape : public IUnknown
{
public:
	virtual int _stdcall shapeType() = 0;
};

对于COM的每一个接口,都有惟一的GUID标志。GUID的生成可以使用VS自带的GUID生成工具。

 2、IDisplay接口

// IDisplay.h
#pragma once
#include <Unknwn.h>
// interface id,COM组件接口唯一标识
// {E3780F3F-875A-4A77-B66E-DAC8119938AC}
static const WCHAR* IID_IDisplayStr = L"{E3780F3F-875A-4A77-B66E-DAC8119938AC}";
static const GUID IID_IDisplay =
{ 0xe3780f3f, 0x875a, 0x4a77, { 0xb6, 0x6e, 0xda, 0xc8, 0x11, 0x99, 0x38, 0xac } };
 
class IDisplay : public IUnknown
{
public:
	virtual int _stdcall draw() = 0;
};

3、COM组件类 – CRectangle类实现

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

CRectangle就实现了IAbstructShape和IDisplay两个接口,当然,IUnknown接口是每一个COM组件类必须要实现的。

// CRectangle.h
#pragma once
#include "include/IAbstructShape.h"
#include "include/IDisplay.h"
 
// class id,COM组件唯一标识
// {C1271670-DD25-4365-B0A6-3FABBF4F0177}
static const WCHAR* CLSID_CRectanleStr = L"{C1271670-DD25-4365-B0A6-3FABBF4F0177}";
static const GUID CLSID_CRectanle =
{ 0xc1271670, 0xdd25, 0x4365, { 0xb0, 0xa6, 0x3f, 0xab, 0xbf, 0x4f, 0x1, 0x77 } };
 
class CRectangle :
	public IAbstructShape, 
	public IDisplay
{
public:
	CRectangle();
	~CRectangle();
 
	// 实现IUnknown接口
	// riid : 输入参数,接口id
	// ppvObject : 输出参数,返回相应的接口
	virtual HRESULT _stdcall QueryInterface(const IID &riid, void ** ppvObject) override;
	virtual ULONG _stdcall AddRef() override;
	virtual ULONG _stdcall Release() override;
 
	// 实现IAbstructShape接口
	virtual int _stdcall shapeType() override;
 
	// 实现IDisplay接口
	virtual int _stdcall draw() override;
 
protected:
	//引用计数
	ULONG m_RefCount;
	//全局创建对象个数
	static ULONG g_ObjNum;
};
// CRectangle.cpp
#include <stdio.h>
#include "CRectangle.h"
 
ULONG CRectangle::g_ObjNum = 0;
 
CRectangle::CRectangle()
{
	m_RefCount = 0;
	g_ObjNum++;//对象个数+1
}
 
CRectangle::~CRectangle()
{
	g_ObjNum--;//对象个数-1
}
 
HRESULT _stdcall CRectangle::QueryInterface(const IID &riid, void **ppvObject)
{
	// 通过接口id判断返回的接口类型
	if (IID_IUnknown == riid)
	{
		*ppvObject = this;
		((IUnknown*)(*ppvObject))->AddRef();
	}
	else if (IID_IAbstructShape == riid)
	{
		*ppvObject = (IAbstructShape*)this;
		((IAbstructShape*)(*ppvObject))->AddRef();
	}
	else if (IID_IDisplay == riid)
	{
		*ppvObject = (IDisplay*)this;
		((IDisplay*)(*ppvObject))->AddRef();
	}
	else
	{
		*ppvObject = NULL;
		return E_NOINTERFACE;
	}
	return S_OK;
}
 
ULONG _stdcall CRectangle::AddRef()
{
	m_RefCount++;
	return m_RefCount;
}
 
ULONG _stdcall CRectangle::Release()
{
	m_RefCount--;
	if (0 == m_RefCount)
	{
		delete this;
		return 0;
	}
	return m_RefCount;
}
 
int _stdcall CRectangle::shapeType()
{
	return 0x1001;
}
 
int _stdcall CRectangle::draw()
{
	printf("begin draw a Rectangle...\r\n\r\n");
	printf("-----------------\r\n");
	printf("|                |\r\n");
	printf("|                |\r\n");
	printf("-----------------\r\n\r\n");
	printf("end draw! Successful!!!\r\n");
	return 0;
}

4、 COM组件创建工厂

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

// CRectangleFactory.h
#pragma once
#include <Unknwn.h>
 
class CRectangleFactory : public IClassFactory
{
public:
	CRectangleFactory();
	~CRectangleFactory();
 
	// 实现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;//全局创建对象个数
};
// CRectangleFactory.cpp
#include "CRectangleFactory.h"
#include "CRectangle.h"
 
ULONG CRectangleFactory::g_ObjNum = 0;
 
CRectangleFactory::CRectangleFactory()
{
	m_RefCount = 0;
	g_ObjNum++;
}
 
CRectangleFactory::~CRectangleFactory()
{
	g_ObjNum--;
}
 
// 查询指定接口
HRESULT _stdcall CRectangleFactory::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 CRectangleFactory::AddRef()
{
	m_RefCount++;
	return m_RefCount;
}
 
ULONG _stdcall CRectangleFactory::Release()
{
	m_RefCount--;
	if (0 == m_RefCount)
	{
		delete this;
		return 0;
	}
	return m_RefCount;
}
 
// 创建COM对象,并返回指定接口
HRESULT _stdcall CRectangleFactory::CreateInstance(IUnknown *pUnkOuter, const IID &riid, void **ppvObject)
{
	if (NULL != pUnkOuter)
	{
		return CLASS_E_NOAGGREGATION;
	}
	HRESULT hr = E_OUTOFMEMORY;
	//ComClass::Init();
	CRectangle* pObj = new CRectangle();
	if (NULL == pObj) 
	{
		return hr;
	}
	hr = pObj->QueryInterface(riid, ppvObject);
	if (S_OK != hr) 
	{
		delete pObj;
	}
	return hr;
}
 
HRESULT _stdcall CRectangleFactory::LockServer(BOOL fLock)
{
	return NOERROR;
}

5、 COM组件的注册

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

  • DllGetClassObject:用于获得类工厂指针
  • DllCanUnloadNow:系统空闲时会调用这个函数,以确定是否可以卸载COM组件
  • DllRegisterServer:将COM组件注册到注册表中
  • DllUnregisterServer:删除注册表中COM组件的注册信息
// CRectangleExport.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);
// CRectangleExport.cpp
#include <stdio.h>
#include "CRectangle.h"
#include "CRectangleFactory.h"
#include "CRectangleExport.h"
 
HMODULE g_hModule;  //dll进程实例句柄
ULONG g_num;        //组件中ComTest对象的个数,用于判断是否可以卸载本组建,如值为0则可以卸载
 
int RegCRectangle(LPCWSTR lpPath)   //将本组件的信息写入注册表,包括CLSID、所在路径lpPath、ProgID
{
	int iRet = 1;
	HKEY thk;
	HKEY tclsidk;
	//打开键HKEY_CLASSES_ROOT\CLSID,创建新键为CRectangle的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_CRectanleStr, &tclsidk)) 
		{
			wprintf(L"RegCreateKey %s ok\r\n", CLSID_CRectanleStr);
 
			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)) 
				{
					iRet = 0;
				}
				RegCloseKey(tinps32k);
			}
			RegCloseKey(tclsidk);
		}
		RegCloseKey(thk);
	}
 
	//在键HKEY_CLASSES_ROOT下创建新键为COMCTL.CRectangle,
	//在该键下创建子键,并将CRectangle的CLSID写为该键的默认值
	if (ERROR_SUCCESS == RegCreateKey(HKEY_CLASSES_ROOT, L"COMCTL.CRectangle", &thk))
	{
		if (ERROR_SUCCESS == RegCreateKey(thk, L"CLSID", &tclsidk)) 
		{
			if (ERROR_SUCCESS == RegSetValue(tclsidk,
				NULL,
				REG_SZ,
				CLSID_CRectanleStr,
				wcslen(CLSID_CRectanleStr) * 2)) 
			{
				printf("RegCreateKey COMCTL.CRectangle ok\r\n");
			}
		}
	}
	//这样的话一个客户端程序如果想要使用本组件,首先可以以COMCTL.CRectangle为参数调用CLSIDFromProgID函数
	//来获取CRectangle的CLSID,再以这个CLSID为参数调用CoCreateInstance创建COM对象
	return iRet;
}
 
extern "C" HRESULT _stdcall DllRegisterServer()
{
	WCHAR szModule[1024];
	//获取本组件(dll)所在路径
	DWORD dwResult = GetModuleFileName(g_hModule, szModule, 1024);
	if (0 == dwResult) 
	{
		printf("Get file name failed!!!\r\n");
		return -1;
	}
 
	//将路径等信息写入注册表
	if (0 == RegCRectangle(szModule))
		MessageBox(NULL, szModule, L"CRectangle", MB_OK);
	else
		MessageBox(NULL, szModule, L"CRectangle", MB_OK | MB_ICONERROR);
 
	return 0;
}
 
int DelRegKey(HKEY hk, LPCWSTR lp)
{
	if (ERROR_SUCCESS == RegDeleteKey(hk, lp)) 
	{
		return 0;
	}
	return 1;
}
 
//删除注册时写入注册表的信息
int UnRegCRectangle()
{
	HKEY thk;
	if (ERROR_SUCCESS == RegOpenKey(HKEY_CLASSES_ROOT, L"CLSID", &thk))
	{
		DelRegKey(thk, L"{C1271670-DD25-4365-B0A6-3FABBF4F0177}\\InprocServer32");
		DelRegKey(thk, CLSID_CRectanleStr);
 
		RegCloseKey(thk);
	}
	if (ERROR_SUCCESS == RegOpenKey(HKEY_CLASSES_ROOT, L"COMCTL.CRectangle", &thk)) 
	{
		DelRegKey(thk, L"CLSID");
	}
	DelRegKey(HKEY_CLASSES_ROOT, L"COMCTL.CRectangle");
	return 0;
}
 
extern "C" HRESULT _stdcall DllUnregisterServer()
{
	//删除注册时写入注册表的信息
	UnRegCRectangle();
	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_CRectanle == rclsid)
	{
		CRectangleFactory* pFactory = new CRectangleFactory();//创建类厂对象
		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;
}

 定义一个导出文件(CRectangle.def),方便导出函数。

LIBRARY "CRectangle"EXPORTSDllCanUnloadNowDllGetClassObjectDllUnregisterServerDllRegisterServer

在VS中,还需要在linker中将导出文件名加上,否则不会导出任何函数 

 编译成功后,在Debug/Release目录下,会生成CRectangle.dll文件,这个就是COM组件。使用regsvr32命令注册到到系统中。在命令行中输入以下命令。

会两次弹框,第一个弹框是CRectangleExport中自己弹框提示的,第二个是regsvr32命令弹框的。

 查看注册表。可以看到正确生成相关信息。

在Root目录下,生成了COMCTL.CRectangle节点,其子节点CLSID的值就是CRectangle的classid。 

 在Root/wow6432node/CLSID下面,生成了对应的GUID节点,其子节点InprocServer32的值就是COM组件在磁盘上的目录路径。

 注意:若是32位系统,就早Root/CLSID下面,若是64位系统,就在Root/wow6432node/CLSID路径下。

至此,CRectangle这个COM组件就完成了,现在可以使用了。

二、COM组件的使用

COM组件的使用包括:

  1. 如何创建COM组件
  2. 如何得到组件对象上的接口以及如何调用接口方法
  3. 如何管理组件对象(需熟悉COM的引用计数机制)
#include <stdio.h>
#include "CRectangle.h"
 
int main()
{
	// 初始化COM库
	CoInitialize(NULL);
 
	IUnknown *pUnk = NULL;
	// 创建COM组件,返回默认接口
	HRESULT hResult = CoCreateInstance(CLSID_CRectanle, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void **)&pUnk);
	if (S_OK == hResult)
	{
		// 查询IAbstructShape接口
		IAbstructShape *pAbstructShape = NULL;
		hResult = pUnk->QueryInterface(IID_IAbstructShape, (void **)&pAbstructShape);
		if (S_OK == hResult)
		{
			// 调用接口方法
			printf("shapetype:%d\r\n", pAbstructShape->shapeType());
		}
		// 释放组件
		pAbstructShape->Release();
 
		// 查询IDisplay接口
		IDisplay *pDisplay = NULL;
		hResult = pUnk->QueryInterface(IID_IDisplay, (void **)&pDisplay);
		if (S_OK == hResult)
		{
			// 调用接口方法
			pDisplay->draw();
		}
		// 释放组件
		pDisplay->Release();
	}
 
	// 释放COM库
	CoUninitialize();
	return 0;
}

运行效果:


参考链接:
COM编程(2)– 第一个COM组件
UUID、CLSID、IID的获取


comments powered by Disqus