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

使用 WRL/C++、C++/CX 和 C# 实现使用 WRL/C++ 定义的 WinRT 接口

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2013年1月8日

CPOL

9分钟阅读

viewsIcon

52239

downloadIcon

388

本文演示了如何使用 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 参考书或教程来了解它。

接口

在之前的文章中,我定义了三个简单的接口 IPersonICitizenIAddress 来阐述接口/组件开发的一些基本知识。在 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),并在类的实现部分使用另一个宏来使类可激活。

我使用了一些预定义的宏,例如 IFACEMETHODIMPSTDMETHODIMP 来声明和实现类的属性方法,并创建了一个简单的宏 StringProperty 来实现 get/set 属性的方法。如果您不熟悉 C++,宏在 C++ 开发人员中非常流行,是简化代码的强大手段。

为了实现三个组件都尝试实现的 ISaveable 接口,我试图使用一个基类,但由于 RuntimeClass 使用的一些模式,我未能完全实现我想要的功能。但是,通过一个简单的宏,我几乎实现了它的预期效果。

在实现 ICitizen 类时,我遇到了一个奇怪的问题,无法使用 ComPtr 类。ICitizen 对象与 IAddress 对象存在组合关系。通常,IAddress 指针应该由 ComPtr 类管理,但如果调用 ICitizen 的 get Address 属性,IAddress 指针的释放顺序似乎有问题。

为了解决这个问题,我使用了 COM 的原始引用计数,手动调用 IUnknownAddRef()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 平台提供类型投影等现代功能,那将非常有益。

© . All rights reserved.