转载请注明出处为KlayGE游戏引擎,本文的永久链接为http://www.klayge.org/?p=3426

自从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有几个缺点。

  1. C++/CX和标准C++代码交互起来有点麻烦。比如UI如果用C++/CX写成,那么消息响应部分也必须用C++/CX包一层,才能转到标准C++的事件响应代码。
  2. C++/CX的类构造函数不能有参数。
  3. 编译出来的二进制文件比标准C++大。即便同一份代码,打开/ZW和不打开,二进制最大的能差到20%。
  4. 性能略微下降。经过CX包一层,每一次调用性能都有影响。
  5. 只能用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个问题全都一下解决了。