在VS环境下使用C++编写你的第一个COM

本文详细介绍了如何在VisualStudio2022环境下使用纯C++创建并实现一个COM组件,包括设置项目为DLL、声明接口、实现接口、创建工厂类以及实现自注册的过程。通过示例代码展示了COM组件的关键步骤,帮助初学者理解和实践COM编程。

在VS(2022)环境下使用C++编写你的第一个COM

作者:王震宇
最后更改时间:2023年2月17日

  本人入职公司实习第一课就是学习COM,国内网络上有关COM的博客资料比较少,本人对此加以补充。
  本篇博客仅针对学习并初步了解COM以及其基本结构的同学,在本博客的基础上建立起自己的第一个COM并加以学习

从项目创建开始

  本文使用纯C++编写COM,所以我们需要创建一个新的空项目。将项目名称命名为ComTest.
  此时的项目默认生成的将会是一个.exe文件,但是我们需要的是一个dll文件,所以我们需要再稍作更改。
项目->属性->ComTest属性->配置属性->常规
  在其中右侧将配置程序将应用程序(.exe)改为动态库(.dll)
  最后将Debug改为Release。
  至此,项目就创建完成了!

声明你的接口

  创建IComTest.h文件,我们需要在这个文件中完成GUID的定义,这一点至关重要,关系着客户端对COM的使用。sayHello()函数即为我们自己定义的接口,我们将会在客户端调用此接口。其类型为HRESULT,如果需要返回值,需要使用指针。例如:
virtual HRESULT _stdcall squre(ULONG num, ULONG* ret);
  通过传入的ret指针,即可获得输出

// 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 HRESULT _stdcall SayHello() = 0;
};

接口的实现

  添加一个类,命名为ComTest,这个类将是我们主要着重编写的类。
  在ComTest.h文件中,需要定义CLSID,这也是至关重要的。

// 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 HRESULT _stdcall SayHello();

protected:
	// 引用计数
	ULONG m_RefCount;

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

  在ComTest.cpp文件中,需要完成CComTest类的实现。

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

ULONG CComTest::g_ObjNum = 0;

CComTest::CComTest()
{
	//CComTest实例化,将引用次数置0
	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--;
	//若引用次数为0,则可以释放对象
	if (0 == m_RefCount) {
		delete this;
		return 0;
	}
	return m_RefCount;
}

//实现自己声明的接口SayHello
HRESULT _stdcall CComTest::SayHello()
{
	printf("hello \n");
	return S_OK;
}

工厂类的实现

  创建ComTestFactory类
  ComTestFactory类需要继承IClassFactory类,与IComTest类似,但需要实现CreateInstance()与LockServer()函数。
  CreateInstance()负责创建新的CComTest对象
  LockServer()负责控制类厂的生命周期
ComTestFactory.h文件:

// 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文件:

// 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;
	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;
}

实现自注册

  DllRegisterServer()实现自注册
  DllUnregisterServer()实现反注册
  DllCanUnloadNow()确定是否可以卸载组件
  DllGetClassObject(__in REFCLSID rclsid, __in REFIID riid, LPVOID FAR * ppv)负责创建新的类厂

// 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);

  实现DllRegisterServer()过程中至关重要的一项是确定好ProgID,这决定着客户端能否在注册表中找到接口的CLSID,本例的ProgID为:COMCTL.CComTest。
  ProgID命名约定: < Program >.< Component >.< Version >

// 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;
}

模块定义文件

  这是项目中最后一个文件啦!
  右键ComTest项目->添加->新建项->代码->模块定义文件(.def)(名字随意)
  .def文件用于标明引出函数,在COM组件中,以下四个文件是必须引出的。
  在其中写入以下内容:

LIBRARY ComTest

EXPORTS

DllRegisterServer    PRIVATE
DllUnregisterServer  PRIVATE
DllGetClassObject    PRIVATE
DllCanUnloadNow      PRIVATE

  如果你没有按照上述方法添加模块定义文件,而是自己打后缀名创建,需要在项目->属性->配置属性->链接器->输入右侧的模块定义文件中加入你所创建的文件。

注册

  写完所有代码后点击生成(不是运行),快捷键Ctrl+B。所生成的.dll文件在项目目录\x64\Release\,打开该目录,以管理员身份下运行Windows PowerShell,输入regsvr32.exe .\ComTest.dll。
  若提示DllRegisterServer在.\ComTest.dll已成功,就表示注册完成啦,你可以在注册表HKEY_CLASSES_ROOT目录下找到COMCTL.CComTest。

调用自己的COM

  因为.dll文件已经在注册表中注册,所以不  需要再导入其他文件,但是需要一个IComTest.h文件声明接口。比较简单,不多赘述。
附上测试代码
IComTest.h:


#ifndef ICOMPTEST_H  
#define ICOMPTEST_H  

#include <unknwn.h>  

static const GUID IID_IComTest =
{ 0x213D1B15, 0x9BBA, 0x414A, { 0xBA, 0xB6, 0xCA, 0x5B, 0x6C, 0xEF, 0x00, 0x06 } };


class IComTest :
    public IUnknown
{
public:
    virtual HRESULT _stdcall sayHello() = 0;
};
#endif  

main.cpp:

#include <iostream>
#include <objbase.h>
#include <atlbase.h>
#include <iostream>
#include "IComTest.h"
using namespace std;


int main()
{
	CoInitialize(NULL);	// 初始化COM库,使用默认的内存分配器
	GUID CLSID_ComTest;
	HRESULT hResult = CLSIDFromProgID(L"COMCTL.CComTest", &CLSID_ComTest);
	//获取ProgID为COMCTL.CComTest组建的CLSID
	LPOLESTR szCLSID;
	StringFromCLSID(CLSID_ComTest, &szCLSID);	// 将其转化为字符串形式用来输出 
	CoTaskMemFree(szCLSID);     // 调用COM库的内存释放  
	// 用此CLSID创建一个COM对象并获取IComTest接口  
	IComTest* pComTest = NULL;
	hResult = CoCreateInstance(CLSID_ComTest, NULL, CLSCTX_INPROC_SERVER, IID_IComTest, (void**)&pComTest);
	cout << pComTest->sayHello() << endl;	// 调用我们自己接口中的函数  
	pComTest->Release();		// 释放自己的接口 
	CoUninitialize();			// COM库反初始化  
	return 0;

}

其他

  附上一个查看dll文件中导出函数的方法
  打开Developer Command Prompt for VS 2022,输入dumpbin /exports 文件绝对路径\xxx.dll即可查看

一 组件基础 1 软件开发的阶段 1.1 结构化编程 采用自顶向下的编程方式,划分模块 和功能的一种编程方式。 1.2 面向对象编程 采用对象的方式,将程序抽象成类, 模拟现实世界,采用继承、多态的方式 设计软件的一种编程方式。 1.3 面向组件编程 将功能和数据封装成二进制代码,采用 搭积木的方式实现软件的一种编程方式。 2 组件和优点 2.1 组件 - 实际是一些可以执行的二进 制程序,它可以给其他的应用程序、操 作系统或其他组件提供功能 2.2 优点 2.2.1 可以方便的提供软件定制机制 2.2.2 可以很灵活的提供功能 2.2.3 可以很方便的实现程序的分布式 开发。 3 组件的标准 - COMComponent Object Model ) 3.1 COM是一种编程规范,不论任何开发语言 要实现组件都必须按照这种规范来实现。 组件和开发语言无关。 这些编程规范定义了组件的操作、接口的 访问等等。 3.2 COM接口 COM接口是组件的核心,从一定程度上 讲"COM接口是组件的一切". COM接口给用户提供了访问组件的方式. 通过COM接口提供的函数,可以使用组件 的功能. 4 COM组件 4.1 COM组件-就是在Windows平台下, 封装在动态库(DLL)或者可执行文件(EXE) 中的一段代码,这些代码是按照COM的 规范实现. 4.2 COM组件的特点 4.2.1 动态链接 4.2.2 与编程语言无关 4.2.3 以二进制方式发布 二 COM接口 1 接口的理解 DLL的接口 - DLL导出的函数 类的接口 - 类的成员函数 COM接口 - 是一个包含了一组函数指针 的数据结构,这些函数是由组件实现的 2 C++的接口实现 2.1 C++实现接口的方式,使用抽象类 定义接口. 2.2 基于抽象类,派生出子类并实现 功能. 2.3 使用 interface 定义接口 interface ClassA { }; 目前VC中,interface其实就是struct 3 接口的动态导出 3.1 DLL的实现 3.1.1 接口的的定义 3.1.2 接口的实现 3.1.3 创建接口的函数 3.2 DLL的使用 3.2.1 加载DLL和获取创建接口的函数 3.2.2 创建接口 3.2.3 使用接口的函数 4 接口的生命期 4.1 问题 在DLL中使用new创建接口后,在用户 程序使用完该接口后,如果使用delete 直接删除,会出现内存异常. 每个模块有自己的内存堆(crtheap) EXE - crtheap DLL - crtheap new/delete/malloc/free默认情况 下都是从自己所在模块内存堆(crtheap) 中分配和施放内存.而各个模块的 这个内存堆是各自独立.所以在DLL中 使用new分配内存,不能在EXE中delete. 4.2 引用计数和AddRef/Release函数 引用计数 - 就是一个整数,作用是 表示接口的使用次数 AddRef - 增加引用计数 +1 Release - 减少引用计数 -1, 如果 当引用计数为0,接口被删除 4.3 使用 4.3.1 创建接口 4.3.2 调用AddRef,增加引用计数 4.3.3 使用接口 4.3.4 调用Release,减少引用计数 4.4 注意 4.4.1 在调用Release之后,接口指针 不能再使用 4.4.2 多线程情况下,接口引用计数 要使用原子锁的方式进行加减 5 接口的查询 5.1 每个接口都具有唯一标识 GUID 5.2 实现接口查询函数 QueryInterface 6 IUnknown 接口 6.1 IUnknown是微软定义的标准接口 我们实现所有接口就是继承这个接口 6.2 IUnknown定义了三个函数 QueryInterface 接口查询函数 AddRef 增加引用计数 Release 减少引用计数 7 接口定义语言 - IDL(Interface Definition Language ) 7.1 IDL和MIDL IDL - 定义接口的一种语言,与开发 语言无关. MIDL.EXE - 可以将IDL语言定义接口, 编译成C++语言的接口定义 7.2 IDL的基础 import "XXXX.idl" [ attribute ] interface A : interface_base { } 7.2.1 Import 导入,相当于C++的 #include 7.2.2 使用"[]"定义区域,属性描述 关键字 1) object - 后续是对象 2) uuid - 定义对象GUID 3) helpstring - 帮助信息 4) version - 版本 5) point_default - 后续对象 中指针的默认使用方式 比如: uniqune - 表示指针可以 为空,但是不能修改 7.2.3 对象定义 1) 父接口是IUnknown接口 2) 在对象内添加函数,函数定义必须 是返回 HRESULT. HRESULT是32位整数,返回函数是否 执行成功,需要使用 SUCCESSED和 FAILED宏来判断返回值.
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值