C++ COM / ATL / WRL:Windows 平台组件化编程

各位观众,各位朋友,欢迎来到今天的“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;
}

代码解释:

  1. 定义接口 IHello: 定义了一个名为 IHello 的接口,只有一个方法 SayHello
  2. 定义组件 CHello: 实现了 IHello 接口,并实现了 SayHello 方法,打印 "Hello from COM!"。
  3. 定义类工厂 CHelloFactory: 负责创建 CHello 对象。这是COM创建对象的标准方式。
  4. 注册组件: 将组件的信息写入注册表,以便系统能够找到并加载组件。
  5. 使用组件: 使用 CoCreateInstance 函数创建组件实例,并调用 SayHello 方法。
  6. 引用计数: COM使用引用计数来管理对象的生命周期。AddRef 增加引用计数,Release 减少引用计数。当引用计数为0时,对象会被销毁。

运行这个例子,你需要:

  1. 编译这段代码生成一个DLL文件。
  2. 运行程序,它会自动注册组件。
  3. 程序会创建一个COM对象并调用SayHello方法,控制台会输出 "Hello from COM!"。
  4. 程序会自动注销组件。

这个例子虽然简单,但已经包含了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;
}

代码解释:

  1. 使用ATL宏定义接口和组件: 使用了 __interfacecoclass 属性来定义接口和组件,简化了代码。
  2. 使用ATL提供的类: 使用了 CComObjectRootExCComSingleThreadModel 等ATL提供的类,自动处理了引用计数和线程模型等问题。
  3. 使用COM_MAP宏: 使用了 COM_MAP 宏来声明组件实现的接口。
  4. 使用CComPtr智能指针: 使用了 CComPtr 智能指针来管理COM对象的生命周期,避免了手动释放内存的麻烦。

运行这个例子,你需要:

  1. 创建一个ATL项目。
  2. 将这段代码添加到项目中。
  3. 编译运行。

你会发现,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;
}

代码解释:

  1. 使用WRL提供的类: 使用 RuntimeClassComPtr 等 WRL 提供的类,简化了 COM 对象的创建和管理。
  2. 使用 ActivateInstance 函数: 使用 ActivateInstance 函数来创建 COM 对象,而不是 CoCreateInstance
  3. 使用 HStringReference 类: 使用 HStringReference 类来传递字符串,避免了字符串的复制。

运行这个例子,你需要:

  1. 创建一个Windows控制台应用程序项目。
  2. 将这段代码添加到项目中。
  3. 在项目属性中,设置“平台工具集”为Visual Studio 2017 或更高版本。
  4. 编译运行。

你会发现,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有了更深入的了解。

记住,编程就像搭积木,只要掌握了方法,就能创造出无限可能!感谢大家的收看,我们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注