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






4.98/5 (31投票s)
使用外观模式简化 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::RenderTarget
,renderTarget
,被赋值给一个 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 日 - 一些新类,一些更新和一些错误修复