自从Win8引入WinRT以来,主流开发者的说法一直是,C++/CX可以方便地把现有C++代码移植到WinRT。一般来说确实如此,绝大部分原有C++代码可以的保留,只需要写UI的部分就可以了。但因为C++/CX的引入,还是会带来一些副作用,不完全都是好处。本篇将以KlayGE为例,讲解如何在不用C++/CX的情况下完成WinRT应用(包括UWP)。
C++/CX的功与过
C++/CX使用起来挺方便,语法和原先的C++/CLR极其相似,但是native的,没有虚拟机执行开销。任何懂C++的人都可以很快上手,把程序移植到WinRT。同时,WinRT背后一大套COM机制都被隐藏掉了,可以直接用普通的类一样使用COM对象。
但是,仔细观察就能发现,在C++项目中加入C++/CX有几个缺点。
- C++/CX和标准C++代码交互起来有点麻烦。比如UI如果用C++/CX写成,那么消息响应部分也必须用C++/CX包一层,才能转到标准C++的事件响应代码。
- C++/CX的类构造函数不能有参数。
- 编译出来的二进制文件比标准C++大。即便同一份代码,打开/ZW和不打开,二进制最大的能差到20%。
- 性能略微下降。经过CX包一层,每一次调用性能都有影响。
- 只能用vc编译。MinGW和clang都不支持C++/CX的扩展。
很多C++的项目,代码都是存在了很长时间了,现在才加入一点点C++/CX的部分用以移植到WinRT。在这种情况下,还有更好的选择吗?
WRL呢
微软给的另一个选项是WRL。作为一个类似于ATL的模板库,它从库的层面,而不是语言层面,把WinRT的细节包装起来。这样一来,调用开销可以在编译阶段都内联掉,并通过优化器去掉没必要的代码。因为整体代码仍然是标准C++,构造函数不再有限制,也能直接和标准C++相互调用。另外,理论上WRL也可以用MinGW和clang进行编译,虽然我也没试过效果如何。
换句话说,前面说的5个C++/CX的缺点,前4个都可以用WRL解决,第5个很可能可以用WRL解决。那么,剩下的问题就是,WRL能完全代替C++/CX吗?在所有的公开资料中,WRL都被声明为用于构建组件dll的方法,而不是应用的exe。看来这个只能自己探索了。
WRL如何替代C++/CX
这里将会列出一些常用的C++/CX如何用WRL代替。
类名
系统类名XXX替换成ABI::XXX。比如Windows::UI::Core::CoreWindow替换成ABI::Windows::UI::Core::ICoreWindow。不用C++/CX的话,系统定义的类都在ABI namespace下,并且接口名需要I前缀。
指针
Platform::Agile<T>替换成std::shared_ptr<ABI::IT>。对,用WRL::ComPtr或者std::shared_ptr都可以。
对象建立
p = ref new T替换成hr = Windows::Foundation::ActivateInstance(HStringReference(RuntimeClass_T).Get(), &p)。ActivateInstance是个内建函数,专门用来建立一个WinRT对象。对象名是个HStringReference,参数为一个字符串。具体要用什么字符串可以从SDK里查到。
属性
WinRT里的属性,其实是通过调用函数实现的,因为COM只有函数。如果属性名是Attr,那么获取的函数名就是get_Attr(),设置的是put_Attr()。原先不加限制地获取属性和设置属性,现在变成函数调用后,就能显式地精确控制,而防止频繁调用造成的性能损失。
静态函数调用
WinRT里有不少静态函数调用,比如DisplayInformation::GetForCurrentView()。而COM是没有静态函数的。这里其实是C++/CX耍的一个间接调用。不用C++/CX的话,这个过程是这样的:
ComPtr<IDisplayInformationStatics> disp_info_stat; hr = GetActivationFactory(HStringReference(RuntimeClass_Windows_Graphics_Display_DisplayInformation).Get(), &disp_info_stat); ComPtr<IDisplayInformation> disp_info; hr = disp_info_stat->GetForCurrentView(&disp_info);
也就是说,首先建立一个带Statics后缀的类,在它上面调用函数,等价于C++/CX里的静态函数调用。
事件
C++/CX里的事件只要event+=func,就能把func加到event的回调队列里面。比如,响应stereo状态变化的事件,C++/CX的代码如下:
stereo_enabled_changed_token_ = DisplayInformation::GetForCurrentView()->StereoEnabledChanged += ref new TypedEventHandler<DisplayInformation^, Platform::Object^>(metro_d3d_render_win_, &MetroD3D11RenderWindow::OnStereoEnabledChanged);
用WRL需要这么做:
auto callback = Callback<ITypedEventHandler<DisplayInformation*, IInspectable*>>( std::bind(&D3D11RenderWindow::OnStereoEnabledChanged, this, std::placeholders::_1, std::placeholders::_2)); disp_info->add_StereoEnabledChanged(callback.Get(), &stereo_enabled_changed_token_);
结合C++11和WRL,实现事件也不麻烦。Event+=变成了add_Event。同理,Event-=token变成remove_Event(token)。
程序框架
ref class MetroFramework sealed : public Windows::ApplicationModel::Core::IFrameworkView { ... } ref class MetroFrameworkSource sealed : Windows::ApplicationModel::Core::IFrameworkViewSource { public: virtual Windows::ApplicationModel::Core::IFrameworkView^ CreateView(); private: void BindAppFramework(App3DFramework* app); ... }; MetroFrameworkSource^ metro_app = ref new MetroFrameworkSource; metro_app->BindAppFramework(this); CoreApplication::Run(metro_app);
变成
class MetroFramework : public RuntimeClass<ABI::Windows::ApplicationModel::Core::IFrameworkView> { public: InspectableClass(L"KlayGE.MetroFramework", BaseTrust); public: HRESULT RuntimeClassInitialize(App3DFramework* app); ... } class MetroFrameworkSource : public RuntimeClass<ABI::Windows::ApplicationModel::Core::IFrameworkViewSource, FtmBase> { public: InspectableClass(L"KlayGE.MetroFrameworkSource", BaseTrust); public: HRESULT RuntimeClassInitialize(App3DFramework* app); IFACEMETHOD(CreateView)(ABI::Windows::ApplicationModel::Core::IFrameworkView** framework_view); ... }; ComPtr<ICoreApplication> core_app; hr = GetActivationFactory(HStringReference(RuntimeClass_Windows_ApplicationModel_Core_CoreApplication).Get(), &core_app); ComPtr<MetroFrameworkSource> metro_app; MakeAndInitialize<MetroFrameworkSource>(&metro_app, this); hr = core_app->Run(metro_app.Get());
利用WRL提供的RuntimeClass和MakeAndInitialize,代码并不比C++/CX复杂。
入口
WinRT的函数入口是
[Platform::MTAThread] int main(Platform::Array<Platform::String^>^ args) { ... }
而如果用WRL,只要用标准C++的main就可以了!不需要那些修饰。
好了,知道了这些规则后,基本就能把C++/CX都用WRL替换了。并且可以发现,WRL并不限于组件,应用也完全没问题。
替换效果
用WRL替换KlayGE中的C++/CX之后,代码变短了,更直观易维护。并且直接可见二进制文件缩小了。
C++/CX(字节) | WRL(字节) | 变化 | |
Core | 2,648,064 | 2,544,128 | -3.92% |
D3D11 | 375,808 | 334,336 | -11.04% |
D3D12 | 399,872 | 373,760 | -6.53% |
MsgInput | 94,208 | 93,184 | -1.09% |
注意,绝大部分代码都是标准C++,只有几十行是C++/CX。即便如此,都已经有1-11%的改善。对于使用C++/CX面积大的项目来说,改善可以更可观。
更进一步?
实际上在KlayGE中,就连WRL都用的很少。实际上可以把WRL也去掉,就用最原始的COM调用完成所有这些事情,这样能避免template膨胀,进一步减少二进制大小。这是下一步需要探索的事情。
当然,这里提到的是KlayGE这种绝大部分标准C++,少部分C++/CX的情况。对于大量使用C++/CX的项目(真的有?),这样的替换还是非常麻烦的。所以,这里需要的是另一条路子,一个叫Modern C++的库。作者Kenny Kerr原先是MSDN杂志的编辑和Microsoft MVP,上个月加入了微软。Modern C++包含一个编译器,可以把WinRT的meta data编译成个纯头文件的库。之后就能直接用标准C++写WinRT应用。不用CX的语法,不用WRL的RuntimeClass和各种模板,不用HStringReference。并且,它可以用于MinGW和clang。也就是说,前面提到的5个问题全都一下解决了。
Comments