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);
}
有一些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_ptrcom_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>()
,两者皆可,返回GUIDIWinRTTemplateInterface<T>
,映射类型,虽然使用作成类型可以编译,但是这是错误的make<T>
,作成类型,如果用在映射类型上,会出现:'implements_type': is not a member of any direct or indirect base class of 'T'
make_agile(T const&)
,作成类型,在映射类型上会出现:'Release': is not a member of any direct or indirect base class of 'T'
name_of<T>
,映射类型,返回字符串类型的GUIDweak_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,然后不要把作成类的析构函数声明为私有的。
(更新完)