各位观众,各位朋友,欢迎来到今天的“C++ COM/ATL/WRL:Windows 平台组件化编程”特别节目!我是你们的老朋友,也是今天的主讲人,江湖人称“代码界的段子手”。
今天咱们要聊聊Windows平台上那些“高大上”的组件化编程技术,说白了,就是怎么把你的代码像搭积木一样,模块化、可复用,并且还能跨语言、跨进程地使用。听起来是不是有点玄乎?别怕,今天我就用最通俗易懂的语言,把这些概念给你们掰开了、揉碎了,喂到嘴里!
第一部分:COM,组件对象模型,一切的基石
首先,咱们得说说COM,也就是Component Object Model,组件对象模型。这玩意儿就像一座大厦的地基,是ATL和WRL的基础。
COM是微软为了解决软件组件复用问题而提出的一个规范。它定义了一套标准,让不同的软件组件可以互相“交流”,而不用关心对方是用什么语言写的,在哪里运行。这就像联合国,大家操着不同的语言,但都能通过共同的协议一起开会。
COM的核心思想:
- 接口(Interface): 这是COM组件对外暴露功能的唯一途径。你可以把接口想象成插座,不同的电器(组件)只要插头(接口)匹配,就能使用插座(接口)提供的电力(功能)。
- 组件(Component): 这是实现了特定接口的二进制代码模块。就像家里的电器,比如电饭煲,它实现了“煮饭”这个功能,并且通过插头(接口)对外提供服务。
- GUID(Globally Unique Identifier): 这是COM组件和接口的唯一标识符。就像每个人的身份证号码,确保不会重复。
COM的优点:
- 二进制复用: 组件编译后可以直接使用,不需要重新编译。
- 语言无关性: 组件可以用任何支持COM的语言编写。
- 版本控制: 可以方便地升级组件,而不会破坏现有应用程序。
COM的缺点:
- 学习曲线陡峭: COM的概念比较抽象,需要一定的学习成本。
- 手动内存管理: 需要手动管理COM对象的生命周期,容易出错。
一个简单的COM例子 (C++):
#include <iostream>
#include <objbase.h> // COM相关的头文件
#include <initguid.h> // 定义GUID
// 定义接口IHello
interface IHello : public IUnknown {
public:
virtual HRESULT STDMETHODCALLTYPE SayHello() = 0;
};
// 定义接口的GUID
DEFINE_GUID(IID_IHello, 0x12345678, 0x1234, 0x1234, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef);
// 定义组件CHello,实现IHello接口
class CHello : public IHello {
private:
long m_cRef; // 引用计数
public:
CHello() : m_cRef(1) {} // 构造函数,初始化引用计数
// IUnknown接口的实现
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override {
if (riid == IID_IUnknown || riid == IID_IHello) {
*ppvObject = static_cast<IHello*>(this);
AddRef();
return S_OK;
}
*ppvObject = nullptr;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef() override {
return InterlockedIncrement(&m_cRef);
}
ULONG STDMETHODCALLTYPE Release() override {
ULONG cRef = InterlockedDecrement(&m_cRef);
if (cRef == 0) {
delete this;
}
return cRef;
}
// IHello接口的实现
HRESULT STDMETHODCALLTYPE SayHello() override {
std::cout << "Hello from COM!" << std::endl;
return S_OK;
}
};
// 定义类工厂CHelloFactory,用于创建CHello对象
class CHelloFactory : public IClassFactory {
private:
long m_cRef; // 引用计数
public:
CHelloFactory() : m_cRef(1) {} // 构造函数,初始化引用计数
// IUnknown接口的实现
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override {
if (riid == IID_IUnknown || riid == IID_IClassFactory) {
*ppvObject = static_cast<IClassFactory*>(this);
AddRef();
return S_OK;
}
*ppvObject = nullptr;
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef() override {
return InterlockedIncrement(&m_cRef);
}
ULONG STDMETHODCALLTYPE Release() override {
ULONG cRef = InterlockedDecrement(&m_cRef);
if (cRef == 0) {
delete this;
}
return cRef;
}
// IClassFactory接口的实现
HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppvObject) override {
if (pUnkOuter != nullptr) {
return CLASS_E_NOAGGREGATION;
}
CHello* pHello = new CHello();
if (pHello == nullptr) {
return E_OUTOFMEMORY;
}
HRESULT hr = pHello->QueryInterface(riid, ppvObject);
if (FAILED(hr)) {
delete pHello;
return hr;
}
return S_OK;
}
HRESULT STDMETHODCALLTYPE LockServer(BOOL fLock) override {
return S_OK;
}
};
// 定义组件的CLSID
DEFINE_GUID(CLSID_Hello, 0x87654321, 0x4321, 0x4321, 0x43, 0x21, 0x87, 0x65, 0x43, 0x21, 0xfe, 0xdc);
// 注册组件的函数
HRESULT RegisterServer() {
HKEY hKey = nullptr;
LONG lResult;
wchar_t szModulePath[MAX_PATH];
GetModuleFileNameW(nullptr, szModulePath, MAX_PATH);
// 注册CLSID
wchar_t szCLSID[39]; // GUID的字符串表示形式
StringFromGUID2(CLSID_Hello, szCLSID, 39);
wchar_t szKeyPath[256];
swprintf_s(szKeyPath, L"Software\Classes\CLSID\%s", szCLSID);
lResult = RegCreateKeyExW(HKEY_LOCAL_MACHINE, szKeyPath, 0, nullptr, REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &hKey, nullptr);
if (lResult != ERROR_SUCCESS) {
return HRESULT_FROM_WIN32(lResult);
}
RegSetValueExW(hKey, nullptr, 0, REG_SZ, (BYTE*)L"CHello", sizeof(L"CHello"));
RegCloseKey(hKey);
// 注册InprocServer32
swprintf_s(szKeyPath, L"Software\Classes\CLSID\%s\InprocServer32", szCLSID);
lResult = RegCreateKeyExW(HKEY_LOCAL_MACHINE, szKeyPath, 0, nullptr, REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &hKey, nullptr);
if (lResult != ERROR_SUCCESS) {
return HRESULT_FROM_WIN32(lResult);
}
RegSetValueExW(hKey, nullptr, 0, REG_SZ, (BYTE*)szModulePath, (wcslen(szModulePath) + 1) * sizeof(wchar_t));
RegCloseKey(hKey);
return S_OK;
}
// 注销组件的函数
HRESULT UnregisterServer() {
HKEY hKey = nullptr;
LONG lResult;
// 注销CLSID
wchar_t szCLSID[39]; // GUID的字符串表示形式
StringFromGUID2(CLSID_Hello, szCLSID, 39);
wchar_t szKeyPath[256];
swprintf_s(szKeyPath, L"Software\Classes\CLSID\%s\InprocServer32", szCLSID);
lResult = RegDeleteTreeW(HKEY_LOCAL_MACHINE, szKeyPath); // 删除InprocServer32
swprintf_s(szKeyPath, L"Software\Classes\CLSID\%s", szCLSID);
lResult = RegDeleteTreeW(HKEY_LOCAL_MACHINE, szKeyPath); // 删除CLSID
return S_OK;
}
// 导出函数,用于创建类工厂
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) {
if (rclsid == CLSID_Hello) {
CHelloFactory* pFactory = new CHelloFactory();
if (pFactory == nullptr) {
return E_OUTOFMEMORY;
}
HRESULT hr = pFactory->QueryInterface(riid, ppv);
if (FAILED(hr)) {
delete pFactory;
return hr;
}
return S_OK;
}
return CLASS_E_CLASSNOTAVAILABLE;
}
// 导出函数,用于判断是否可以卸载DLL
STDAPI DllCanUnloadNow() {
// 这里简单判断,如果组件没有被使用,则可以卸载
// 实际情况需要更复杂的逻辑
return S_OK;
}
// 程序的入口点
int main() {
// 初始化COM
CoInitialize(nullptr);
// 注册组件
RegisterServer();
// 使用组件
IHello* pHello = nullptr;
HRESULT hr = CoCreateInstance(CLSID_Hello, nullptr, CLSCTX_INPROC_SERVER, IID_IHello, (void**)&pHello);
if (SUCCEEDED(hr)) {
pHello->SayHello();
pHello->Release(); // 释放组件
} else {
std::cerr << "Failed to create COM object: " << hr << std::endl;
}
// 注销组件
UnregisterServer();
// 释放COM
CoUninitialize();
return 0;
}
代码解释:
- 定义接口
IHello
: 定义了一个名为IHello
的接口,只有一个方法SayHello
。 - 定义组件
CHello
: 实现了IHello
接口,并实现了SayHello
方法,打印 "Hello from COM!"。 - 定义类工厂
CHelloFactory
: 负责创建CHello
对象。这是COM创建对象的标准方式。 - 注册组件: 将组件的信息写入注册表,以便系统能够找到并加载组件。
- 使用组件: 使用
CoCreateInstance
函数创建组件实例,并调用SayHello
方法。 - 引用计数: COM使用引用计数来管理对象的生命周期。
AddRef
增加引用计数,Release
减少引用计数。当引用计数为0时,对象会被销毁。
运行这个例子,你需要:
- 编译这段代码生成一个DLL文件。
- 运行程序,它会自动注册组件。
- 程序会创建一个COM对象并调用
SayHello
方法,控制台会输出 "Hello from COM!"。 - 程序会自动注销组件。
这个例子虽然简单,但已经包含了COM的核心概念。是不是感觉有点像在玩乐高积木?
第二部分:ATL,让COM编程更轻松
COM虽然强大,但手动编写COM代码非常繁琐。大量的代码都是重复的,比如接口的实现、引用计数的管理等等。这时候,ATL(Active Template Library)就闪亮登场了!
ATL是微软提供的一个C++模板库,旨在简化COM组件的开发。它提供了一系列的类和宏,可以自动生成大量的COM代码,让你专注于业务逻辑的实现。
ATL的优点:
- 简化COM编程: 自动生成大量的COM代码,减少了手动编写的工作量。
- 高性能: 使用模板技术,避免了虚函数调用,提高了性能。
- 易于使用: 提供了丰富的类和宏,方便开发人员使用。
ATL的缺点:
- 学习曲线: 需要掌握ATL的类和宏的使用方法。
- 代码膨胀: 使用模板技术可能会导致代码膨胀。
一个简单的ATL例子 (C++):
#include <atlbase.h> // ATL基本头文件
#include <atlcom.h> // ATL COM头文件
#include <iostream>
// 定义接口IHello
[
object,
uuid("12345678-1234-1234-1234-567890abcdef"), // 接口的GUID
pointer_default(unique)
]
__interface IHello : IUnknown {
HRESULT SayHello();
};
// 定义组件CHello
[
coclass,
uuid("87654321-4321-4321-4321-fedcba098765"), // 组件的CLSID
threading(apartment) // 指定线程模型
]
class CHello : public CComObjectRootEx<CComSingleThreadModel>, public IHello {
public:
CHello() {}
DECLARE_REGISTRY_RESOURCEID(IDR_HELLO) // 注册表资源ID
BEGIN_COM_MAP(CHello)
COM_INTERFACE_ENTRY(IHello)
COM_INTERFACE_ENTRY(IUnknown)
END_COM_MAP()
DECLARE_PROTECT_FINAL_CONSTRUCT()
HRESULT FinalConstruct() {
return S_OK;
}
void FinalRelease() {}
public:
// IHello接口的实现
STDMETHODIMP SayHello() {
std::cout << "Hello from ATL!" << std::endl;
return S_OK;
}
};
OBJECT_ENTRY_AUTO(__uuidof(CHello), CHello) // 自动注册组件
// 程序的入口点
int main() {
// 初始化COM
CoInitialize(nullptr);
// 创建COM对象
CComPtr<IHello> pHello;
HRESULT hr = pHello.CoCreateInstance(__uuidof(CHello));
if (SUCCEEDED(hr)) {
pHello->SayHello();
} else {
std::cerr << "Failed to create COM object: " << hr << std::endl;
}
// 释放COM
CoUninitialize();
return 0;
}
代码解释:
- 使用ATL宏定义接口和组件: 使用了
__interface
和coclass
属性来定义接口和组件,简化了代码。 - 使用ATL提供的类: 使用了
CComObjectRootEx
和CComSingleThreadModel
等ATL提供的类,自动处理了引用计数和线程模型等问题。 - 使用COM_MAP宏: 使用了
COM_MAP
宏来声明组件实现的接口。 - 使用CComPtr智能指针: 使用了
CComPtr
智能指针来管理COM对象的生命周期,避免了手动释放内存的麻烦。
运行这个例子,你需要:
- 创建一个ATL项目。
- 将这段代码添加到项目中。
- 编译运行。
你会发现,ATL大大简化了COM编程的复杂性。
第三部分:WRL,拥抱现代C++的COM
随着C++标准的不断发展,COM也需要与时俱进。WRL(Windows Runtime Library)应运而生,它是微软为了开发Windows Runtime组件而推出的一个C++模板库。
WRL在ATL的基础上,进一步简化了COM编程,并引入了现代C++的特性,比如智能指针、lambda表达式等等。
WRL的优点:
- 更简洁的语法: 使用现代C++特性,语法更加简洁易懂。
- 更好的性能: WRL的设计更加注重性能,避免了不必要的开销。
- 与Windows Runtime集成: 可以方便地开发Windows Runtime组件。
WRL的缺点:
- 学习曲线: 需要掌握WRL的类和函数的使用方法。
- 只支持Windows平台: WRL只能在Windows平台上使用。
一个简单的WRL例子 (C++):
#include <wrl/client.h> // WRL客户端头文件
#include <wrl/implements.h> // WRL实现头文件
#include <iostream>
using namespace Microsoft::WRL;
using namespace Microsoft::WRL::Wrappers;
// 定义接口IHello
MIDL_INTERFACE("12345678-1234-1234-1234-567890abcdef")
IHello : public IUnknown {
public:
virtual HRESULT STDMETHODCALLTYPE SayHello() = 0;
};
// 定义组件CHello
class CHello : public RuntimeClass<RuntimeClassFlags<ClassicCom>, IHello> {
public:
IFACEMETHODIMP SayHello() override {
std::cout << "Hello from WRL!" << std::endl;
return S_OK;
}
};
//创建组件
CoCreatableClass(CHello);
// 程序的入口点
int main() {
// 初始化COM
RoInitialize(RO_INIT_MULTITHREADED);
// 创建COM对象
ComPtr<IHello> pHello;
HRESULT hr = ActivateInstance(HStringReference(L"CHello").Get(), &pHello); // 使用字符串激活类名
if (SUCCEEDED(hr)) {
pHello->SayHello();
} else {
std::cerr << "Failed to create COM object: " << hr << std::endl;
}
// 释放COM
RoUninitialize();
return 0;
}
代码解释:
- 使用WRL提供的类: 使用
RuntimeClass
和ComPtr
等 WRL 提供的类,简化了 COM 对象的创建和管理。 - 使用
ActivateInstance
函数: 使用ActivateInstance
函数来创建 COM 对象,而不是CoCreateInstance
。 - 使用
HStringReference
类: 使用HStringReference
类来传递字符串,避免了字符串的复制。
运行这个例子,你需要:
- 创建一个Windows控制台应用程序项目。
- 将这段代码添加到项目中。
- 在项目属性中,设置“平台工具集”为Visual Studio 2017 或更高版本。
- 编译运行。
你会发现,WRL让COM编程更加现代化。
总结:COM/ATL/WRL的比较
为了让大家更清晰地了解COM、ATL和WRL的区别,我做了一个表格:
特性 | COM | ATL | WRL |
---|---|---|---|
编程模型 | 手动 | 模板库 | 模板库 |
语法 | 繁琐 | 简洁 | 更简洁 |
内存管理 | 手动 | 智能指针 | 智能指针 |
性能 | 一般 | 高 | 更好 |
兼容性 | 广泛 | Windows平台 | Windows Runtime |
学习曲线 | 陡峭 | 中等 | 中等 |
适用场景 | 底层组件开发 | COM组件开发 | Windows Runtime组件开发 |
选择哪个?
- 如果你需要开发底层的COM组件,或者需要最大限度的兼容性,那么可以选择COM。
- 如果你需要快速开发COM组件,并且对性能有一定要求,那么可以选择ATL。
- 如果你需要开发Windows Runtime组件,或者喜欢现代C++的风格,那么可以选择WRL。
结语:
好了,各位观众,今天的“C++ COM/ATL/WRL:Windows 平台组件化编程”特别节目就到这里了。希望通过今天的讲解,大家对COM、ATL和WRL有了更深入的了解。
记住,编程就像搭积木,只要掌握了方法,就能创造出无限可能!感谢大家的收看,我们下期再见!