使用 WRL/C++、C++/CX 和 C# 实现使用 WRL/C++ 定义的 WinRT 接口
本文演示了如何使用 WRL 和 C++ 实现为 WinRT 组件定义的接口。
引言
本文是第三篇,它探讨了使用 Visual Studio 2012 构建 WinRT 组件的最原生方式。WinRT 组件实现的最低级别是为 Windows 8 现代风格更新的基于 COM 的技术。在 COM 中,您使用 ATL (ActiveX Template Library) 开发组件,而在 WinRT 中,Microsoft 提供的最低级框架是 WRL (Windows Runtime Library)。WRL 就像 ATL 对于 COM 一样,是一个 C++ 模板库,它提供类以直接在 C++ 中开发 WinRT 组件。
在上一篇文章中,我使用 C++/CX 定义了接口,然后使用 C++/CX 和 C# 实现这些接口。现在,让我们重新审视这些接口的创建,并使用 WRL/C++ 来定义和实现它们。然后,我们将使用 C++/CX 和 C# 来实现它们。这将提供实现相同接口的三种不同方式:两种原生技术 WRL/C++ 和 C++/CX,以及一种托管技术 C#。由于 C# 基于 CLR,因此可以使用任何其他 .NET 语言,并且代码将非常相似。
再次说明,在编写代码时,我发现了一些可能存在 bug 的问题,但到目前为止我还没有得到 Microsoft 的确认,因此本文的内容仅在发布时有效。请关注任何更新!
背景
我建议在阅读本文之前,您先阅读我之前写的第一篇和第二篇文章。正如我刚才提到的,由于 WinRT 完全基于 COM 技术(该技术是在 Windows 3.11 中为 OLE (Object Linking and Embedding) 引入的),如果您还不熟悉这项技术,也可以阅读一些 COM 参考书或教程来了解它。
接口
在之前的文章中,我定义了三个简单的接口 IPerson
、ICitizen
和 IAddress
来阐述接口/组件开发的一些基本知识。在 C++/CX 和 C# 中,编译器会生成包含组件实现公共部分的接口。在 COM 中,一个对象仅通过其接口存在,因此有必要设计这些接口并围绕它们构建您的架构。
使用 WRL,接口必须使用 MIDL (Microsoft Interface Description Language) 定义。这个 MIDL 虽然与 COM 中的 MIDL 类似,但似乎存在一些重大差异。
接口定义如下。
namespace WRLCompV1
{
interface IPerson;
interface IAddress;
interface ICitizen;
interface ISaveable;
runtimeclass PersonClass;
runtimeclass AddressClass;
runtimeclass CitizenClass;
[uuid(0d585932-fbc4-4b0a-90b5-ccf34aefd4c6)]
[version(COMPONENT_VERSION)]
interface IPerson : IInspectable
{
[propget] HRESULT Name([out, retval] HSTRING* value);
[propput] HRESULT Name([in] HSTRING value);
[propget] HRESULT Surname([out, retval] HSTRING* value);
[propput] HRESULT Surname([in] HSTRING value);
}
[uuid(497783FC-D66D-4DF6-AAFC-C4D879AB22F1)]
[version(COMPONENT_VERSION)]
interface IAddress : IInspectable
{
[propget] HRESULT Street([out, retval] HSTRING* value);
[propput] HRESULT Street([in] HSTRING value);
[propget] HRESULT City([out, retval] HSTRING* value);
[propput] HRESULT City([in] HSTRING value);
}
[uuid(C3F9CEA3-B897-4A79-BF6C-02B5DF4DB77D)]
[version(COMPONENT_VERSION)]
interface ISaveable : IInspectable
{
HRESULT CanSave([out, retval] boolean *value);
}
[uuid(863571FC-4CBB-47E8-8BD3-2709D5CB7D0D)]
[version(COMPONENT_VERSION)]
interface ICitizen : IInspectable
{
[propget] HRESULT Name([out, retval] HSTRING* value);
[propput] HRESULT Name([in] HSTRING value);
[propget] HRESULT Surname([out, retval] HSTRING* value);
[propput] HRESULT Surname([in] HSTRING value);
[propget] HRESULT Address([out, retval] IAddress** value);
[propput] HRESULT Address([in] IAddress* value);
}
[version(COMPONENT_VERSION)]
[activatable(COMPONENT_VERSION)]
runtimeclass PersonClass
{
[default] interface IPerson;
interface ISaveable;
}
[version(COMPONENT_VERSION)]
[activatable(COMPONENT_VERSION)]
runtimeclass AddressClass
{
[default] interface IAddress;
interface ISaveable;
}
[version(COMPONENT_VERSION)]
[activatable(COMPONENT_VERSION)]
runtimeclass CitizenClass
{
[default] interface ICitizen;
interface IPerson;
interface ISaveable;
}
}
使用 C++ 编译此 MIDL 接口描述将生成一个 .h 头文件,其中包含接口的所有 C 和 C++ 定义。所有接口都必须继承自 IInspectable
。到目前为止,我发现与 COM 的 MIDL 不同,除了 IInspectable
之外,不能继承其他任何内容,即使您使用的基接口已经继承自 IInspectable
,编译器也会阻止它。您也不能同时继承 IInspectable
和另一个接口。我没有从 Microsoft 获得确认,这是一种限制还是一个 bug。在我看来,这是一个严重的限制,我不太明白他们为什么这样做。但是,一个组件仍然可以实现多个不同的接口。
创建 MIDL 代码时,您必须首先按顺序声明接口和运行时类,然后定义接口,最后定义运行时类。如果您定义的运行时类使用了尚未定义的接口,即使您在同一文件中稍后定义了它,也不会编译通过。
编译 MIDL 时,编译器会生成接口的类头文件,并将运行时类名字符串定义为外部。
C++ 代码在 ABI
命名空间下创建,然后您会找到您自己的命名空间。这是 Microsoft 的一项设计决策,将所有 WRL 代码放在 ABI
命名空间下。
与传统的 COM 组件相比,存在一些差异,您创建的组件不会在全球系统注册表中注册。当应用程序通过 Windows 应用商店分发时,所有组件都必须包含在包中,不能与其他应用程序共享。只有当您在公司内部分发应用程序时,才能与不同的应用程序共享组件。此限制意味着您基本上无法构建一个允许您自由扩展通过 Windows 应用商店分发的应用程序的插件机制。
Visual Studio 2012 不自带 WRL C++ 开发的项目模板,您必须从在线模板中获取一个基本模板并导入。模板名称是WRLClassLibrary
。此模板不会为您生成太多代码,也没有向类添加方法或属性的向导,您必须手动完成所有操作。实际上,这个项目模板的功能不如 Visual C++ 提供的使用 ATL 开发 COM 组件的模板强大,但该框架现在比原始 ATL 更简单。让我们来看看这些接口在 C++ 中的实现。
使用 C++ 和 WRL 实现接口
创建项目时,模板会附带一个示例实现类,任何其他类都必须手动创建。幸运的是,实现 WinRT 组件所需的代码相当少。该类需要继承自 RuntimeClass
模板类。根据组件需要实现的接口数量,提供了几种模板。
namespace ABI { namespace WRLCompV1
{
/**
WRL component class for the IPerson interface. Also implements ISaveable as
a seperate interface
*/
class PersonClass: public RuntimeClass<IPerson, ISaveable>, protected SaveableHelper
{
InspectableClass(L"WRLCompV1.PersonClass", BaseTrust)
private:
std::wstring m_name;
std::wstring m_surname;
public:
PersonClass()
{
}
public:
// IPerson
IFACEMETHODIMP get_Name(_Out_ HSTRING* value);
IFACEMETHODIMP put_Name(_In_ HSTRING value);
IFACEMETHODIMP get_Surname(_Out_ HSTRING* value);
IFACEMETHODIMP put_Surname(_In_ HSTRING value);
// ISaveable
IFACEMETHODIMP CanSave(_Out_ boolean* value);
protected:
virtual boolean CanSaveImpl();
};
ActivatableClass(PersonClass);
// Implementation of IPerson
StringProperty(PersonClass, Name, m_name);
StringProperty(PersonClass, Surname, m_surname);
// Implementation of ISaveable
boolean PersonClass::CanSaveImpl()
{
return m_name.length() > 0 && m_surname.length() > 0;
}
CanSaveMethod(PersonClass);
}}
WinRT 运行时类将始终需要继承自模板类 RuntimeClass
。提供了一个宏来实现 IInspectable
接口(同时实现 IUnknown
),并在类的实现部分使用另一个宏来使类可激活。
我使用了一些预定义的宏,例如 IFACEMETHODIMP
、STDMETHODIMP
来声明和实现类的属性方法,并创建了一个简单的宏 StringProperty
来实现 get/set 属性的方法。如果您不熟悉 C++,宏在 C++ 开发人员中非常流行,是简化代码的强大手段。
为了实现三个组件都尝试实现的 ISaveable
接口,我试图使用一个基类,但由于 RuntimeClass
使用的一些模式,我未能完全实现我想要的功能。但是,通过一个简单的宏,我几乎实现了它的预期效果。
在实现 ICitizen
类时,我遇到了一个奇怪的问题,无法使用 ComPtr
类。ICitizen
对象与 IAddress
对象存在组合关系。通常,IAddress
指针应该由 ComPtr
类管理,但如果调用 ICitizen
的 get Address 属性,IAddress
指针的释放顺序似乎有问题。
为了解决这个问题,我使用了 COM 的原始引用计数,手动调用 IUnknown
的 AddRef()
和 Release()
。我的猜测是,当 ComPtr
用作运行时类的实例成员时会发生问题,因为在其他情况下它工作得很好。
CitizenClass
的代码在此处提供(我删除了与 ComPtr
问题无关的部分)。
class CitizenClass: public RuntimeClass<ICitizen, IPerson, ISaveable>, protected SaveableHelper
{
InspectableClass(L"WRLCompV1.CitizenClass", BaseTrust)
private:
std::wstring m_name;
std::wstring m_surname;
ABI::WRLCompV1::IAddress* m_pAddress;
public:
CitizenClass()
{
m_pAddress = nullptr;
ComPtr<ABI::WRLCompV1::IAddress> pAddress;
HStringReference activableClassId(L"WRLCompV1.AddressClass");
HRESULT hr = ActivateInstance<ComPtr<ABI::WRLCompV1::IAddress>>(activableClassId.Get(), &pAddress);
// Detach the pointer from the ComPtr class and manually call AddRef()
m_pAddress = pAddress.Detach();
m_pAddress->AddRef();
}
~CitizenClass()
{
// When the class is destroyed, release the reference of IAddress
if (m_pAddress != nullptr)
{
m_pAddress->Release();
}
}
public:
// IPerson, ICitizen
...
};
// Implementation of ICitizen
STDMETHODIMP CitizenClass::get_Address(ABI::WRLCompV1::IAddress** value)
{
HRESULT hr = E_POINTER;
if (value != nullptr)
{
*value = m_pAddress;
hr = S_OK;
}
return hr;
}
STDMETHODIMP CitizenClass::put_Address(ABI::WRLCompV1::IAddress* value)
{
HRESULT hr = E_POINTER;
if (value != nullptr)
{
// Release the previously used IAddress
if (m_pAddress != nullptr)
{
m_pAddress->Release();
}
// Set the given one and increase the reference count
m_pAddress = value;
m_pAddress->AddRef();
hr = S_OK;
}
return hr;
}
// Implementation of ISaveable
boolean CitizenClass::CanSaveImpl()
{
boolean canSave = true;
ComPtr<ABI::WRLCompV1::ISaveable> pSaveable;
ComPtr<ABI::WRLCompV1::IAddress> pAddress = m_pAddress;
HRESULT hr = pAddress.As(&pSaveable);
if (SUCCEEDED(hr))
{
hr = pSaveable->CanSave(&canSave);
}
return m_name.length() > 0 && m_surname.length() > 0 && canSave;
}
使用 C++/CX 和 C# 实现接口
使用 C++/CX 和 C# 实现给定的三个接口非常简单,您只需要将 WRL DLL 添加到项目中,并使用声明接口的命名空间。请注意,当您在 C++/CX 或 C# 中使用命名空间时,根命名空间 ABI 不存在,它仅在 C++ 与 WRL 一起使用时才需要。实际上,当您在项目中导入 DLL 时,是通过 Windows 元数据文件进行的,而该文件不包含对 ABI 的任何引用。
在 C++/CX 中,您不需要包含任何 .h 文件来获取您的组件,因为编译器所需的一切都在引用项目中的 DLL 时提供。您可以最终使用 using namespace 语句。以下是 C# 实现的代码。C++/CX 非常相似,并且与第一篇文章中提供的实现基本相同。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WRLCompV1;
namespace WinRTCompCsharpWRLItf
{
/// <summary>
/// The class Citizen implements in C# the following interfaces of WRLCompV1 namespace:
/// - ICitizen
/// - IPerson
/// - ISaveable
/// </summary>
public sealed class Citizen : ICitizen, IPerson, ISaveable
{
private IPerson person;
private IAddress address;
public Citizen()
{
person = new Person();
address = new Address();
}
#region Explicit implementation of IPerson
string IPerson.Name
{
get
{
return person.Name;
}
set
{
person.Name = value;
}
}
string IPerson.Surname
{
get
{
return person.Surname;
}
set
{
person.Surname = value;
}
}
#endregion
#region Explicit implementation of ICitizen
IAddress ICitizen.Address
{
get
{
return address;
}
set
{
address = value;
}
}
string ICitizen.Name
{
get
{
return ((IPerson) this).Name;
}
set
{
((IPerson)this).Name = value;
}
}
string ICitizen.Surname
{
get
{
return ((IPerson)this).Surname;
}
set
{
((IPerson)this).Surname = value; ;
}
}
#endregion
#region Explicit implementation of ISaveable
public bool CanSave()
{
return
((ISaveable)person).CanSave() &&
((ISaveable)address).CanSave();
}
#endregion
}
}
与上一篇关于 C# 实现 C++/CX 接口的文章一样,您需要显式实现给定的接口以避免 C# 编译器的 bug。
使用 WRL 和 C++ 的单元测试
我编写了一些简单的单元测试来演示使用 WRL/C++ 创建不同的组件。这是您使用旧式 COM 方法创建 WinRT 组件的唯一方式。您无需使用 GUID,WRL 提供了更友好的创建组件的方式。
尽管只有一套组件是用 WRL 和 C++ 实现的,但它们都可以被 C++ 代码加载。使用 WRL 创建 WinRT 组件实例是通过方法 Windows::Foundation::ActivateInstance()
完成的。以下代码展示了如何使用这个静态方法。
template<typename T>
void CreateComponent(const wchar_t* activableClassId,
::Microsoft::WRL::Details::ComPtrRef<ComPtr<T>> pInstance)
{
HStringReference runtimeClass(activableClassId);
HRESULT hr = Windows::Foundation::ActivateInstance< ComPtr<T>>( runtimeClass.Get(), pInstance );
Assert::IsTrue(SUCCEEDED(hr));
}
使用此方法,可以通过名称创建对象实例,就像使用 COM 和 CoCreateInstance()
所做的那样。这样,您可以实现一个伪插件机制,例如,您可以在 XML 文件中列出组件的名称,从而允许您在不重新编译代码的情况下添加新组件,只要它们实现了预期的接口。但是,您需要将所有组件与应用程序包一起分发,它们不能单独交付,除非您的应用程序是企业应用程序且未通过 Windows 应用商店部署。
单元测试还演示了不同技术实现的混合使用,没有任何问题。测试方法的代码在此处。
void TestMixCitizenAddress(ComPtr<ABI::WRLCompV1::ICitizen> pICitizen,
const wchar_t* wsName,
const wchar_t* wsSurname,
ComPtr<ABI::WRLCompV1::IAddress> pIAddress,
const wchar_t* wsStreet,
const wchar_t* wsCity,
const wchar_t* wsStreet2,
const wchar_t* wsCity2)
{
// Get the IPerson interface of ICitizen
ComPtr<ABI::WRLCompV1::IPerson> pIPerson;
HRESULT hr = pICitizen.As(&pIPerson);
Assert::IsTrue(SUCCEEDED(hr));
// Test the IPerson (of ICitizen...)
TestIPerson(pIPerson, wsName, wsSurname);
ComPtr<ABI::WRLCompV1::IAddress> pCitizenAddress;
hr = pICitizen->get_Address(&pCitizenAddress);
Assert::IsTrue(SUCCEEDED(hr));
// Test the integrated address
TestIAddress(pCitizenAddress, wsStreet, wsCity);
// Test the separate address
TestIAddress(pIAddress, wsStreet2, wsCity2);
// Set Address of Citizen with the separate address
hr = pICitizen->put_Address(pIAddress.Get());
Assert::IsTrue(SUCCEEDED(hr));
// Get the Address of Citizen
hr = pICitizen->get_Address(&pCitizenAddress);
Assert::IsTrue(SUCCEEDED(hr));
HSTRING hStreet;
HSTRING hCity;
// Get the Street and the City
TestGetProperty(pCitizenAddress, Street, hStreet);
TestGetProperty(pCitizenAddress, City, hCity);
HString street;
HString city;
street.Set(hStreet);
city.Set(hCity);
// Validate the test
Assert::AreEqual(wsStreet2, street.GetRawBuffer(nullptr));
Assert::AreEqual(wsCity2, city.GetRawBuffer(nullptr));
}
那些有 COM 经验的人会认出代码的结构,因为 WRL 与 ATL 非常相似。组件和单元测试的完整代码包含在附带的解决方案中。
关注点
与之前的文章一样,我使用的类层次结构非常简单,但它阐明了 WinRT 组件和 WRL 的许多可能性。我遇到许多问题,有些问题我很容易解决,有些是编译器或框架的 bug,但幸运的是,目前它们都有解决方法。
与传统的 COM 组件相比,WinRT 组件也有许多限制,我发现其中一些限制相当令人沮丧,尤其是如果您是 COM 的资深用户。
目前,WRL 和类型投影等新技术仅适用于 Windows 应用商店应用程序。如果 Microsoft 能够为常规 Windows 平台提供类型投影等现代功能,那将非常有益。