Interop between C++/WinRT and the ABI
WinRT是基于COM,而COM分服务端和客户端,之间通过接口来通信。所以对于C++ WinRT而言,一个重要的工作是封装COM接口,共上层使用。
In general, C++/WinRT exposes ABI types as void*, so that you don’t need to include platform header files.
如果你安装了Win10的SDK,默认会带有WinRT的头文件,比如在"%WindowsSdkDir%Include\10.0.17134.0\winrt"目录下(不同SDK版本号要变)有windows.foundatiaon.h
头文件,就是WinRT提供的C++头文件。这些个头文件的命名空间都是以ABI开头的,比如ABI::Windows::Foundation::IUriRuntimeClass
。
你可以直接以ABI的方式来直接使用WinRT的API,或者可以使用具体语言的投射来使用WinRT的API。C++/WinRT所生成的就是针对C++所定制的,基于模板的投射。可以在"%WindowsSdkDir%Include\10.0.17134.0\cppwinrt\winrt"找到其头文件。上面的windows.foundatiaon.h
头文件投射之后变为winrt/Windows.Foundation.h
。
可以在代码中把ABI和投射类型放置在不同的命名空间中,比如:
namespace winrt
{
using namespace Windows::Foundation;
}
namespace abi
{
using namespace ABI::Windows::Foundation;
};
在投射类型和ABI类型之间转化
调用投射类型的as方法可以将投射类型转化为ABI类型(com_ptr)的形式:
winrt::Uri uri(L"http://aka.ms/cppwinrt");
// Convert to an ABI type.
winrt::com_ptr<abi::IStringable> ptr{ uri.as<abi::IStringable>() };
反之,通过com_ptr的as方法可以将com_ptr转回投射类型:
// Convert from an ABI type.
uri = ptr.as<winrt::Uri>();
winrt::IStringable uriAsIStringable{ ptr.as<winrt::IStringable>() };
as方法在实现的时候调用了QueryInterface接口。如果想避免调用QueryInterface,而只调用AddRef,那么可以 winrt::copy_to_abi 和winrt::copy_from_abi:
// Convert to an ABI type.
ptr = nullptr;
winrt::copy_to_abi(uri, *ptr.put_void());
// Convert from an ABI type.
uri = nullptr;
winrt::copy_from_abi(uri, ptr.get());
ptr = nullptr;
如果不想使用com_ptr这个智能指针:
// Copy to an owning raw ABI pointer with copy_to_abi.
abi::IStringable* owning{ nullptr };
winrt::copy_to_abi(uri, *reinterpret_cast<void**>(&owning));
// Copy from a raw ABI pointer.
uri = nullptr;
winrt::copy_from_abi(uri, owning);
owning->Release();
如果只是想复制地址,而不想触发引用计数,可以使用 winrt::get_abi, winrt::detach_abi, 和winrt::attach_abi 等辅助函数:
// Lowest-level conversions that only copy addresses
// Convert to a non-owning ABI object with get_abi.
abi::IStringable* non_owning{ static_cast<abi::IStringable*>(winrt::get_abi(uri)) };
WINRT_ASSERT(non_owning);
// Avoid interlocks this way.
owning = static_cast<abi::IStringable*>(winrt::detach_abi(uri));
WINRT_ASSERT(!uri);
winrt::attach_abi(uri, owning);
WINRT_ASSERT(uri);
下面的convert_from_abi函数可以帮助从ABI类型,转化到投射类型
template <typename T>
T convert_from_abi(::IUnknown* from)
{
T to{ nullptr };
winrt::check_hresult(from->QueryInterface(winrt::guid_of<T>(),
reinterpret_cast<void**>(winrt::put_abi(to))));
return to;
}
put_abi返回C++ WinRT对象的IUnknown指针的地址,可以用来修改IUnknown指针。
此函数使用到了QueryInterface方法,下面是一个例子:
winrt::Uri uri(L"http://aka.ms/cppwinrt");
winrt::com_ptr<abi::IUriRuntimeClass> ptr = uri.as<abi::IUriRuntimeClass>();
winrt::Uri uri_from_abi = convert_from_abi<winrt::Uri>(ptr.get());
不安全的接口操作
假设Sample是一个COM对象,其默认接口为ISample,可以使用下面的断言来判断:
static_assert(std::is_same_v<winrt::default_interface<winrt::Sample>, winrt::ISample>);
下面所列的操作是不安全的:
p = reinterpret_cast<ISample*>(get_abi(s));
,s仍然拥有这个对象p = reinterpret_cast<ISample*>(detach_abi(s));
,s丧失了这个对象的所有权winrt::Sample s{ p, winrt::take_ownership_from_abi };
,s接过对象的所有权*put_abi(s) = p;
,s获得了对象的所有权,之前s拥有的指针则是泄露了,在debug模式下会出错GetSample(reinterpret_cast<ISample**>(put_abi(s)));
,s获得了对象的所有权,之前s拥有的指针则是泄露了,在debug模式下会出错attach_abi(s, p);
,s获得了对象的所有权,s之前的对象被释放copy_from_abi(s, p);
,s获得了对象的引用,s之前的对象被释放copy_to_abi(s, reinterpret_cast<void*&>(p));
,p获得了对象的一份拷贝,p之前所有的对象泄露了
GUID在C++ WinRT中投射为winrt::guid。如果包含C++ WinRT的头文件之前包含了unknwn.h头文件,则winrt::guid和GUID之间可以互相转化,否则需要用reinterpret_cast 来转化:
- 从winrt::guid转到GUID
- abiguid = winrtguid;
- abiguid = reinterpret_cast<GUID&>(winrtguid);
- 从GUID到winrt::guid
- winrtguid = abiguid;
- winrtguid = reinterpret_castwinrt::guid&(abiguid);
winrt::hstring 和HSTRING 之间的转化:
h = static_cast<HSTRING>(get_abi(s));
,s依然拥有此字符串h = reinterpret_cast<HSTRING>(detach_abi(s));
,s放弃所有权*put_abi(s) = h;
,s接过字符串所有权,s之前所拥有的字符串丧失(在debug配置下会报错)GetString(reinterpret_cast<HSTRING*>(put_abi(s)));
,s接过字符串所有权,s之前所拥有的字符串丧失(在debug配置下会报错)attach_abi(s, h);
,s接过字符串所有权,s之前所有的被释放- copy_from_abi(s, h);,s保存了字符串的副本,s之前所有的被释放
copy_to_abi(s, reinterpret_cast<void*&>(h));
,h获得字符串的一个副本,h之前所有的字符串丧失
Passing parameters into the ABI boundary
C++ WinRT使用winrt::param命名空间下的类型来给ABI接口的参数做转换。许多类型有同步和异步两种,根据你调用方法d的类型来使用。
- winrt::param::hstring用来简化处理ABI接口的HSTRING类型的参数
winrt::param::iterable<T>
和winrt::param::async_iterable<T>
简化处理ABI接口IIterable<T>
类型的参数winrt::param::vector_view<T>
和winrt::param::async_vector_view<T>
简化处理ABI接口IVectorView<T>
类型的参数winrt::param::map_view<T>
和winrt::param::async_map_view<T>
简化处理ABI接口的IMapView<T>
类型的参数winrt::param::vector<T>
简化处理ABI接口IVector<T>
类型的参数winrt::param::map<T>
简化处理ABI接口IMap<T>
类型的参数winrt::array_view<T>
不在winrt::param命名空间中,但是可以用来简化处理ABI接口C数组类型(也叫conformant arrays)的参数
Author COM components with C++/WinRT
winrt::implements也可以用来实现COM接口。虽然默认情况下它只支持从IInspectable接口派生的出接口。所以QueryInterface(QI)等接口默认没有实现,会返回E_NOINTERFACE。
为了让C++/WinRT能够支持COM,只需要做一件事,那就是在包含其他C++/WinRT头文件(winrt/base.h)之前,包含unknwn.h
。有其他头文件间接包含了unknwn.h,比如ole2.h。另一个推荐的方法是包含来自WIL的wil\cppwinrt.h
,这个头文件不仅确保unknwn.h在winrt/base.h之前包含,还确保wil和cppwinrt之间错误代码的转化。
Author COM components with C++/WinRT包含一个例子,用来发送一个Toast通知,并且支持Toast回调。这个例子有两个模式,一个是.exe的本地进程;另一个是.dll的线程挂载模式。
Author APIs with C++/WinRT
Instantiating and returning implementation types and interfaces
在WinRT的实现侧,可以通过winrt::make来实例化一个runtimeclass。
下面的例子中,MyType实现了两个接口,分别是IStringable和IClosable:
// MyType.idl
namespace MyProject
{
runtimeclass MyType: Windows.Foundation.IStringable, Windows.Foundation.IClosable
{
MyType();
}
}
#include <winrt/Windows.Foundation.h>
using namespace winrt;
using namespace Windows::Foundation;
struct MyType : implements<MyType, IStringable, IClosable>
{
winrt::hstring ToString(){ ... }
void Close(){}
};
可以通过下面的代码来激活一个MyType的示例,并赋值给其投射类:
IStringable istringable = winrt::make<MyType>();
上面的步骤其实可以分成两步:
IStringable istringable { nullptr };
istringable = winrt::make<MyType>();
通过nullptr构建的接口不会激活相应的runtimeclass实例,而默认构造函数会。
因为是在实现类之中,其实可以直接调用实现侧的方法,而不用通过接口:
winrt::com_ptr<MyType> myimpl = winrt::make_self<MyType>();
myimpl->ToString();
myimpl->Close();
IClosable iclosable = myimpl.as<IClosable>();
iclosable.Close();
通过make_self来创建实例,然后使用实例的方法,最后在需要的时候通过as来进行QI操作,完成从实现侧到投射侧的转化。在实现侧调用runtimeclass的方法,可以避免虚函数的开销,也可以使用没有在idl中声明的方法。
如果你知道一个接口的实现类是什么,则可以通过get_self来获取实现类的实例。具体请看Instantiating and returning implementation types and interfaces中的描述。
(本篇完)