目录
步骤一:理解COM技术
介绍COM的基础知识
1. COM的目的和特点
2. COM的关键概念
3. COM的实现
4. COM与DCOM、ActiveX
讨论COM的用途
1. 软件自动化
2. 插件和扩展
3. 跨语言开发
4. 分布式计算
5. 系统级组件
6. 网络浏览器插件
步骤二:设置开发环境
步骤三:编写COM组件
1. 定义COM接口
定义接口和CLSID
2. 实现接口
3. 注册组件
步骤四:使用COM组件
自动调用,需注册DLL
手动调用
步骤五:单元测试
单元测试策略
步骤六:更新DLL并兼容老的
1. 使用版本控制
2. 使用COM的接口继承
示例:加入新功能到COM DLL
出现的问题及其成因
步骤一:理解COM技术
介绍COM的基础知识
件对象模型(COM)是一个由微软开发的软件架构,旨在促进不同软件组件之间的二进制交互。COM定义了一种方法,使得在各种编程语言中编写的组件可以相互通信,不仅在同一个程序内部,而且可以在不同的计算机上。以下是对COM基础知识的更详尽的解释:
1. COM的目的和特点
COM是为了解决软件开发中的可重用性、灵活性和版本控制等问题而设计的。其主要特点包括:
- 语言中立性:COM允许用任何支持COM的编程语言(如C++, Visual Basic, Delphi等)编写的组件彼此交互。
- 二进制互操作性:COM组件以二进制标准进行通信,这意味着可以在不重新编译调用者代码的情况下替换组件。
- 位置透明性:COM组件可以在本地机器上运行,也可以通过网络在远程机器上运行。
2. COM的关键概念
理解COM的工作原理涉及几个核心概念:
- 接口:COM使用接口与实现分离的方式,组件提供的功能通过一组严格定义的接口暴露。接口是一组函数的集合,类似于C++中的纯虚函数的类定义。
- GUID和IID:每个COM接口和组件都有一个全局唯一标识符(GUID),接口的GUID也称为接口ID(IID)。这些标识符确保COM的注册和引用是唯一的。
- COM库和注册表:COM组件在系统中的注册依靠COM库来管理,每个组件的信息(包括位置和可用的接口)都记录在Windows注册表中。
- 引用计数:COM使用引用计数来管理内存和生命周期,组件负责跟踪有多少客户端正在使用它,并相应地管理其生命周期。
3. COM的实现
在实际应用中,一个COM对象通常会通过以下步骤实现和使用:
- 创建实例:客户端通过CoCreateInstance()等函数创建COM对象实例。
- 接口查询:使用IUnknown接口的QueryInterface方法来查询支持的接口。IUnknown是所有COM接口的基接口,提供了对象生命周期管理和接口查询的基本方法。
- 调用接口:一旦获取了接口的指针,客户端就可以调用其方法来执行操作。
- 释放接口:完成操作后,客户端需要调用接口的Release方法来减少其引用计数,当引用计数达到0时,COM对象将自行销毁。
4. COM与DCOM、ActiveX
- DCOM:分布式组件对象模型(DCOM)是COM的扩展,支持在网络上的不同计算机之间通信。
- ActiveX:ActiveX控件是基于COM的一种特殊形式,用于在网页上嵌入和执行特定功能的组件。
COM为开发者提供了一种强大的机制,用于创建可在多个程序之间共享的模块化组件。尽管现代软件开发中已经出现了许多新的技术和方法,但在需要高度稳定和兼容性的大型企业应用中,COM依然具有其独特的价值。通过确保组件可以被不同语言编写的应用程序使用,并且可以被安全地更新和替换,COM帮助软件系统实现了更好的维护性和
讨论COM的用途
组件对象模型(COM)是一个为了提高软件模块化和可重用性而设计的技术。自从1990年代初期由微软引入以来,它已经在各种应用中证明了其价值。COM的设计允许开发者创建灵活、可重用的组件,这些组件可以被不同的应用程序在不同的环境中使用,而不需要了解组件的内部实现细节。以下是一些具体的COM用途:
1. 软件自动化
COM广泛用于办公软件的自动化。通过COM接口,应用程序(如Microsoft Office套件)可以暴露其功能给外部脚本或程序,允许自动化复杂的任务。例如,一个企业可能使用VBA(Visual Basic for Applications)脚本与Excel交互,自动化报表的生成和数据分析。
2. 插件和扩展
COM使得软件开发者能够为他们的应用程序创建插件架构,其他开发者可以为这些应用程序开发添加功能的插件。这种插件通常以DLL的形式实现,并通过COM接口与主应用程序通信。例如,图形处理软件如Photoshop可以通过COM插件来扩展其图像处理能力。
3. 跨语言开发
由于COM的语言无关性,使用不同编程语言开发的组件可以互相操作。这意味着一个用C++编写的组件可以被一个用Visual Basic或C#开发的应用程序使用,反之亦然。这种特性极大地增加了不同软件项目间代码的可重用性。
4. 分布式计算
通过DCOM(分布式组件对象模型),COM技术扩展到网络。DCOM允许在不同计算机上运行的组件通过网络通信,支持构建分布式应用。这在处理大型企业级应用中尤其有用,例如金融服务领域的数据处理和实时交易系统。
5. 系统级组件
COM还用于操作系统级别的功能扩展,如Shell扩展处理器、服务组件等。Windows操作系统本身就广泛使用COM为开发者提供可扩展的API,使得第三方开发者可以创建与Windows深度集成的软件解决方案。
6. 网络浏览器插件
ActiveX控件,一种基于COM的技术,曾经是实现浏览器功能扩展的主要方式,允许网页通过嵌入的ActiveX组件来提供富交互性应用,虽然现在由于安全考虑,其使用已经大幅减少。
COM技术的设计初衷是促进软件组件的重用,降低开发成本,并加速开发过程。虽然现代软件开发中已经有了更现代的技术(如.NET Framework),但COM因其强大的跨语言和跨平台的互操作性,在很多现存的系统中仍然保持着其重要性。通过灵活使用COM,软件开发者可以创造出更加模块化、易于管理和维护的应用程序。
步骤二:设置开发环境
安装必要的工具:需要Visual Studio和Windows SDK。
Microsoft Visual Studio C++2017+Windows 11 SDK环境_microsoft visual c++ 2017-CSDN博客
配置项目:创建一个新的Win32项目,并设置为DLL类型,因为大多数COM组件都是以DLL形式发布。
Visual Studio 2022如何创建Win32项目_vs2022怎么创建win32项目-CSDN博客
步骤三:编写COM组件
1. 定义COM接口
定义接口和CLSID
MathOperations.h
#include <windows.h>
#include <Unknwnbase.h>
// Interface ID (IID) for IMathOperations
// {12345678-1234-1234-1234-123456789012}
static const IID IID_IMathOperations =
{ 0x12345678, 0x1234, 0x1234, { 0x12, 0x34, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12 } };
// Class ID (CLSID) for MathOperations
// {87654321-4321-4321-4321-210987654321}
static const CLSID CLSID_MathOperations =
{ 0x87654321, 0x4321, 0x4321, { 0x43, 0x21, 0x21, 0x09, 0x87, 0x65, 0x43, 0x21 } };
// Define the IMathOperations interface
class IMathOperations : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE Add(int a, int b, int* result) = 0;
virtual HRESULT STDMETHODCALLTYPE Subtract(int a, int b, int* result) = 0;
};
2. 实现接口
实现COM接口和类,并提供必要的方法和引用计数逻辑。
MathOperations.cpp
#include "ComTest.h"
class MathOperations : public IMathOperations
{
volatile long refCount;
public:
MathOperations() : refCount(1) {}
// IUnknown methods
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override {
if (riid == IID_IUnknown || riid == IID_IMathOperations) {
*ppvObject = static_cast<IMathOperations*>(this);
this->AddRef();
return S_OK;
}
*ppvObject = NULL;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef() override {
return InterlockedIncrement(&refCount);
}
ULONG STDMETHODCALLTYPE Release() override {
ULONG res = InterlockedDecrement(&refCount);
if (res == 0) delete this;
return res;
}
// IMathOperations methods
HRESULT STDMETHODCALLTYPE Add(int a, int b, int* result) override {
*result = a + b;
return S_OK;
}
HRESULT STDMETHODCALLTYPE Subtract(int a, int b, int* result) override {
*result = a - b;
return S_OK;
}
};
extern "C" __declspec(dllexport) HRESULT CreateInstance(REFIID riid, void** ppv) {
MathOperations* pMath = new MathOperations();
HRESULT hr = pMath->QueryInterface(riid, ppv);
pMath->Release(); // release initial reference
return hr;
}
3. 注册组件
注册COM组件通常涉及向Windows注册表添加条目,以便系统可以找到并实例化COM对象。这可以通过手动添加注册表键值或使用注册表函数在安装过程中自动完成。
也可以直接调用dll,无需注册。
步骤四:使用COM组件
为了调用上文定义的 COM DLL,我们需要编写一个客户端程序,该程序使用 COM 组件进行通信。以下是如何在 C++ 中编写调用 MathOperations COM 组件的代码。此代码演示了如何初始化 COM,创建组件实例,调用接口方法,以及最后如何清理。
自动调用,需注册DLL
#include <iostream>
#include <windows.h>
#include "MathOperations.h" // 包含COM接口定义
int main() {
HRESULT hr = CoInitialize(NULL); // 初始化COM库
if (FAILED(hr)) {
std::cout << "Failed to initialize COM library." << std::endl;
return -1;
}
IMathOperations* pMathOps = nullptr; // 指向接口的指针
// 创建组件实例
hr = CoCreateInstance(CLSID_MathOperations, // 组件的CLSID
NULL, // 没有外部聚合
CLSCTX_INPROC_SERVER, // DLL运行在相同的进程
IID_IMathOperations, // 请求的接口IID
(void**)&pMathOps); // 指针存放位置
if (SUCCEEDED(hr)) {
int result = 0;
// 调用Add方法
hr = pMathOps->Add(5, 3, &result);
if (SUCCEEDED(hr)) {
std::cout << "Addition Result: " << result << std::endl;
}
// 调用Subtract方法
hr = pMathOps->Subtract(5, 3, &result);
if (SUCCEEDED(hr)) {
std::cout << "Subtraction Result: " << result << std::endl;
}
// 释放接口
pMathOps->Release();
} else {
std::cout << "Failed to create component instance." << std::endl;
}
CoUninitialize(); // 清理COM
return 0;
}
关键步骤解释
-
初始化 COM:
- 使用
CoInitialize
或CoInitializeEx
初始化 COM 库,这是使用 COM 组件前必需的步骤。
- 使用
-
创建 COM 对象实例:
- 使用
CoCreateInstance
函数来创建 COM 组件的实例。这个函数需要 CLSID 来找到正确的组件,还需要 IID 来获取指定的接口。
- 使用
-
调用接口方法:
- 一旦获取了接口指针,就可以调用定义的方法。此示例中调用了
Add
和Subtract
方法。
- 一旦获取了接口指针,就可以调用定义的方法。此示例中调用了
-
释放接口:
- 完成操作后,需要调用接口的
Release
方法来减少引用计数。当引用计数达到零时,COM 对象会被销毁。
- 完成操作后,需要调用接口的
-
清理 COM:
- 在程序结束前,使用
CoUninitialize
清理 COM 环境。
- 在程序结束前,使用
这段代码假设 COM 组件已经正确注册在系统上,且客户端和服务器共享接口定义(头文件)。这是在同一台机器上或已经通过某种方式共享了头文件的情况。在实际部署中,通常需要将接口的定义(通常是 IDL 文件或编译后的类型库)与客户端开发者共享。
手动调用
如果您想要手动加载 DLL 而不是使用 CoCreateInstance()
来自动加载,可以采用显式加载的方式。这通常涉及使用 Windows API 如 LoadLibrary
和 GetProcAddress
来动态加载 DLL 并获取函数指针。这种方式对于 COM 组件来说稍微复杂,但可行,尤其是在某些特定环境中,如当你没有注册 COM 组件到系统注册表时。
#include <iostream>
#include <windows.h>
#include "MathOperations.h" // 包含COM接口定义
// 定义函数指针类型
typedef HRESULT (*PFN_CREATE_INSTANCE)(REFIID riid, void** ppv);
int main() {
HMODULE hDll = LoadLibrary(TEXT("MathOperations.dll")); // 动态加载DLL
if (hDll == NULL) {
std::cout << "Failed to load DLL." << std::endl;
return -1;
}
// 获取函数指针
PFN_CREATE_INSTANCE pfnCreateInstance = (PFN_CREATE_INSTANCE)GetProcAddress(hDll, "CreateInstance");
if (pfnCreateInstance == NULL) {
std::cout << "Failed to get function address." << std::endl;
FreeLibrary(hDll);
return -1;
}
HRESULT hr = CoInitialize(NULL); // 初始化COM库
if (FAILED(hr)) {
std::cout << "Failed to initialize COM library." << std::endl;
FreeLibrary(hDll);
return -1;
}
IMathOperations* pMathOps = nullptr; // 指向接口的指针
// 创建组件实例
hr = pfnCreateInstance(IID_IMathOperations, (void**)&pMathOps);
if (SUCCEEDED(hr)) {
int result = 0;
// 调用Add方法
hr = pMathOps->Add(5, 3, &result);
if (SUCCEEDED(hr)) {
std::cout << "Addition Result: " << result << std::endl;
}
// 调用Subtract方法
hr = pMathOps->Subtract(5, 3, &result);
if (SUCCEEDED(hr)) {
std::cout << "Subtraction Result: " << result << std::endl;
}
// 释放接口
pMathOps->Release();
} else {
std::cout << "Failed to create component instance." << std::endl;
}
CoUninitialize(); // 清理COM
FreeLibrary(hDll); // 释放DLL
return 0;
}
关键改动说明:
-
加载 DLL:
- 使用
LoadLibrary
加载 DLL 文件。这需要 DLL 文件的路径,路径可以是绝对或相对的。
- 使用
-
获取函数地址:
- 使用
GetProcAddress
获取 DLL 中导出函数的地址。这里的CreateInstance
是假设你的 DLL 中导出了一个创建 COM 对象的函数。
- 使用
-
手动创建实例:
- 使用获取到的函数指针
pfnCreateInstance
来创建 COM 对象的实例。
- 使用获取到的函数指针
-
释放资源:
- 使用
FreeLibrary
释放加载的 DLL。这是确保资源正确管理的重要步骤。
- 使用
步骤五:单元测试
讨论如何为COM组件编写和执行单元测试,确保其功能正确。
单元测试策略
-
测试环境设置:
- 确保COM环境已正确初始化。
- 加载COM组件,以便在测试期间使用。
-
测试案例设计:
- 对每个方法执行正常值测试。
- 对每个方法进行边界值测试。
- 对每个方法进行错误处理测试,例如输入无效参数。
-
资源清理:
- 测试完成后,正确释放所有资源。
#include "pch.h"
#include "CppUnitTest.h"
#include "../MathOperations/MathOperations.h" // 包含MathOperations接口定义
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace MathOperationsTests
{
TEST_CLASS(MathOperationsTests)
{
public:
IMathOperations* pMathOps = nullptr;
// 测试初始化
TEST_METHOD_INITIALIZE(Setup)
{
CoInitialize(NULL); // 初始化COM
HRESULT hr = CoCreateInstance(CLSID_MathOperations, NULL, CLSCTX_INPROC_SERVER,
IID_IMathOperations, (void**)&pMathOps);
Assert::IsTrue(SUCCEEDED(hr));
}
// 测试清理
TEST_METHOD_CLEANUP(Teardown)
{
if (pMathOps)
{
pMathOps->Release();
pMathOps = nullptr;
}
CoUninitialize(); // 清理COM
}
// 测试Add方法
TEST_METHOD(TestAdd)
{
int result = 0;
HRESULT hr = pMathOps->Add(10, 20, &result);
Assert::AreEqual(S_OK, hr);
Assert::AreEqual(30, result);
}
// 测试Subtract方法
TEST_METHOD(TestSubtract)
{
int result = 0;
HRESULT hr = pMathOps->Subtract(30, 10, &result);
Assert::AreEqual(S_OK, hr);
Assert::AreEqual(20, result);
}
};
}
步骤六:更新DLL并兼容老的
1. 使用版本控制
在DLL中,通常通过接口的版本控制来保持向后兼容性。为此,你可以:
- 保留旧接口不变:确保原有的接口不发生变化,以保证依赖于这些接口的现有应用程序可以继续无缝工作。
- 新增接口:为新功能创建新的接口。这可以通过继承原有接口并添加新方法来实现,或者定义一个完全独立的新接口。
2. 使用COM的接口继承
对于COM组件,接口继承是保持老版本兼容的常见方法。具体步骤如下:
- 定义新接口:基于现有的接口(如
IMathOperations
),你可以定义一个新接口(如IMathOperations2
),在其中加入新的方法。 - 实现新接口:在COM类中实现这个新接口,同时保留对旧接口的支持。
示例:加入新功能到COM DLL
假设原来的DLL提供了加法和减法功能,现在需要添加一个乘法功能。
#include <windows.h>
#include <Unknwnbase.h>
// Interface ID (IID) for IMathOperations
// {12345678-1234-1234-1234-123456789012}
static const IID IID_IMathOperations =
{ 0x12345678, 0x1234, 0x1234, { 0x12, 0x34, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12 } };
// Class ID (CLSID) for MathOperations
// {87654321-4321-4321-4321-210987654321}
static const CLSID CLSID_MathOperations =
{ 0x87654321, 0x4321, 0x4321, { 0x43, 0x21, 0x21, 0x09, 0x87, 0x65, 0x43, 0x21 } };
// Define the IMathOperations interface
class IMathOperations : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE Add(int a, int b, int* result) = 0;
virtual HRESULT STDMETHODCALLTYPE Subtract(int a, int b, int* result) = 0;
};
// Interface ID (IID) for IMathOperations2
// {98765432-4321-4321-4321-123456789012}
static const IID IID_IMathOperations2 =
{ 0x98765432, 0x4321, 0x4321, { 0x43, 0x21, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12 } };
class IMathOperations2 : public IMathOperations
{
public:
virtual HRESULT STDMETHODCALLTYPE Multiply(int a, int b, int* result) = 0;
};
LoadDLL:这里注意:
IMathOperations2* pMathOps = nullptr; 这个指针不能生命错了,需要是新更新的类,更新的类里有继承老的类,所以这个IMathOperations2*调用老方法也没问题。
#include <iostream>
#include <windows.h>
#include "../COMTest/ComTest.h" // 包含COM接口定义
// 定义函数指针类型
typedef HRESULT(*PFN_CREATE_INSTANCE)(REFIID riid, void** ppv);
int main() {
HMODULE hDll = LoadLibrary(TEXT("COMTest.dll")); // 动态加载DLL
if (hDll == NULL) {
std::cout << "Failed to load DLL." << std::endl;
return -1;
}
// 获取函数指针
PFN_CREATE_INSTANCE pfnCreateInstance = (PFN_CREATE_INSTANCE)GetProcAddress(hDll, "CreateInstance");
if (pfnCreateInstance == NULL) {
std::cout << "Failed to get function address." << std::endl;
FreeLibrary(hDll);
return -1;
}
HRESULT hr = CoInitialize(NULL); // 初始化COM库
if (FAILED(hr)) {
std::cout << "Failed to initialize COM library." << std::endl;
FreeLibrary(hDll);
return -1;
}
IMathOperations2* pMathOps = nullptr; // 指向接口的指针
// 创建组件实例
hr = pfnCreateInstance(IID_IMathOperations2, (void**)&pMathOps);
if (SUCCEEDED(hr)) {
int result = 0;
// 调用Add方法
hr = pMathOps->Add(5, 3, &result);
if (SUCCEEDED(hr)) {
std::cout << "Addition Result: " << result << std::endl;
}
// 调用Subtract方法
hr = pMathOps->Subtract(5, 3, &result);
if (SUCCEEDED(hr)) {
std::cout << "Subtraction Result: " << result << std::endl;
}
hr = pMathOps->Multiply(5, 3, &result);
if (SUCCEEDED(hr)) {
std::cout << "Subtraction Result: " << result << std::endl;
}
// 释放接口
pMathOps->Release();
}
else {
std::cout << "Failed to create component instance." << std::endl;
}
CoUninitialize(); // 清理COM
FreeLibrary(hDll); // 释放DLL
system("pause");
return 0;
}
出现的问题及其成因
-
GetProcAddress返回空指针:
- 原因可能包括函数名称修饰(name mangling)错误、DLL未正确加载或者指定的函数未正确导出。解决方法包括使用
extern "C"
来避免C++的名称修饰,并确保使用__declspec(dllexport)
正确导出函数。
- 原因可能包括函数名称修饰(name mangling)错误、DLL未正确加载或者指定的函数未正确导出。解决方法包括使用
-
COM接口的IID定义和使用:
- 在C++中定义新接口时需要提供全新的IID。这是因为每个COM接口必须有一个全局唯一的标识符。未正确处理这一点可能导致接口不可识别或引用错误。
-
代码示例中缺少导出声明:
- 最初的示例中未包括
__declspec(dllexport)
,这导致了动态链接库中的函数不能被外部访问。这反映了在COM开发过程中对DLL导出规则的关注不足。
- 最初的示例中未包括
-
保持向后兼容性的方法:
- 展示了如何通过继承和扩展现有COM接口来添加新功能,而不会影响依赖旧接口的现有应用程序。这是软件维护中非常重要的策略,以避免引入破坏性变更。
COM编出来的dll可维护性极高。