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。另一个推荐的方法是包含来自WILwil\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中的描述。

(本篇完)