C++/WinRT 与 ABI 之间的互操作

本主题演示如何在 SDK 应用程序二进制接口(ABI)和 C++/WinRT 对象之间进行转换。 可以使用这些技术在使用这两种编程方式与 Windows 运行时 进行编程的代码之间进行互操作,也可以在将代码从 ABI 逐步移动到 C++/WinRT 时使用它们。

通常,C++/WinRT 将 ABI 类型公开为 void*,因此无需包含平台头文件。

注释

在代码示例中,我们使用 reinterpret_cast(而不是 static_cast),以明确表明这些强制转换本质上是不安全的。

什么是 Windows 运行时 ABI,什么是 ABI 类型?

Windows 运行时类(运行时类)实际上是一种抽象。 此抽象定义一个二进制接口(应用程序二进制接口或 ABI),该接口允许各种编程语言与对象交互。 无论编程语言如何,客户端代码与Windows 运行时对象的交互都发生在最低级别,客户端语言构造转换为对对象的 ABI 的调用。

位于“%WindowsSdkDir%Include\10.0.17134.0\winrt”文件夹中的 Windows SDK 标头文件(如有需要,请根据你的情况调整 SDK 版本号)是 Windows 运行时 ABI 头文件。 它们由 MIDL 编译器生成。 以下是包含其中一个标头的示例。

#include <windows.foundation.h>

下面是一个可在该特定 SDK 标头中找到的 ABI 类型的简化示例。 记下 ABI 命名空间;Windows::Foundation 和所有其他Windows命名空间由 ABI 命名空间中的 SDK 标头声明。

namespace ABI::Windows::Foundation
{
    IUriRuntimeClass : public IInspectable
    {
    public:
        /* [propget] */ virtual HRESULT STDMETHODCALLTYPE get_AbsoluteUri(/* [retval, out] */__RPC__deref_out_opt HSTRING * value) = 0;
        ...
    }
}

IUriRuntimeClass 是一个 COM 接口。 但除此之外,IUriRuntimeClass 是一个Windows 运行时接口,因为它的基础是 IInspectable。 请注意,这里使用的是 HRESULT 返回类型,而不是引发异常。 以及使用诸如 HSTRING 句柄之类的机制(在用完后将该句柄重新设回 nullptr 是一种良好做法)。 这让人对 Windows 运行时 在应用程序二进制层面呈现出的样子有一个初步了解;换句话说,就是在 COM 编程层面。

Windows 运行时基于组件对象模型 (COM) API。 可以通过该方式访问Windows 运行时,也可以通过语言投影访问它。 投影会隐藏 COM 详细信息,并为给定语言提供更自然的编程体验。

例如,如果你查看文件夹“%WindowsSdkDir%Include\10.0.17134.0\cppwinrt\winrt”(如有必要,请根据你的实际情况再次调整 SDK 版本号),就会找到 C++/WinRT 语言投影头文件。 每个Windows命名空间都有一个标头,就像每个Windows命名空间有一个 ABI 标头一样。 下面是包含其中一个 C++/WinRT 头文件的示例。

#include <winrt/Windows.Foundation.h>

另外,在该头文件中,这里给出的是我们刚才看到的 ABI 类型所对应的 C++/WinRT 等效形式(简化后)。

namespace winrt::Windows::Foundation
{
    struct Uri : IUriRuntimeClass, ...
    {
        winrt::hstring AbsoluteUri() const { ... }
        ...
    };
}

此处的接口是新式的标准 C++ 。 它不再使用 HRESULT(如果有必要,C++/WinRT 会引发异常)。 访问器函数返回一个简单的字符串对象,该对象会在其作用域结束时被清理。

本主题适用于以下情况:当你想与在应用程序二进制接口(ABI)层工作的代码进行互操作,或移植此类代码时。

在代码中转换到 ABI 类型以及从 ABI 类型转换

为了安全和简单起见,对于双向转换,只需使用 winrt::com_ptrcom_ptr::aswinrt::Windows::Foundation::IUnknown::as。 下面是一个代码示例(基于 控制台应用 项目模板),该模板还说明了如何使用不同岛屿的命名空间别名来处理 C++/WinRT 投影和 ABI 之间潜在的命名空间冲突。

// pch.h
#pragma once
#include <windows.foundation.h>
#include <unknwn.h>
#include "winrt/Windows.Foundation.h"

// main.cpp
#include "pch.h"

namespace winrt
{
    using namespace Windows::Foundation;
}

namespace abi
{
    using namespace ABI::Windows::Foundation;
};

int main()
{
    winrt::init_apartment();

    winrt::Uri uri(L"http://aka.ms/cppwinrt");

    // Convert to an ABI type.
    winrt::com_ptr<abi::IStringable> ptr{ uri.as<abi::IStringable>() };

    // Convert from an ABI type.
    uri = ptr.as<winrt::Uri>();
    winrt::IStringable uriAsIStringable{ ptr.as<winrt::IStringable>() };
}

as 函数的实现会调用 QueryInterface。 如果想要仅调用 AddRef 的较低级别的转换,则可以使用 winrt::copy_to_abiwinrt::copy_from_abi 帮助程序函数。 下一个代码示例将这些较低级别的转换添加到上面的代码示例。

Important

与 ABI 类型进行互操作时,使用的 ABI 类型对应于 C++/WinRT 对象的默认接口至关重要。 否则,在 ABI 类型上调用方法实际上最终会在默认接口的同一 vtable 槽中调用方法,结果非常意外。 请注意, winrt::copy_to_abi 在编译时不会对此进行保护,因为它对所有 ABI 类型使用 void* ,并假定调用方已小心不要错误地匹配类型。 这是为了避免在 ABI 类型可能根本不会被使用的情况下,仍要求 C++/WinRT 头文件引用 ABI 头文件。

int main()
{
    // The code in main() already shown above remains here.

    // Lower-level conversions that only call AddRef.

    // Convert to an ABI type.
    ptr = nullptr;
    winrt::copy_to_abi(uriAsIStringable, *ptr.put_void());

    // Convert from an ABI type.
    uri = nullptr;
    winrt::copy_from_abi(uriAsIStringable, ptr.get());
    ptr = nullptr;
}

下面介绍其他类似的底层转换技术,不过这次使用的是指向 ABI 接口类型的原始指针(即 Windows SDK 标头中定义的类型)。

    // The code in main() already shown above remains here.

    // Copy to an owning raw ABI pointer with copy_to_abi.
    abi::IStringable* owning{ nullptr };
    winrt::copy_to_abi(uriAsIStringable, *reinterpret_cast<void**>(&owning));

    // Copy from a raw ABI pointer.
    uri = nullptr;
    winrt::copy_from_abi(uriAsIStringable, owning);
    owning->Release();

对于只复制地址的最低层级的转换,可以使用 winrt::get_abiwinrt::detach_abiwinrt::attach_abi 帮助函数。

WINRT_ASSERT 是宏定义,它扩展到 _ASSERTE

    // The code in main() already shown above remains here.

    // Lowest-level conversions that only copy addresses

    // Convert to a non-owning ABI object with get_abi.
    abi::IStringable* non_owning{ reinterpret_cast<abi::IStringable*>(winrt::get_abi(uriAsIStringable)) };
    WINRT_ASSERT(non_owning);

    // Avoid interlocks this way.
    owning = reinterpret_cast<abi::IStringable*>(winrt::detach_abi(uriAsIStringable));
    WINRT_ASSERT(!uriAsIStringable);
    winrt::attach_abi(uriAsIStringable, owning);
    WINRT_ASSERT(uriAsIStringable);

convert_from_abi 函数

此帮助程序函数将原始 ABI 接口指针转换为等效的 C++/WinRT 对象,开销最小。

template <typename T>
T convert_from_abi(::IUnknown* from)
{
    T to{ nullptr }; // `T` is a projected type.

    winrt::check_hresult(from->QueryInterface(winrt::guid_of<T>(),
        winrt::put_abi(to)));

    return to;
}

该函数只调用 QueryInterface 来查询所请求的 C++/WinRT 类型的默认接口。

正如我们所看到的,不需要帮助程序函数从 C++/WinRT 对象转换为等效的 ABI 接口指针。 只需使用 winrt::Windows::Foundation::IUnknown::as(或 try_as) 成员函数来查询请求的接口。 astry_as 函数返回包装所请求的 ABI 类型的 winrt::com_ptr 对象。

使用 convert_from_abi 的代码示例

下面是一个代码示例,展示了该辅助函数的实际用法。

// pch.h
#pragma once
#include <windows.foundation.h>
#include <unknwn.h>
#include "winrt/Windows.Foundation.h"

// main.cpp
#include "pch.h"
#include <iostream>

using namespace winrt;
using namespace Windows::Foundation;

namespace winrt
{
    using namespace Windows::Foundation;
}

namespace abi
{
    using namespace ABI::Windows::Foundation;
};

namespace sample
{
    template <typename T>
    T convert_from_abi(::IUnknown* from)
    {
        T to{ nullptr }; // `T` is a projected type.

        winrt::check_hresult(from->QueryInterface(winrt::guid_of<T>(),
            winrt::put_abi(to)));

        return to;
    }
    inline auto put_abi(winrt::hstring& object) noexcept
    {
        return reinterpret_cast<HSTRING*>(winrt::put_abi(object));
    }
}

int main()
{
    winrt::init_apartment();

    winrt::Uri uri(L"http://aka.ms/cppwinrt");
    std::wcout << "C++/WinRT: " << uri.Domain().c_str() << std::endl;

    // Convert to an ABI type.
    winrt::com_ptr<abi::IUriRuntimeClass> ptr = uri.as<abi::IUriRuntimeClass>();
    winrt::hstring domain;
    winrt::check_hresult(ptr->get_Domain(sample::put_abi(domain)));
    std::wcout << "ABI: " << domain.c_str() << std::endl;

    // Convert from an ABI type.
    winrt::Uri uri_from_abi = sample::convert_from_abi<winrt::Uri>(ptr.get());

    WINRT_ASSERT(uri.Domain() == uri_from_abi.Domain());
    WINRT_ASSERT(uri == uri_from_abi);
}

与 ABI COM 接口指针互操作

下面的帮助程序函数模板演示如何将给定类型的 ABI COM 接口指针复制到其等效的 C++/WinRT 投影智能指针类型。

template<typename To, typename From>
To to_winrt(From* ptr)
{
    To result{ nullptr };
    winrt::check_hresult(ptr->QueryInterface(winrt::guid_of<To>(), winrt::put_abi(result)));
    return result;
}
...
ID2D1Factory1* com_ptr{ ... };
auto cppwinrt_ptr {to_winrt<winrt::com_ptr<ID2D1Factory1>>(com_ptr)};

下一个帮助程序函数模板是等效的,只不过它从Windows实现库(WIL)中的智能指针类型复制。

template<typename To, typename From, typename ErrorPolicy>
To to_winrt(wil::com_ptr_t<From, ErrorPolicy> const& ptr)
{
    To result{ nullptr };
    if constexpr (std::is_same_v<typename ErrorPolicy::result, void>)
    {
        ptr.query_to(winrt::guid_of<To>(), winrt::put_abi(result));
    }
    else
    {
        winrt::check_result(ptr.query_to(winrt::guid_of<To>(), winrt::put_abi(result)));
    }
    return result;
}

另请参阅 通过 C++/WinRT 使用 COM 组件

使用 ABI COM 接口指针进行不安全的互操作

下表显示了给定类型的 ABI COM 接口指针与其等效的 C++/WinRT 投影智能指针类型之间的不安全转换(除了其他操作)。 对于表中的代码,假定这些声明。

winrt::Sample s;
ISample* p;

void GetSample(_Out_ ISample** pp);

假设 ISample示例的默认接口。

你可以使用这段代码在编译时进行断言。

static_assert(std::is_same_v<winrt::default_interface<winrt::Sample>, winrt::ISample>);
Operation 操作方法 Notes
winrt::Sample 提取 ISample* p = reinterpret_cast<ISample*>(get_abi(s)); s 仍拥有该对象。
winrt::Sample 分离 ISample* p = reinterpret_cast<ISample*>(detach_abi(s)); s 不再拥有该对象。
ISample* 传输到新的 winrt::Sample winrt::Sample s{ p, winrt::take_ownership_from_abi }; s 获取对象的所有权。
ISample* 设置为 winrt::Sample *put_abi(s) = p; s 获取对象的所有权。 任何先前由 s 拥有的对象都会发生泄漏(在调试模式下会触发断言)。
ISample* 接收到 winrt::Sample GetSample(reinterpret_cast<ISample**>(put_abi(s))); s 获取对象的所有权。 任何先前由 s 拥有的对象都会发生泄漏(在调试模式下会触发断言)。
替换 winrt::Sample 中的 ISample* attach_abi(s, p); s 获取对象的所有权。 之前由 s 拥有的对象被释放。
ISample* 复制到 winrt::Sample copy_from_abi(s, p); s 对对象进行新的引用。 先前由s拥有的对象被释放。
winrt::Sample 复制到 ISample* copy_to_abi(s, reinterpret_cast<void*&>(p)); p 接收对象的副本。 以前由 p 拥有的任何对象都泄露。

与 ABI 的 GUID 结构进行互操作

GUID/previous-versions/aa373931(v%3Dvs.80)) 投影为 winrt::guid。 对于实现的 API,必须为 GUID 参数使用 winrt::guid 。 否则,只要在包含任何 C++/WinRT 头文件之前先包含 unknwn.h(它会由 <windows.h> 和许多其他头文件隐式包含),就会在 winrt::guidGUID 之间自动进行转换。

如果不这样做,那么你可以在它们之间进行硬reinterpret_cast切换。 对于后面的表,请假定这些声明。

winrt::guid winrtguid;
GUID abiguid;
Conversion 使用 #include <unknwn.h> 没有 #include <unknwn.h>
winrt::guidGUID abiguid = winrtguid; abiguid = reinterpret_cast<GUID&>(winrtguid);
GUIDwinrt::guid winrtguid = abiguid; winrtguid = reinterpret_cast<winrt::guid&>(abiguid);

可以构造如下所示的 winrt::guid

winrt::guid myGuid{ 0xC380465D, 0x2271, 0x428C, { 0x9B, 0x83, 0xEC, 0xEA, 0x3B, 0x4A, 0x85, 0xC1} };

有关如何从字符串构造 winrt::guid 的 gist,请参阅 make_guid.cpp

与 ABI 的 HSTRING 进行互操作

下表显示了 winrt::hstringHSTRING 之间的转换,以及其他操作。 对于表中的代码,假定这些声明。

winrt::hstring s;
HSTRING h;

void GetString(_Out_ HSTRING* value);
Operation 操作方法 Notes
hstring 中提取 HSTRING h = reinterpret_cast<HSTRING>(get_abi(s)); s 仍拥有字符串。
HSTRINGhstring 分离 h = reinterpret_cast<HSTRING>(detach_abi(s)); s 不再拥有字符串。
HSTRING 设置为 hstring *put_abi(s) = h; s 获取字符串的所有权。 任何先前由 s 持有的字符串都会发生泄漏(在调试模式下会触发断言)。
HSTRING 接收到 hstring GetString(reinterpret_cast<HSTRING*>(put_abi(s))); s 获取字符串的所有权。 以前拥有 s 的任何字符串都会泄露(将在调试中断言)。
hstring 中替换 HSTRING attach_abi(s, h); s 获取字符串的所有权。 以前由 s 拥有的字符串已释放。
HSTRING 复制到 hstring copy_from_abi(s, h); s 创建字符串的私有副本。 以前由 s 拥有的字符串已释放。
hstring 复制到 HSTRING copy_to_abi(s, reinterpret_cast<void*&>(h)); h 接收字符串的副本。 以前由 h 拥有的任何字符串都泄露。

此外,Windows实现库(WIL)字符串帮助程序执行基本的字符串操作。 若要使用 WIL 字符串帮助程序,请包括 <wil/resource.h>,并参阅下表。 按照表中的链接了解完整详细信息。

Operation 有关详细信息,请参阅 WIL 字符串帮助程序
提供原始 Unicode 或 ANSI 字符串指针和可选长度;获取适当专用 unique_any 包装器 wil::make_something_string
解包智能对象,直到找到原始以 null 结尾的 Unicode 字符串指针 wil::str_raw_ptr
获取由智能指针对象封装的字符串;或者,如果智能指针为空,则获取空字符串 L"" wil::string_get_not_null
连接任意数量的字符串 wil::str_concat
从 printf 样式格式字符串和相应的参数列表获取字符串 wil::str_printf

重要 API