本主題說明如何在 SDK 應用二進位介面(ABI)與 C++/WinRT 物件之間轉換。 你可以使用這些技術,讓採用這兩種 Windows 執行階段 程式設計方式的程式碼彼此互通;也可以在你逐步將程式碼從 ABI 遷移到 C++/WinRT 的過程中使用這些技術。
一般來說,C++/WinRT 會將 ABI 類型暴露為空檔 *,因此你不需要包含平台標頭檔案。
Note
在程式碼範例中,我們使用 reinterpret_cast (而非 static_cast)來預 示 哪些是本質上不安全的鑄造。
什麼是 Windows 執行階段 ABI?有哪些 ABI 類型?
Windows 執行階段 類別(runtime 類別)其實是一種抽象。 此抽象定義了一個二進位介面(應用二進位介面,ABI),允許各種程式語言與物件互動。 無論程式語言為何,客戶端與 Windows 執行階段 物件的互動皆發生在最低層級,客戶端語言結構會轉譯為物件 ABI 的呼叫。
資料夾「%WindowsSdkDir%Include\10.0.17134.0\winrt」中的Windows SDK 標頭(如有需要可調整 SDK 版本號)是Windows 執行階段 ABI 標頭檔。 它們由 MIDL 編譯器產生。 這裡有一個包含這些標頭的範例。
#include <windows.foundation.h>
這裡有一個簡化的 ABI 類型範例,你會在那個特定的 SDK 標頭中找到。 請注意 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 介面。 但更重要的是——因為它的基底是 IInspectable——IUriRuntimeClass 是一個 Windows 執行階段 介面。 請注意,這裡使用的是 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。 這裡有一個程式碼範例(以 Console App 專案範本為基礎),也示範如何為不同的 island 使用命名空間別名,以避免 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 輔助函式。 以下範例將這些較低階的轉換加入上述範例中。
這很重要
在與 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::d etach_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 函式會回傳 winrt::com_ptr 物件,包裹所請求的 ABI 型態。
使用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 是 Sample 的預設介面。
你可以在編譯時用這段程式碼來斷言這點。
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::範例 | 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::樣本 到 ISample* | copy_to_abi(s, reinterpret_cast<void*&>(p)); |
p 會收到物件的副本。 任何先前屬於 p 的物件都會被洩漏。 |
與 ABI 的 GUID 結構體互通
GUID (/previous-versions/aa373931(v%3Dvs.80)) 會投影為 winrt::guid。 對於你實作的 API,必須使用 winrt::guid 作為 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 的要點,請參見 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 擁有 string 的所有權。 任何先前由 s 擁有的字串都會被洩漏(會在除錯中斷言)。 |
| 將 HSTRING 接收到 hstring 中 | GetString(reinterpret_cast<HSTRING*>(put_abi(s))); |
S 擁有 string 的所有權。 任何先前由 s 擁有的字串都會被洩漏(會在除錯中斷言)。 |
| 將 hstring 中的 HSTRING 替換 | attach_abi(s, h); |
S 擁有 string 的所有權。 先前屬於 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 |