发表于

Windows上的c++/winrt

在Windows 1803上微软带来了c++/winrt既是基于Windows Runtime上的C++,而且是标准C++,不再是之前微软搞的C++/CLIC++/CX两个变种。

亮眼之处

有几个吸引人的地方。本质是COM却超越了MFC/ATL,COM的接口概念仍然存在,但是整合成了更加方便使用的形态。也没有了Win32 API中繁琐的调用方式,比ATL用起来也更加简单。我认为完全就是MFC/ATL的替代品。

宽字符和UTF8字符之间的转换太方便了,使用winrt::to_stringwinrt::to_wstring即可转换,其内部实现本质还是调用的Win32 API中的哪些名称特长的API。

引入异步函数和多线程的理念,大量的异步函数方便使用。

最好的库都是第一方平台提供的。如果自己开发的库有一大堆的第三方依赖,是一件很让人头疼的事情。因为第三方库的接口可能更改,可能会有很多的bug。而对于财大气粗的微软来说,请了全球这么多高手维护Windows自家的库,质量上有保证不会有这么多bug,而且万一你有问题也可以找微软技术支持,第三方库可就没有这么好事情了。所以我一直非常信赖大公司的靠钱堆集起来的库。

  • Windows.Web.Http用于HTTP通信
  • Windows.Networking.Sockets用于TCP/UDP通信
  • Windows.Data.Json用于解析JSON格式
  • Windows.Devices空间下可以连接蓝牙,WIFI等设备的类
  • Windows.Graphics空间可以直接调用D3D接口

除此之外最近几年微软出的C++库都是基于C++/WinRT实现的。

场景

主要有三种场景

  • consume api
  • author api
  • xaml app

其中xaml开发会同时涉及到consume和author的情况。

常用工具

  • midl将idl文件生成winmd文件
  • mdmerge将多个winmd文件生成单个winmd文件
  • cppwinrt将winmd文件生成完整的c++类文件

所有的winmd文件都在C:/Windows/System32/WinMetaData中,可以使用ildasm工具查看。

WinRT

Consume API

所有的C++/winrt的投影类型,本质上都是代理了WinRT上的对象,通过一个m_ptr指针来实现的,因此投影类型本质上可以看成是智能指针,引用计数归零时会把真正的实现类型析构掉。

投影类型都有个特殊的构造函数,只有唯一的成员std::nullptr_t类型,通过该构造函数可以延迟初始化,也就是构造的时候,本质上m_ptr并没有指向任何的WinRT对象。延迟初始化的对象,要通过winrt::make来初始化。

using namespace winrt::Windows::Foundation;

Uri myuri{nullptr};

因为投影类型都是继承自winrt::Windows::Foundation::IUnknown类型,可以用IUnknown::as来获得接口,类似于QueryInterface操作。

由于投影类型是智能指针实现的,有两个重要的知识点

  • 单参数构造时,如果该参数也是投影类型且延迟初始化,会导致对象构造有潜在问题;
  • 浅拷贝导致两个投影类型对象指向了同一个WinRT对象,需要用winrt::get_activation_factory来实现深拷贝

Consume API分三种情况

  • 系统的ABI
  • WinRT组件
  • 第三方库

如果是第三方库,因为即使构造了智能指针的投影类型,但是类型本身并未实例化,因此需要用一种方法初始化。在C++/WinRT的v1.0版本中使用winrt::make来实现,在v2.0版本中取消了先延迟初始化再winrt::make的限制。这和std::make_shared的原理是类似的。系统ABI和WinRT组件是在应用初始化的时候,已经通过COM接口初始化了,所以不需要用winrt::make方法。

winrt::get_activation_factory类似于工厂方法,对于ABI和组件来说需要传递接口类作为参数,第三方组件则不需要

using namespace winrt::Windows::Foundation;

auto uri_factory = winrt::get_activation_factory<Uri, IUriRuntimeClassFactory>();
Uri myuri = uri_factory.CreateUri(L"HelloWorld");

auto tm_factory = winrt::get_activation_factory<Thermometer::Themometer>();
auto tm = tm_factory.ActivateInstance<Thermometer::Themometer>();

这种实现太麻烦了,我们通常还是通过延迟构造或者直接构造来实现。

Author API

当创建普通C++类型,但是想利用C++/WinRT的功能时,可以直接继承自winrt::implements来实现。只有创建WinRT类时,需要通过IDL语言和MIDL工具来生成。

通过winrt::implements类模板,加上需要的接口类作为参数生产实现类。这里winrt::implements是支持CRTP的。

struct App: winrt::implements<App, IFrameworkViewSource> {
  …
}

CoreApplication::Run(winrt::make<App>());

因为App是普通C++类型,因此需要winrt::make来创建。

该类的对象可以通过winrt::make_self获取到winrt::com_ptr对象,该对象可以通过解引用操作调用所有的成员函数,而不需要先获得对应的接口对象。还有一个winrt::get_self获得的是该类对象的裸指针。

winrt::com_ptr<MyType> myimpl_com = winrt::make_self<MyType>();

MyType* myimpl_ptr = winrt::get_self<MyType>(myimpl_com);

获得裸指针的好处是不需要跨越ABI边界,提高效率。

注意

引起重视的问题

  • 命名空间
  • 生存期

注意命名空间,在写代码的时候先确定自己的命名空间,一般都是用投影空间,这样用实现的时候通过implementation::MyType就可以了。

  • winrt::MyProject
  • winrt::MyProject::implementation
  • winrt::MyProject::factory_implementation

命名空间碰撞问题,如果要用COM里的IUnkown则使用::IUnknown,如果是WinRT的则使用winrt::IUnknown

Uniform Construction的意义在于,无论是使用了投影空间还是实现空间,代码方式都是相同的。

using winrt::MyProject;

// MyType c{ winrt::make<implementation::MyType>()}
MyType c;

Uniform Construction开启方式是给cppwinrt.exe传入-optimize参数。这样在MyType.cpp中会有这样一句话

#include "MyType.g.cpp"   

Windows.ApplicationModel.Core.h是个很重要的头文件,C++/WinRT应用本质上都是基于CoreApplication类来实现的。

对于继承自winrt::implements的普通C++类型来说,离开生存期就会被析构,那么对应的投影类型对象就不能使用了。

链接

需要链接到WindowsApp.lib才能实现winrt功能。除了在链接选项中指定链接外,还能通过在pch.h文件中加入

#pragma comment(lib, "windowsapp")

来实现链接。

COM

Consume COM

构造未出初始化的winrt指针可以用winrt::com_ptr。

如果需要初始化则要调用对应的工厂函数,同时还需要使用COM函数获得不同接口

  • winrt::put_void
  • winrt::put

这两个函数应该是返回指针的指针,传入COM工厂函数中对应形参是指针的引用,从而使得com_ptr能够被初始化。

winrt::com_ptr重置之前要设置为nullptr才行,执行void operator = (nullptr_t)

这里winrt::com_ptr有很多成员

  • winrt::get
  • winrt::get_unknown
  • winrt::as
  • winrt::try_as

Author COM

COM组件和WinRT组件并不是相同的,只是通过C++/WinRT我们可以创建COM组件。

要让winrt::implements支持创建COM接口,则需要引入unknwn.h头文件。有一种方式是使用Windows Implementation Libraries,通过添加头文件wil\cppwinrt.h来实现。

其他

Boxing/Unboxing

IInspectable是WinRT类的标准接口,就像IUnknown是COM类的标准接口一样。如果一个函数的形参是IInspectable,那么传递任何WinRT类的对象都是可以的,但是对于字面值等则需要Boxing操作转化为一个WinRT类的对象。

  • winrt::box_value
  • winrt::unbox_value
  • winrt::unbox_value_or

引用

winrt::implements模板类保护函数

  • get_strong,可以把引用计数增加
  • get_weak,可以把引用计数减少

主要发生在Coroutine和Delegate情况下,有些WinRT类对象的引用计数被减少情况下,需要使用winrt::implements的引用计数功能,保证不会被析构掉。

如果要使用引用计数功能,则需要继承接口IWeakReferenceSource,这是默认会继承的。如果不想使用弱引用计数功能,那么则需要传递模板参数winrt::no_weak_ref即可。

  • winrt::weak_ref
  • winrt::make_weak

这种设计方法和std::weak_ptr很像。

agile

agile对象意味着可以被多个线程同时使用。如果对象不是agile的,那么需要考虑线程模型和marshaling行为。

winrt::implements默认是继承了IAgileObject和IMarshal接口的。可以用try_as来确认对象是否继承了IAgileObject接口。可以添加模板参数winrt::non_agile来成为非agile对象。

对于非agile对象,如果想要临时有该属性,则可以通过winrt::agile_ref或winrt::make_agile来实现。

事件

多线程

异步

构建过程

midl

似乎官方文档也没有讲清楚,我从网站搜索得到的调用方法如下

midl /winrt /nomidl /metadata_dir <\path\to\sdk> /reference <path\to\winmd\file>

这里的winmd文件通常使用"%WindowsSdkDir%References\<sdk_version>\..."来找到winmd文件。

cppwinrt

工具使用比较简单

cppwinrt -optimize -reference 10.0.19041.0 -input <path\to\winmd\file>

如果是生成WinRT组件,那么要加上

cppwinrt -optimize -reference 10.0.19041.0 -input <path\to\winmd\file> -component <path\to\save\component>

cmake/msbuild

如果是WinRT组件的话,用上述命令行工具生成后,还需要自己写CMakeLists.txt文件把cpp文件编译一下。可以尝试用cmake写个模块,把整个WinRT的构建过程封装起来。

如果是非WinRT组件的话,从网络上搜索来看xaml文件的编译是msbuild编译的,但是编译命令似乎没有找到,因此用sln文件和vcproj文件来组织代码似乎成了唯一方法,用cmake似乎不太可行。

<Midl Include="xxx.idl">
  <DependentUpon>xxx.xaml</DependentUpon>
</MidlL>

这是vcproj文件中的声明idl文件和xaml文件的关系,这样能实现编译过程。

程序类型

CoreApplication

C++/WinRT里定义了一个CoreApplication的类型,这个是UWP的基础类型,代表了一个应用程序。

WinUI

这是从UWP中剥离出来的UI相关的库,微软定义是未来最主要的UI库。基础类型包括

  • AppWindow
  • AppWindowPresenter: OverlappedPresenter/FullScreenPresenter/CompactOverlayPresenter

当AppWindowPresenter发生改变时,会触发事件AppWindow的Changed事件,该事件的参数是DidPresenterChange。

有一个非常简单的例子可以参考。

部署

在Linux上开发很少由部署相关的问题,最多就是ldd查询依赖。但是在Windows上还有安全机制的问题,需要考虑到证书。即使只是在本机开发,也需要证书。

首先需要在设置中开启开发者模式,然后创建一个开发者证书,可以参考官方文档,需要注意的是Publisher是个很重要的参数。

证书导出成pfx文件后,还需要在项目文件中指定

  • AppxPackageSigningEnabled设置为true
  • PackageCertificateKeyFile设置pfx文件路径

这样在编译过程中,msbuild会帮我们对msix文件进行证书验证。当然如果不想用命令行自己生成,可以用Visual Studio里的工具生成。总的来说,微软对于命令行的支持真的很一般啊,就是想要让大家去学习msbuild整套构建过程。

微软官方库

WindowsApp SDK

由一些基础功能组成

  • MRT Core API用于资源管理,提供了ResourceLoader/ResourceManager/ResourceContext三种方式
  • DWriteCore是DirectWrite的重写版本,主要支持了文字渲染
  • Windowing API,其实就是WinUI,主要包括了AppWindow/AppWindowPresenter类,容器化方式同时支持UWP和Win32应用
  • AppLifecycle API主要用于是否支持多个应用实例

WinApp的运行时架构很有意思,所有的App共享同一套WinApp运行时,而且支持运行时更新。只有当没有任何App使用老的运行时,这时候才会被删除。

CppWin32

win32也实现了投影类型,有个cppwin32的库。相关的引用查找在 Windows Runtime C++(Win32) Reference

Windows Implementation Library

wil库似乎是一个帮助库,比如可以支持创作COM组件。

资料

C++/WinRT的创建者Kenny Kerr有自己的博客,有个一个系列的文章很不错,叫做Meet C++/WinRT 2.0