本主题演示如何在 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_ptr、com_ptr::as 和 winrt::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_abi 和 winrt::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_abi、winrt::detach_abi 和 winrt::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) 成员函数来查询请求的接口。 as 和 try_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::guid 和 GUID 之间自动进行转换。
如果不这样做,那么你可以在它们之间进行硬reinterpret_cast切换。 对于后面的表,请假定这些声明。
winrt::guid winrtguid;
GUID abiguid;
| Conversion | 使用 #include <unknwn.h> |
没有 #include <unknwn.h> |
|---|---|---|
| 从 winrt::guid 到 GUID | abiguid = winrtguid; |
abiguid = reinterpret_cast<GUID&>(winrtguid); |
| 从 GUID 到 winrt::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::hstring 和 HSTRING 之间的转换,以及其他操作。 对于表中的代码,假定这些声明。
winrt::hstring s;
HSTRING h;
void GetString(_Out_ HSTRING* value);
| Operation | 操作方法 | Notes |
|---|---|---|
| 从 hstring 中提取 HSTRING | h = reinterpret_cast<HSTRING>(get_abi(s)); |
s 仍拥有字符串。 |
| 将 HSTRING 从 hstring 分离 | 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 |