C++/WinRT 與 ABI 之間的互通性

本主題說明如何在 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_ptrcom_ptr::aswinrt::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_abiwinrt::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_abiwinrt::d etach_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 函式會回傳 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);

進一步假設 ISampleSample 的預設介面。

你可以在編譯時用這段程式碼來斷言這點。

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::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 的要點,請參見 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 擁有 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

重要 API