在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
即可查看