Windows上的c++/winrt
在Windows 1803上微软带来了c++/winrt
既是基于Windows Runtime上的C++,而且是标准C++,不再是之前微软搞的C++/CLI
和C++/CX
两个变种。
亮眼之处
有几个吸引人的地方。本质是COM却超越了MFC/ATL,COM的接口概念仍然存在,但是整合成了更加方便使用的形态。也没有了Win32 API中繁琐的调用方式,比ATL用起来也更加简单。我认为完全就是MFC/ATL的替代品。
宽字符和UTF8字符之间的转换太方便了,使用winrt::to_string
和winrt::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
。