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

Visual C++ 和 WinRT/Metro - 一些基础知识

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (55投票s)

2011 年 9 月 29 日

CPOL

13分钟阅读

viewsIcon

310709

使用 Visual C++ 消耗和创建 WinRT 对象的基础知识

开发者预览警告

本文及其内容基于 Windows 8 和 Visual Studio 11 的第一个公开开发者预览版。因此,文章中提到的代码片段和其他信息可能会在操作系统/Visual Studio 发布 Beta/RTM 版本时发生重大变更。

目标 WinRT/Metro

WinRT 是用于在 Windows 8 上编写 Metro 应用程序的新 API。与传统的基于 C 的 Windows API 不同,WinRT 是一个基于 COM 的 C++ API。COM 本身得到了显著的改进和增强,以支持现代面向对象的设计范例。因此,您现在拥有了诸如继承和静态函数等概念(这对于传统的 COM 开发人员来说可能很有趣)。这里最大的优势在于,这使得开发部门在发布各种 Microsoft 语言(Visual C++、C#、VB 以及现在的 JS/HTML5)的编译器时,能够实现真正的语言平等。他们通过使用特定于语言的投影来实现这一点,将底层的 WinRT 对象“投影”到对特定编程语言的开发人员而言在语义上熟悉的语言结构中。我在这里并不想过多讨论 WinRT 编程模型,相反,在本文中,我打算快速回顾一下使用 Visual C++ 来使用和创建 WinRT 对象的基础知识。

为什么使用 C++?

在多年忍受了 Windows Forms、WPF (Avalon) 以及最近的 Silverlight 等开发框架之后,这些框架明显是为从 C# 和 VB 使用而设计的,并且要求 C++ 用户必须经历一些严重的障碍才能使用它们,这是一个非常合理的问题。那么,这里是其中的要点。WinRT 是原生的。我再说一遍,这次带上感叹号。WinRT 是原生的!虽然基于 COM 的设计使其可以被托管客户端使用,但当您这样做时,确实需要付出原生-托管互操作的代价。但性能通常是一个被高估的概念。当数据库/Web 服务调用无论如何都要花费 2 秒钟时,为什么还要节省 20 纳秒呢?并非所有应用程序都如此。有相当数量的应用程序,即使是性能的微小提升也能给最终用户带来很大的影响。而且随着 C++ 的发展,我们已经达到了一个点,我们不必成为受虐狂就能获得更好的性能。假装使用 C++ 会像使用 C# 或 VB 一样容易是幼稚的。但同样幼稚的是对这门语言及其库在过去五年中发生的现代变化保持无知,这些变化使其使用起来比五年前要简单得多。如果您是 C++ 开发人员,您是否愿意付出 15-20% 的额外努力来获得 15-30% 的性能提升?这个问题的答案就是本文的理由。

C++ 组件扩展

虽然 WinRT 本身部分是用 C++ 开发的,但它并不是为 C++ 调用者直接使用的。它还需要支持其他语言,因此是基于 COM 的 API。现在,当我提到 COM 时,我并不完全准确,因为 WinRT 远不止 COM,但我会继续使用 COM 这个词,因为它比“RT”(代表运行时)听起来更悦耳。Visual C++ 11 引入了一种新的编程模型/语法来创建和使用 WinRT 组件。他们称之为 C++ 组件扩展,简称 C++/CX。在语法上,它几乎(但不是 100%)与用于目标 CLR 的 C++/CLI 语法相同。因此,如果您以前使用过 C++/CLI(或者像我认识的某个人一样写过一本关于它的书),您会觉得语法非常熟悉。这有点像一艘外星飞船降落在地球上,然后我们发现外星人说英语,但带着威尔士口音。这种口音确实很奇怪,但它毕竟还是某种形式的英语。

嗯,这种相似性纯粹是语法上的,其语义和含义完全不同。C++/CX 是原生代码,C++/CLI 是托管代码。C++/CX 允许开发人员专注于重要的事情,例如设计代码和数据结构,而不是浪费时间纠正 COM 调用。COM 在 .NET 出现后难以保持流行的原因之一是,当进行严肃的 COM 开发时,您最终会花费大量时间进行“管道工作”,而不是专注于实际应用程序。

ref new^ (hat)

C++/CLI 添加了 gcnew 关键字,它是 CLR 中原生 new 的等价物。同样,C++/CX 也有上下文关键字 ref new 来创建 WinRT 对象。ref new 返回一个 ^ (hat),它类似于 * (指针),但它用于引用计数的 COM 对象。这是一个代码示例。

WinRTComponentDll2::WinRTComponent^ comp = ref new WinRTComponentDll2::WinRTComponent();

请注意对 ref new 的调用(而不是 new),以及返回的变量的类型是如何成为 ^(而不是 *)的。我使用了这种语法来展示返回类型是一个 hat,通常我会只使用 auto

auto comp = ref new WinRTComponentDll2::WinRTComponent();

或者,我可以完全避免使用 ref new 并使用栈语义。

WinRTComponentDll2::WinRTComponent comp;

因此,如果您对在代码中到处看到 hat 感到厌恶,可以在某些地方避免看到它。您需要记住,您不仅仅是在堆上创建一个 C++ 对象。comp 对象是我们创建的,它是一个 COM 对象。在 WinRT 下,COM 对象是通过激活工厂创建的。激活工厂实现了 IActivationFactory 接口。

MIDL_INTERFACE("00000035-0000-0000-C000-000000000046")
IActivationFactory : public IInspectable
{
public:
    virtual HRESULT STDMETHODCALLTYPE ActivateInstance( 
        /* [out] */ __RPC__deref_out_opt IInspectable **instance) = 0;
    
}

WinRT 包含一个 RoGetActivationFactory 函数,它为您提供特定 WinRT 类的激活工厂。如果您实现了自己的 WinRT 类,那么您也需要实现一个工厂(该工厂实现 IActivationFactory)。话虽如此,您实际上不需要这样做,因为当您使用 C++/CX 时,编译器会为您生成它。因此,一旦获得 IActivationFactory 对象,就会在其上调用 ActivateInstance 方法。如果调用成功,将通过 out 参数返回指向 IInspectable 对象的指针。IInspectable 对于 WinRT 来说,就像 IUnknown 对于传统 COM 一样。IInspectable 继承自 IUnknown,所以这在某种程度上使得每个 WinRT 组件也成为 COM 对象。

MIDL_INTERFACE("AF86E2E0-B12D-4c6a-9C5A-D7AA65101E90")
IInspectable : public IUnknown
{
public:
    virtual HRESULT STDMETHODCALLTYPE GetIids( 
        /* [out] */ __RPC__out ULONG *iidCount,
        /* [size_is][size_is][out] */ __RPC__deref_out_ecount_full_opt(*iidCount) IID **iids) = 0;
    
    virtual HRESULT STDMETHODCALLTYPE GetRuntimeClassName( 
        /* [out] */ __RPC__deref_out_opt HSTRING *className) = 0;
    
    virtual HRESULT STDMETHODCALLTYPE GetTrustLevel( 
        /* [out] */ __RPC__out TrustLevel *trustLevel) = 0;
    
};

每个 WinRT 对象都将实现 IUnknownIInspectable。一旦我们有了 IInspectable 对象,获取我们想要的接口仅仅是调用 QueryInterface 的问题。编译器还会生成代码来适当地调用 AddRefRelease。因此,当您执行一些看似微不足道的操作(调用 ref new)时,底层会发生很多事情,从性能的角度来看,这是需要考虑的。

什么是 hat?

Deon Brewis 在 //Build/ 大会上有一个 C++ 会话,他将 hat 定义为指向 vtable 的指针的语法糖,换句话说,就是指向指向函数指针的指针。为了更好地说明这一点,请看下面的代码。

WinRTComponentDll2::WinRTComponent^ comp = ref new WinRTComponentDll2::WinRTComponent();

typedef HRESULT  (__stdcall* GetRuntimeClassNameFunc)(Object^, HSTRING*);

auto pGetRuntimeClassNameFunc = (*reinterpret_cast<GetRuntimeClassNameFunc**>(comp))[4];

HSTRING className;
HRESULT hr = pGetRuntimeClassNameFunc(comp, &className);	
if(SUCCEEDED(hr))
{
  WindowsDeleteString(className);
  className = nullptr;
}

那么,我怎么知道我需要的函数指针是 vtable 中的第 5 个呢?嗯,IUnknownQIAddRefRelease——这占据了前 3 个位置,然后我们有 IInspectable 方法,GetIids 是第 4 个位置,接着是 GetRuntimeClassName,然后是 GetTrustLevel。这是固定的,对于任何 WinRT 对象都不会改变。

每次您调用 hat 上的方法时,都是对相应接口的 COM 调用。考虑一个简单的如下方法调用。

comp->Method(5);

这将导致对相应接口的 QueryInterface 调用。C++/CX 将把自定义 WinRT 类中定义的所有非虚方法包装在一个编译器生成的接口中,该接口在开发者预览版中显示为 winmd 元数据文件中的 __IWinRTComponentPublicNonVirtuals。因此,hat 上的每个方法调用都将是虚函数调用,这一点需要注意。

使用 Platform::String

在前面的代码示例中,您可能已经看到了 HSTRING 的使用。HSTRING 是 WinRT 字符串的句柄,似乎是 COM 的 BSTR 的运行时等价物。WinRT 中的字符串是不可变的。有 WindowsCreateStringWindowsDeleteString 等方法可用于操作 HSTRING。您还可以使用 WindowsGetStringRawBuffer 获取原始缓冲区。正如您所能想象的那样,进行所有这些操作非常不方便,毫不奇怪,C++ 库包含了一个名为 Platform::String 的包装器/帮助类。这是一个小代码片段,用于操作字符串并显示包装器为您进行的相应的 WinRT 调用。

void Foo()
{
  String^ str = "Hello C++ world";
  // WindowsCreateStringReference is called
  
  auto len = str->Length();
  // WindowsGetStringLen gets called
  
  String^ copy = str;
  // WindowsDuplicateString gets called
  
  str = nullptr;
  // WindowsDeleteString is called
  
  auto raw = copy->Data();
  // WindowsGetStringRawBuffer is called
 
  raw = nullptr;
  // Pointer set to null
  
  copy = nullptr;
  // WindowsDeleteString is called
}

几乎所有对 String^ 的操作都会导致 WinRT 调用。编译器可能会优化其中一些调用,但在这种情况下,最好不要总是依赖编译器来完成这项工作,或者做出此类假设。这一点非常重要,需要了解。在设计自定义 WinRT 组件时,您只能在 ABI 边界使用 String^。对于所有内部代码,您绝对必须使用 C++ 字符串(如 std::wstring)。既然您是 C++ 程序员,这里还有另一个建议。切勿修改原始后端缓冲区,因为 WinRT 将始终假定原始缓冲区是不可变的且以 null 结尾。

创建 WinRT 组件

使用组件仅仅是 ref new 然后调用 hat 上的方法。创建自定义组件也同样容易。

public ref class WinRTComponent sealed
{
    int _data;

public:
    WinRTComponent();
    ~WinRTComponent();

    property int Data
    {
      int get() 
      { 
        return _data; 
      }

      void set(int value) 
      { 
        _data = value; 
      }
    }

    int Method(int i);
};

语法与 C++/CLI 惊人地相似(依我看,这是设计使然)。COM 实际上没有属性,因此会生成 getter 和 setter 方法。在 C++/CLI 中,您无法混合类型,例如,您无法在托管类中嵌入原生类,或者在原生类中嵌入托管类,而不使用 gcrootCAutoNativePtr(我写的)之类的间接引用。C++/CX 没有这样的限制。您可以混合 C++ 和 RT 类型。

class NonRTClass
{
};

public ref class WinRTComponent sealed
{
  NonRTClass _nonRTClass;
};

class AnotherNonRTClass
{
  WinRTComponent^ _winRTComponent;
};

在设计 WinRT 组件时,您有一个限制,那就是在公共方法中只能使用 WinRT 类型。您不能在公共接口中使用 C++ 类型。幸运的是,编译器不会让您无意中这样做。

public ref class Ref sealed
{
private:
  void Foo(std::wstring) // <-- compiles
  {
  }

public:
  void Bar(std::wstring) // <-- will not compile
  {
  }
};

请注意,编译器并不关心您如何处理私有方法。它只关心公共方法。

Object 类

所有 WinRT 组件都直接或间接继承自 Object。.NET 范例中 System::Object 是绝对基类,具有可重写的方法,在这里并不太适用。WinRT 在设计时并未考虑继承,并且继承官方仅支持 Xaml 组件。Object 中的任何方法都不是 virtual 的。因此,您可能会在 Object 中看到 ToString 并认为您可以重写它以返回一些好的内容。不行,行不通!如果您真的想要类似的东西,您可能需要考虑拥有自己的根基类,然后从那里实现您的对象层次结构。我认为在 C++/CX 中使用 Object^ 的方式就像 void* (顺便说一句,没有 void^ 这种东西)。我的严格非官方观点是,Object 最终成为所有类的基类是一个设计决策,旨在方便 C# 和 VB 等托管语言使用和创建 WinRT 组件。

装箱

VC++ 团队和装箱(boxing)有着一段历史。当他们首次发布 Managed C++ 时,他们使用 __box 作为执行显式装箱的关键字。那时 C# 已经具有隐式装箱。后来,当 C++/CLI 首次推出时,一个重大变化是装箱现在是隐式的。好吧,现在使用 C++/CX,我们又回到了显式装箱。而且目前甚至不那么直接。

void Foo()
{
  int n = 12;

  // Box to Object^
  Object^ boxedObj = PropertyValue::CreateInt32(n);

  // Unbox to an int
  IReference<int>^ refInt =  dynamic_cast<IReference<int>^>(boxedObj);
  int x = refInt->Value;
}

使用自定义值类型(例如您创建的类型)在当前预览版本中是不可能这样做的。

value struct V 
{
public:
    int x;
};

void Foo()
{
    V v;
    v.x = 15;

    // Box to Object^
    Object^ boxedObj = %v; // <-- compiler error

抛出的错误是:error C3960: 'V' : illegal usage of not-yet-implemented feature: Boxing。所以,也许他们会在 Beta 版本中修复它!

WinRT 类型和 C++ 类型之间的转换

基本转换相当简单,例如在 C++ 和 WinRT 字符串之间进行转换。

String^ s = "WinRT/Metro";
std::wstring ws = s->Data();
s = nullptr;
s = ref new String(ws.c_str());

对于集合,您可以使用 <collection.h>(由 VC++ 团队的 Stephan T. Lavavej (STL) 编写),其中包含许多用于集合操作的功能。这里有一些示例代码,演示了如何在 std::mapPlatform::Map 之间进行转换。

std::map<int, String^> map;
map[1] = L"hello";
map[3] = L"C++";
map[4] = L"world";
	
auto rtMap = ref new Platform::Map<int, String^>(map);
String^ s = rtMap->Lookup(1);

当然,这之所以可行,是因为我在 std::map 中使用了 String^。通常情况下,您会期望那里是 wstring。如果确实如此,那么您需要编写代码来在复制项目时手动转换类型。

std::map<int, std::wstring> map;
map[1] = L"hello";
map[3] = L"C++";
map[4] = L"world";

auto rtMap = ref new Platform::Map<int, String^>();
for (auto it = map.begin(); it != map.end(); it++)
{
  rtMap->Insert(it->first, ref new String(it->second.c_str()));
}

到 VC++ 11 RTM 发布时,我猜会有更多的帮助器/包装器被添加,这将使大多数这些转换变得更容易一些。但无论如何,自己完成转换并不复杂。

不在乎组件扩展?

我与之交谈过的一些 C++ 开发人员表示,他们无法忍受 C++/CX 的语法,并且认为它不是真正的 C++。好吧,如果您不想使用 C++/CX 语法,则不必使用它。您可以直接使用常规的 C++/COM 来编写 WinRT 应用程序。您可以将 C++/CX 视为高级 WinRT 访问,而常规 COM 则是低级访问。但这将是一项相当大的工作才能使其正常工作。您将不得不处理编译器在高层模式下为您处理的所有事情,例如创建激活工厂、实现 IUnknownIInspectable。手动处理 HSTRING。确保正确处理引用计数。执行 QueryInterface 部分将是您最不担心的问题。最后,由于 COM 实际上并不支持 C++ 方式的继承,WinRT 使用一种组合形式来模拟继承。因此,当您有多层对象模型时,您最终会编写更多的代码才能使其编译和运行。除了学术练习之外,让任何开发人员承受这种极端情况都没有意义。

不过,还有一个选择。它被称为 Windows Runtime Library(或简称 WRL)。目前 MSDN(或其他任何地方)上的文档非常少(实际上为零)。它基本上是 WinRT 的 ATL 等价物。WRL 似乎属于 SDK 团队而不是 VC++ 团队,并且您会在以下位置(在开发者预览版中)找到头文件:Program Files (x86)\Windows Kits\8.0\Include\winrt\wrl。我猜至少部分 WinRT 是使用 WRL 开发的,因此到 RTM 版本时它应该相当没有 bug。所以,如果您绝对想避免 C++/CX,那么 WRL 应该是您的最佳选择(直接 COM 绝对是不切实际的)。

结论

如果您是 C++ 程序员,或者过去曾是 C++ 程序员但转到 C# 或 VB 以更好地使用 WPF 和 Silverlight 等现代框架,那么现在是激动人心的时刻。因为现在,C++ 再次成为编写 Windows 应用程序(至少是 Windows Metro 应用程序)的首选语言。我并不是说没有理由使用 C# 或 VB。对于快速应用程序开发和更好的 IDE 支持,这些语言可能占有优势,特别是当您考虑到 .NET 的所有优势和庞大的基础类库时。但对于具有更小内存占用、性能是关键焦点的更快的应用程序,使用 C++ 编写 Metro 应用是可行的方法,因为当您这样做时,就是“金属对金属”!复兴终于到来了。

历史

  • 2011 年 9 月 29 日 - 文章首次发布
© . All rights reserved.