Marvin's Blog【程式人生】

Ability will never catch up with the demand for it

17 Dec 2019

C++/WinRT学习笔记(六):接口的录用和作成

Consume APIs with C++/WinRT

C++/WinRT实现的是对winmd的投射。Windows SDK自带的ABI头文件会被投射成C++/WinRT的头文件。比如,ABI的Windows::Foundation::Uri会被投射成winrt::Windows::Foundation::Uri。投射生成的C++文件也包含在SDK中,在目录:%WindowsSdkDir%Include<WindowsTargetPlatformVersion>\cppwinrt\winrt\

C++/WinRT的投射实现方式采用的是引用计数方式的智能指针。实际的实例在堆上分配(通过RoActivateInstance),但是管理实例的智能指针在栈上分配。在入栈出栈的过程会自动释放智能指针并减少引用,当引用降低为0的时候,释放堆上的实例。所以,投射的类型也可以算是一种代理(Proxy)。

C++/WinRT投射的头文件按照命名空间组织,winrt/Windows.Security.Cryptography.Certificates.h。次一级命名空间的头文件会包含上一级命名空间的头文件。

在投射类型上可以直接调用该接口所实现的方法:

Uri contosoUri{ L"http://www.contoso.com" };
contosoUri.ToString() 

ToString()是属于IStringable的方法,所以具体的调用过程是,先将Uri通过QueryInterface获取其IStringable的接口,然后调用其ToString()方法。为了避免每次都涉及QueryInterface,可以先将Uri转化为IStringable:

IStringable stringable = contosoUri; // One-off QueryInterface.
stringable.ToString();

也可以直接在ABI级别调用方法:

   winrt::com_ptr<ABI::Windows::Foundation::IUriRuntimeClass> abiUri{
        contosoUri.as<ABI::Windows::Foundation::IUriRuntimeClass>() };
    HRESULT hr = abiUri->get_Port(&port); // Access the get_Port ABI function.

投射的类型在栈上创建之后,会接连创建在堆上的实例。有一个办法可以避免创建堆上的实例,那就是使用nullptr为参数来构造这个投射类型:

    Buffer m_gamerPicBuffer{ nullptr };
    m_gamerPicBuffer = Buffer(MAX_IMAGE_SIZE);

值得注意的是,默认构造函数也会创建相应的堆上的实例,这个问题在How the default constructor affects collections.里面有所解释。

nullptr的构造函数有可能导致误用,请看:

// These are *not* what you intended. Doing it in one of these two ways
// actually *doesn't* create the intended backing Windows Runtime Gift object;
// only an empty smart pointer.

Gift gift{ nullptr };
auto gift{ Gift(nullptr) };

正确的做法是:

// Doing it in one of these two ways creates an initialized
// Gift with an uninitialized GiftBox.

Gift gift{ GiftBox{ nullptr } };
auto gift{ Gift(GiftBox{ nullptr }) };

对于拷贝构造,C++/WinRT会复制一份智能指针,使两份智能指针指向相同的实例。如果这不是你想要的,比如你想要拷贝的是实例本身,那么就需要显示调用实例工厂来创建实例:

GiftBox bigBox{ ... };

// These two ways call the activation factory explicitly.

GiftBox smallBox{
    winrt::get_activation_factory<GiftBox, IGiftBoxFactory>().CreateInstance(bigBox) };
auto smallBox{
    winrt::get_activation_factory<GiftBox, IGiftBoxFactory>().CreateInstance(bigBox) };

如果你的项目引用了一个WinRT组件,那么cppwinrt.exe会生成那个组件的投射类型,并将这些生成的类型囊括到本项目中。项目启动的时候会注册这个引用了的WinRT组件,在使用这个组件的投射类型的时候,会自动在构造函数中调用RoActivateInstance 来创建这个组件的实例。

在XAML中使用的类型,必须是一个runtimeclass,即便这个runtimeclass和使用它的XAML在同一个项目中。使用同一项目的runtimeclass,不需要注册和激活过程,可以直接使用winrt::make来初始化:

Bookstore::BookstoreViewModel m_mainViewModel = winrt::make<Bookstore::implementation::BookstoreViewModel>();

下面是一个投射类型的例子:

struct MyRuntimeClass : MyProject::IMyRuntimeClass, impl::require<MyRuntimeClass,
    Windows::Foundation::IStringable, Windows::Foundation::IClosable>

你可通过下面的方式实例化投射类型:

// The runtime class is implemented in another compilation unit (it's either a Windows API,
// or it's implemented in a second- or third-party component).
MyProject::MyRuntimeClass myrc1;

// The runtime class is implemented in the same compilation unit.
MyProject::MyRuntimeClass myrc2{ nullptr };
myrc2 = winrt::make<MyProject::implementation::MyRuntimeClass>();

如果你知道投射类型的具体实现,可以使用winrt::make方式进行延迟初始化。

另外,投射类型都是从winrt::Windows::Foundation::IUnknown派生出来的,可以使用IUnknown::as 来QueryInterface:

    myrc.ToString();
    myrc.Close();
    IClosable iclosable = myrc.as<IClosable>();
    iclosable.Close();

投射类型私底下是通过工厂类来实例化具体的实例,你可以直接通过winrt::get_activation_factory来进行相同的操作:

using namespace winrt::Windows::Foundation;
...
auto factory = winrt::get_activation_factory<Uri, IUriRuntimeClassFactory>();
Uri account = factory.CreateUri(L"http://www.contoso.com");

或者对于WinRT组件:

auto factory = winrt::get_activation_factory<BankAccountWRC::BankAccount>();
BankAccountWRC::BankAccount account = factory.ActivateInstance<BankAccountWRC::BankAccount>();

文章中还介绍了一个变量名和类型冲突的例子,可能会导致名字查找失败,文中给出了集中解决办法。

此外,有个特例:

Unqualified name lookup has a special exception in the case that the name is followed by ::, in which case it ignores functions, variables, and enum values. This allows you to do things like this.

    void DoSomething()
    {
        Visibility(Visibility::Collapsed); // No ambiguity here (special exception).
    }

Author APIs with C++/WinRT

如果不是为了实现一个runtimeclass,只是为了实现一个接口,winrt::implements可以直接用来对WinRT接口提供实现。当然,如果是为了实现一个runtimeclass,那么生成的代码中间接使用了winrt::implements

一个只实现接口的例子:

// App.cpp
...
struct App : implements<App, IFrameworkViewSource, IFrameworkView>
{
    IFrameworkView CreateView()
    {
        return *this;
    }

    void Initialize(CoreApplicationView const &) {}

    void Load(hstring const&) {}

    void Run()
    {
        CoreWindow window = CoreWindow::GetForCurrentThread();
        window.Activate();

        CoreDispatcher dispatcher = window.Dispatcher();
        dispatcher.ProcessEvents(CoreProcessEventsOption::ProcessUntilQuit);
    }

    void SetWindow(CoreWindow const & window)
    {
        // Prepare app visuals here
    }

    void Uninitialize() {}
};

using namespace Windows::ApplicationModel::Core;
int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
    CoreApplication::Run(winrt::make<App>());
}

上面的代码让App实现了IFramworkViewSource以及IFrameworkView接口

如果是为了实现一个runtimeclass,则需要在idl中定义这个runtimeclass。

Note the F-bound polymorphism pattern being used (MyRuntimeClass uses itself as a template argument to its base, MyRuntimeClassT). This is also called the curiously recurring template pattern (CRTP)

如果是为XAML实现的IDL,基本创作流程和WinRT组件一样,唯一区别的是,C++/WinRT会在项目中同时生成实现类型和投射类型。

Visual Studio的C++/WinRT项目会生成分散的IDL,这会增加项目的构建时间。一个处理的办法是将所有的IDL合并成一个大的IDL。

在同一编译单元内的映射类和作成类,可以不在IDL中定义构造函数(也不需要作成工厂),可以使用分步初始化,并且可以使用自定义的作成类的构造函数。先用nullptr构建映射类中,然后用自定义的构造方法构造作成类,然后将作成实例赋值给映射指针。

一个有趣的点,作成类中的方法的签名,不需要完全跟映射类一致。只需要能够转化成映射类中的方法的签名即可:

  • 如果映射类方法使用的参数类型是IInspectable,那么作成类中可以使用任意可以转化为IInspectable的类型
  • 可以把传值改成传引用,比如把SomeClass改成const SomeClass&,因为一个值可以被当作一个引用传递,反之则不行。注意,在使用Coroutine的时候,传值比传引用要安全,因为能够确保输入参数(通过传值增加引用计数)的生命周期。
  • void返回类型可以被替换成winrt::fire_and_forget。注意,你不能对于IDL中声明的delegate这么做。

一个runtimeclass可能实现了若干个接口,可以直接用winrt::make来赋值给想要的接口:IStringable istringable = winrt::make<MyType>();。对于映射类和作成类在同一个编译单元的情况下(用于XAML),winrt::make返回的是映射类的实例

winrt::make_self可以创建一个到作成类的智能指针,winrt::get_self可以从映射类从反推出作成类的实例,并返回其智能指针。注意的是,get_self并不会增加引用计数,需要显示调用copy_from:

winrt::com_ptr<MyType> impl;
impl.copy_from(winrt::get_self<MyType>(from));
// com_ptr::copy_from ensures that AddRef is called.

存在一个从作成类到映射类的转化,所以可以把作成类的*this传给一个映射类型的参数,转化会自动进行:

void FreeFunction(MyProject::MyOtherClass const& oc)
{
    auto defaultInterface = winrt::make<MyProject::implementation::MyClass>();
    MyProject::implementation::MyClass* myimpl = winrt::get_self<MyProject::implementation::MyClass>(defaultInterface);
    oc.DoWork(*myimpl);
}

https://docs.microsoft.com/en-us/windows/uwp/cpp-and-winrt-apis/author-apis#deriving-from-a-type-that-has-a-non-default-constructor

有一些WinRT类型没有默认的构造函数,比如ToggleButtonAutomationPeer::ToggleButtonAutomationPeer(ToggleButton) 。这要求你在写作成类的时候传入必要的参数:

// MySpecializedToggleButtonAutomationPeer.cpp
...
MySpecializedToggleButtonAutomationPeer::MySpecializedToggleButtonAutomationPeer
    (MyNamespace::MySpecializedToggleButton const& owner) : 
    MySpecializedToggleButtonAutomationPeerT<MySpecializedToggleButtonAutomationPeer>(owner)
{
    ...
}
...

否则编译器会提示在MySpecializedToggleButtonAutomationPeer_base<MySpecializedToggleButtonAutomationPeer>上没有可用的构造函数。

用一个类型名在不同的命名空间有不同的意义:

  • winrt::MyProject,映射类型
  • winrt::MyProject::implementation,作成类型,可以使用winrt::make来实例化
  • winrt::MyProject::factory_implementation,工厂类,支持IActivationFactory接口

映射类型和作成类型在不同场景中的应用表现:

  • T (只能指针),映射类型
  • agile_ref<T>, 映射类型和作成类型,如果是作成类型,那么参数必须是com_ptr
  • com_ptr<T>,作成类型,如果用在映射类型上会显示:'Release' is not a member of 'T'
  • default_interface<T>,映射类型和作成类型,返回作成类型实现的第一个接口
  • get_self<T>,作成类型,否则返回错误:'_abi_TrustLevel': is not a member of 'T'
  • guid_of<T>(),两者皆可,返回GUID
  • IWinRTTemplateInterface<T>,映射类型,虽然使用作成类型可以编译,但是这是错误的
  • make<T>,作成类型,如果用在映射类型上,会出现:'implements_type': is not a member of any direct or indirect base class of 'T'
  • make_agile(T const&amp;),作成类型,在映射类型上会出现:'Release': is not a member of any direct or indirect base class of 'T'
  • name_of<T>,映射类型,返回字符串类型的GUID
  • weak_ref<T>,两者皆可,在作成类型上,参数必须是com_ptr<T>

对于WinRT组件,为了能够让 RoGetActivationFactory 调用成功,组件需要实现DllGetActivationFactory ,作为DLL的入口点。C++/WinRT对作成类工厂有提供缓存机制,会组织DLL卸载。

cppwinrt.exe提供了一个-opt[imize]选项,可以用来在作成类可知的情况下避免调用RoGetActivationFactory。所以下面的代码:

MyClass c;
c.Method();
MyClass::StaticMethod();

有可能通过RoGetActivationFactory,也有可能直接实例化作成类,这个叫做Uniform Construction。

为了使用Uniform Construction,需要在映射WinRT组件的时候使用-component-opt[imize]选项,这会导致生成类似MyClass.g.cpp的代码,你需要把这个文件包含在作成类的实现中:

#include "pch.h"
#include "MyClass.h"
#include "MyClass.g.cpp" // !!It's important that you add this line!!
 
namespace winrt::MyProject::implementation
{
    void MyClass::StaticMethod()
    {
    }
 
    void MyClass::Method()
    {
    }
}

MyClass.g.cpp包含了映射类中没有实现的代码:

namespace winrt::MyProject
{
    MyClass::MyClass() :
        MyClass(make<MyProject::implementation::MyClass>())
    {
    }
    void MyClass::StaticMethod()
    {
        return MyProject::implementation::MyClass::StaticMethod();
    }
}

-opt[imize]选项还改进了module.g.cpp的生成,使其更简洁。

另一个问题是关于继承的,在Derived Class这个例子中有:

Windows::UI::Xaml::Controls::Page <- BasePage <- DerivedPage.

因为C++/WinRT不要求BasePage 的接口实现采用虚函数:

    struct BasePage : BasePageT<BasePage>
    {
        void OnNavigatedFrom(Windows::UI::Xaml::Navigation::NavigationEventArgs const& e);
    };

    struct DerivedPage : DerivedPageT<DerivedPage>
    {
        void OnNavigatedFrom(Windows::UI::Xaml::Navigation::NavigationEventArgs const& e);
    };

上面的例子中DerivedPage的IPageOverrides 虚指针来自BasePage,DerivedPage::OnNavigatedFrom不会覆盖BasePage::OnNavigatedFrom。

解决办法是把BasePage::OnNavigatedFrom声明为虚函数,这样DerivedPage::OnNavigatedFrom也可以出现在虚函数列表中了,会被正确调用:

namespace winrt::MyNamespace::implementation
{
    struct BasePage : BasePageT<BasePage>
    {
        // Note the `virtual` keyword here.
        virtual void OnNavigatedFrom(Windows::UI::Xaml::Navigation::NavigationEventArgs const& e);
    };

    struct DerivedPage : DerivedPageT<DerivedPage>
    {
        void OnNavigatedFrom(Windows::UI::Xaml::Navigation::NavigationEventArgs const& e);
    };
}

(本篇完)

2020-05-20 Diagnosing direct allocations

WinRT的作成类(implementation type)必须使用winrt::make或者winrt::make_self来实例化,而不是直接调用其构造函数。作成类的引用可以转化投射类,但是作成类的指针不能转化为投射类。

从C++/WinRT 2.0开始,直接初始化作成类造成编译器错误,会提示use_make_function_to_create_this_object。但是为了做到这个,winrt::make必须添加一些把戏。

首先winrt::make不会直接生成作成类,而是先生成一个作成类的派生类,然后再实例化派生类。这个派生类会被标记成final,意味着其不能再被派生。因此,为了避免冲突,不要把作成类的虚函数声明为final,然后不要把作成类的析构函数声明为私有的。

(更新完)

comments powered by Disqus