65.9K
CodeProject 正在变化。 阅读更多。
Home

C++ Windows 开发,COM API 客户端

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (31投票s)

2012年10月31日

CPOL

7分钟阅读

viewsIcon

65798

downloadIcon

1638

使用外观模式简化 COM 基于 API 的开发

引言

Microsoft Component Object Model (COM) 是一种提供软件组件之间二进制互操作性的机制,据我所知,它是迄今为止最成功的互操作性标准。

COM 是 Windows 和 .NET 的基本组成部分。.NET 开发人员一直在使用 COM 编写软件,即使他们可能没有意识到这一点。通过在项目属性的“应用程序”页面上单击“程序集信息…”按钮,然后选择“使程序集 COM 可见”复选框,即可轻松地将大多数为 .NET 平台编写的软件的功能公开给非托管代码。

多年来,Microsoft 已向 Windows 添加了大量基于 COM 的 API,对于 C++ 开发人员来说,使用它们有时会有些麻烦。作为开发人员,您显然需要检查程序错误,并且大多数基于 COM 的 API 会返回一个 HRESULT,其中包含有关调用是否成功的相关信息。通常,负值表示发生了错误,但并非所有 API 都遵循此约定。另一方面,.NET 会透明地将错误转换为异常,从而改善了整个开发体验。

COM 的基本功能之一是对象生命周期管理,它通过引用计数来实现,因此 COM 要求 C++ 开发人员在不再需要对象时显式递减对象的引用计数。

void foo()
{
    IWICImagingFactory* pIWICFactory;

    HRESULT hr = CoCreateInstance(CLSID_WICImagingFactory1,
                               NULL,CLSCTX_INPROC_SERVER,
                               IID_IWICImagingFactory,(void**)&pIWICFactory);
    if(SUCCEEDED(hr))
    {
        __try
        {
          // You can now use the object until you
          // decrement the reference count by calling
          // Release() on the requested interface 
        }
        __finally
        {
            pIWICFactory->Release();
        }
    }
}

使用 Release(),这是所有接口都包含的三个函数之一。

class IUnknown
{
public:
 virtual HRESULT __stdcall QueryInterface(REFIID riid, void **ppvObject) = 0;
 virtual ULONG __stdcall AddRef( void ) = 0;
 virtual ULONG __stdcall Release( void ) = 0;
};

虽然 IUnknown 不再像这样声明,但它在技术上是接口的正确定义,并且也是如此。

typedef struct IUnknownVtbl
{
  HRESULT ( __stdcall *QueryInterface )( IUnknown * This, REFIID riid, void **ppvObject);
  ULONG ( __stdcall *AddRef )( IUnknown * This );
  ULONG ( __stdcall *Release )( IUnknown * This );
} IUnknownVtbl;

typedef struct tagIUnknown
{
    struct IUnknownVtbl *lpVtbl;
}IUnknown;

这告诉我们关于 COM 接口的一个基本事实:接口只是一个 C++ 类,由纯虚函数组成——实际上它就是一个结构体,只包含一个指向另一个只包含函数指针的结构体的指针。

当我们创建一个新接口时

class IClassFactory : public IUnknown
{
public:
 virtual HRESULT __stdcall CreateInstance( IUnknown *pUnkOuter, REFIID riid, void **ppvObject) = 0;
 virtual HRESULT __stdcall LockServer( BOOL fLock) = 0;
};

我们将该接口声明为派生自 IUnknown 的类,仅包含纯 virtual 函数,这与以下内容相同:

typedef struct IClassFactoryVtbl
{
    HRESULT ( __stdcall *QueryInterface )( IClassFactory * This, REFIID riid, void **ppvObject);
    ULONG ( __stdcall *AddRef )( IClassFactory * This );
    ULONG ( __stdcall *Release )( IClassFactory * This);
    HRESULT ( __stdcall *CreateInstance )( IClassFactory * This,IUnknown *pUnkOuter, 
                                                  REFIID riid, void **ppvObject);
    HRESULT ( __stdcall *LockServer )( IClassFactory * This, BOOL fLock);
} IClassFactoryVtbl;

typedef struct tagIClassFactory
{
    struct IClassFactoryVtbl *lpVtbl;
} IClassFactory;

请注意,函数指针的顺序与 C++ 表示接口的纯 virtual 函数声明的顺序相同。来自 IUnknown 的函数指针在前,然后是表示 IClassFactory 类添加到接口的两个函数的函数指针。

IUnknown deceptively simple,但如果您停下来思考一下,您会发现它非常优雅。Addref()Release() 提供了生命周期管理所需的功能,而 QueryInterface 允许我们访问为对象实现的所有接口。

void foo(IUnknown* pUnknownForWICImagingFactory1)
{
    IWICImagingFactory* pIWICFactory = nullptr;

    HRESULT hr = pUnknownForWICImagingFactory1->QueryInterface(IID_IWICImagingFactory,
                                 reinterpret_cast<void**>(&pIWICFactory));
    if(SUCCEEDED(hr))
    {
        __try
        {
            // call functions that are part of the IWICImagingFactory interface
        }
        __finally
        {
            pIWICFactory->Release();
        }
    }
}

真正需要记住的一件事是:您必须始终为通过 QueryInterface 或任何其他 COM API 函数获取的接口调用 Release()

本文附带了我正在开发的一个库的源代码,该库使 C++ 中的 COM 客户端开发在某种程度上类似于 .NET 开发人员体验 COM 开发。

以下函数打开通用项对话框,允许用户选择一个图像,然后使用 Windows Imaging Component 将该图像加载并转换为 DIB 部分。

void DeviceContextExampleForm::OpenImageFile()
{
    auto fileOpenDialog = harlinn::windows::FileOpenDialog::Create();
    if(fileOpenDialog->Show(GetSafeHandle()))
    {
        auto item = fileOpenDialog->GetResult();
        String fileName = item.GetDisplayName(SIGDN_FILESYSPATH);
        bitMap = BitmapHandle::LoadFromFile(fileName);
    }
}

该库包含许多 Windows API COM 接口的包装器,涵盖:

  • Direct2D
  • DirectWrite
  • Windows Imaging Component
  • Windows Property System
  • 核心 COM 接口

当我们使用 .NET 访问基于 COM 的 API 时,事情会更容易一些,因为 .NET 会为我们执行此错误检查,并在发生故障时,.NET 会将其转换为一个异常,在控制权返回到我们的代码之前抛出。这实际上是 .NET 提供的一个非常巧妙的服务,因为当我们转为原生开发时,就像在这个函数中一样:

HRESULT DemoApp::ConvertBitmapSource(HWND hWnd, IWICBitmapSource **ppToRenderBitmapSource)
{
    *ppToRenderBitmapSource = NULL;
    HRESULT hr = S_OK;
    RECT rcClient;
    hr = GetClientRect(hWnd, &rcClient) ? S_OK: E_FAIL;
    if (SUCCEEDED(hr))
    {
        IWICBitmapScaler *pScaler = NULL;
        hr = m_pIWICFactory->CreateBitmapScaler(&pScaler);
        if (SUCCEEDED(hr))
        {
            hr = pScaler->Initialize(m_pOriginalBitmapSource, rcClient.right - rcClient.left, 
                rcClient.bottom - rcClient.top, WICBitmapInterpolationModeFant);
        }
        if (SUCCEEDED(hr))
        {
            IWICFormatConverter *pConverter = NULL;
            hr = m_pIWICFactory->CreateFormatConverter(&pConverter);
            if (SUCCEEDED(hr))
            {
                hr = pConverter->Initialize(pScaler,GUID_WICPixelFormat32bppBGR, 
                    WICBitmapDitherTypeNone,NULL, 0.f,WICBitmapPaletteTypeCustom);

                if (SUCCEEDED(hr))
                {
                    hr = pConverter->QueryInterface(IID_PPV_ARGS(ppToRenderBitmapSource));
                }
            }
            SafeRelease(pConverter);
        }
        SafeRelease(pScaler);
    }
    return hr;
}

突然有很多事情与对象生命周期和错误处理有关。不仅仅是 .NET 将错误转换为异常,它还管理我们引用的生命周期。

为了解决问题的生命周期部分,通常会使用智能接口指针类,例如 CComPtr<>,它在生命周期方面效果很好,但我们仍然需要检查 HRESULT,因为智能接口指针通过实现 T* operator -> () const 来实现其魔力,从而提供对接口的访问。

我想象这样实现它:

BitmapSource DemoApp::ConvertBitmapSource(std::shared_ptr<Control> theControl)
{
    RECT rect = theControl->GetClientRect();
    auto scaler = imagingFactory.CreateBitmapScaler();
    scaler.Initialize(originalBitmapSource, rect.right - rect.left, 
                rect.bottom - rect.top, BitmapInterpolationMode::Fant);
    auto converter = imagingFactory.CreateFormatConverter();
    converter.Initialize(scaler,GUID_WICPixelFormat32bppBGR, 0.f,BitmapPaletteType::Custom);
    return converter;
}

甚至更好,这样:

BitmapSource DemoApp::ConvertBitmapSource(std::shared_ptr<Control> theControl)
{
    RECT rect = theControl->GetClientRect();
    return originalBitmapSource.
        Scale(rect.right - rect.left, rect.bottom - rect.top).
        Convert(GUID_WICPixelFormat32bppBGR);
}

以上说明了我期望一个设计良好的 C++ API 如何工作,让我能够专注于我对特定代码块的期望,而不是错误处理和生命周期管理。

错误处理和生命周期管理仍然需要执行,但它们是在后台进行的,有点类似于 .NET 开发人员在处理 COM 时可以理所当然的事情。

外观模式

外观模式是一种软件设计模式,常用于面向对象编程。名称指的是建筑外观——这是您从外部看到的东西。外观对象为开发人员提供了一个简化的接口,隐藏了与大型代码(如类库)交互的复杂性。外观对象可以:

  • 使软件库更容易使用、理解和测试,因为外观提供了常用任务的便捷方法。
  • 出于类似的原因,使库更具可读性。
  • 减少外部代码对库内部工作原理的依赖。由于客户端代码使用外观,因此这还增加了开发过程的灵活性。
  • 用一个设计良好的 API 包装一组设计不佳的 API。

正如 ConvertBitmapSource 的最后一个实现所示,这可以极大地简化开发过程,同时提高应用程序的健壮性。

要求

由于 COM 接口继承了 IUnknown 接口,允许我们访问底层对象实现的所有其他接口,这是我们希望外观对象也具有的重要功能。

Unknown unknown = renderTarget;
graphics::RenderTarget rt;
if(unknown.Is<graphics::RenderTarget>())
{
    rt = unknown.As<graphics::RenderTarget>();
}

在上面的代码中,一个 graphics::RenderTargetrenderTarget,被赋值给一个 Unknown 对象,unknown。之后,Is<T>() 函数用于确定 unknown 是否可以成功转换为 graphics::RenderTarget 类型的对象——这是使用 As<T>() 函数完成的。

这意味着我们可以以一种直接的方式执行重要的 COM 相关操作,而无需真正关心使其正常工作的任何细节。

实现

Unknown 类的大部分内容在 使用 Direct2D & DirectWrite 渲染文本[^] 中已经介绍过,但当时侧重于如何正确实现移动构造函数和移动赋值运算符。

class Unknown
{
protected:
    IUnknown* unknown;
public:
    // The mandatory InterfaceType, used by many temple functions.
    // 
    // Every derived class must provide a typedef named InterfaceType  
    // for the interface it wraps. 
    typedef IUnknown InterfaceType; 

    Unknown();
    explicit Unknown(IUnknown* theUnknown, bool addref = false);

    // Constructor used during conversion
    Unknown(REFIID iid, const Unknown& theUnknown, bool throwIfNoInterface = true ); 
    Unknown(const Unknown& other);
    Unknown(Unknown&& other);
    ~Unknown();

    operator bool() const;

    Unknown& operator = (const Unknown& other);
    Unknown& operator = (Unknown&& other);

    Unknown& Reset(IUnknown* other = nullptr, bool addRef = false);
    // The As<T> function
    template<typename T> 
    T As() const;

    // The Is<T> function
    template<typename T> 
    bool Is() const;

    template<typename T> 
    static T CoCreateInstanceFromClassId
             (const CLSID& clsid, DWORD classContext = CLSCTX_INPROC_SERVER);

    template<typename T>
    static T CoCreateInstanceFromClassId
             (const String& clsid, DWORD classContext = CLSCTX_INPROC_SERVER);

    template<typename T>
    static T CoCreateInstanceFromProgId
             (const String& progId, DWORD classContext = CLSCTX_INPROC_SERVER);
};

转换过程中使用的构造函数接受三个参数:

  • REFIID iid – 标识请求的接口。
  • const Unknown& theUnknown – 保存我们要从中检索另一个接口的对象引用的对象。
  • bool throwIfNoInterface – 指示如果对象上不存在请求的接口,构造函数是否会抛出异常。
Unknown(REFIID iid, const Unknown& theUnknown, bool throwIfNoInterface = true )
    : unknown(nullptr)
{
    if( theUnknown )
    {
        IUnknown* pInterface = nullptr;
        auto hr = theUnknown.unknown->QueryInterface(iid,(void**)&pInterface);
        if(FAILED(hr))
        {
            if((throwIfNoInterface == false)&&(hr == E_NOINTERFACE))
            {
                return;
            }
            CheckHRESULT(hr);
        }
        unknown = pInterface;
    }
}

派生类通常会提供类似的构造函数供后代使用:

protected:
 BitmapSource (REFIID iid, const Unknown& theUnknown, bool throwIfNoInterface = true );

以及一个使用 __uuidof(InterfaceType) 来获取 REFIID,标识正在构造的对象接口的构造函数。

public:
    BitmapSource (const Unknown& theUnknown, bool throwIfNoInterface = true ) 
        : Base ( __uuidof(InterfaceType), theUnknown, throwIfNoInterface ) 
        { }

现在我们有了实现 As<T>() const 函数所需的构建块:

template<typename T>
T As() const
{
    const Unknown& self = *this;
    T result(self,false);
    return result;
}

这使得从一个接口包装类型转换为另一个接口包装类型,只要它们满足我们已经介绍的要求。

Is<T>() const 函数使用相同的机制查询一个对象是否可以转换为另一种对象类型。

template<typename T>
bool Is() const
{
    if(unknown)
    {
        T::InterfaceType* pInterface = nullptr;
        auto hr = unknown->QueryInterface(__uuidof(T::InterfaceType),(void**)&pInterface);
        if(hr == S_OK )
        {
            pInterface->Release();
            return true;
        }
    }
    return false;
}

到目前为止,我们已经实现了一个提供重要功能来支持我们的 COM 包装器外观的简单机制——使它们非常灵活。

实现包装器函数

IEnumUnknown::Clone 的签名是 COM API 函数的典型示例。

virtual HRESULT STDMETHODCALLTYPE Clone( 
            /* [out] */ __RPC__deref_out_opt IEnumUnknown **ppenum) = 0;

我们的包装器实现如下:

EnumUnknown Clone()
{
    auto pInterface = GetInterface();
    IEnumUnknown* pClone = nullptr;
    auto hr = pInterface->Clone(&pClone);
    if(FAILED(hr))
    {
        CheckHRESULT(hr);
    }
    return EnumUnknown(pClone);
}

GetInterface() 函数将返回 IEnumUnknown 接口的指针,或者在未分配接口指针时抛出异常。然后我们继续调用 COM 接口并检查是否发生错误。如果 hr 小于 0,CheckHRESULT 将抛出异常。

如果一切顺利,我们将构造并返回一个 EnumUnknown 对象。

结论

通过为 COM API 实现外观类,我们可以改进 COM 客户端的开发体验,使我们的应用程序更易于维护和更健壮,而且好处是这并不难。

如果您以前从未用过 C++ 模板,这也说明了您可以只用很少的代码获得很多收益——而且它不必复杂才能有用。

历史

  • 2012 年 11 月 1 日 - 初次发布
  • 2012 年 11 月 15 日 - 库更新 - 查看 C++ Windows 开发,处理菜单[^] 获取库更改列表。
  • 2012 年 11 月 18 日 - 库更新
  • 2012 年 11 月 21 日 - 库更新
  • 2012 年 11 月 22 日 - 库更新
  • 2012 年 11 月 30 日 - 库更新
  • 2014 年 8 月 20 日 - 多次更新和错误修复
  • •2015 年 1 月 3 日 - 一些新类,一些更新和一些错误修复
© . All rights reserved.