C++/WinRT 與 ABI 之間的互通性Interop between C++/WinRT and the ABI

本主題示範如何在 SDK 應用程式二進位介面 (ABI) 與 C++/WinRT 物件之間轉換。This topic shows how to convert between SDK application binary interface (ABI) and C++/WinRT objects. 您可以使用這些技術,在搭配使用這兩種程式設計方式與 Windows 執行階段的程式碼之間進行互通,也可以在將程式碼從 ABI 逐漸移動到 C++/WinRT 時,使用這些技術。You can use these techniques to interop between code that uses these two ways of programming with the Windows Runtime, or you can use them as you gradually move your code from the ABI to C++/WinRT.

一般情況下,C++/WinRT 會將 ABI 類型公開為 void* ,讓您不需要包含平台標頭檔。In general, C++/WinRT exposes ABI types as void*, so that you don't need to include platform header files.

什麼是 Windows 執行階段 ABI,以及有哪些 ABI 類型?What is the Windows Runtime ABI, and what are ABI types?

Windows 執行階段類別 (執行階段類別) 其實是抽象概念。A Windows Runtime class (runtime class) is really an abstraction. 這個抽象概念定義可讓各種不同程式設計語言與物件互動的二進位介面 (應用程式二進位介面或 ABI)。This abstraction defines a binary interface (the Application Binary Interface, or ABI) that allows various programming languages to interact with an object. 無論程式設計語言為何,與 Windows 執行階段物件的用戶端程式碼互動會發生在最低層級,用戶端語言建構會轉譯成物件 ABI 的呼叫。Regardless of programming language, client code interaction with a Windows Runtime object happens at the lowest level, with client language constructs translated into calls into the object's ABI.

"%WindowsSdkDir%Include\10.0.17134.0\winrt" 資料夾中的 Windows SDK 標頭 (視需要調整案例中的 SDK 版本號碼),是 Windows 執行階段 ABI 標頭檔案。The Windows SDK headers in the folder "%WindowsSdkDir%Include\10.0.17134.0\winrt" (adjust the SDK version number for your case, if necessary), are the Windows Runtime ABI header files. 這些標頭是由 MIDL 編譯器產生。They were produced by the MIDL compiler. 這是包括其中一個標頭的範例。Here's an example of including one of these headers.

#include <windows.foundation.h>

以下是您在特定 SDK 標頭中找到的其中一個 ABI 類型的簡化範例。And here's a simplified example of one of the ABI types that you'll find in that particular SDK header. 請注意,ABI 命名空間;Windows::Foundation,以及其他 Windows 命名空間,均由 ABI 命名空間內的 SDK 標頭來宣告。Note the ABI namespace; Windows::Foundation, and all other Windows namespaces, are declared by the SDK headers within the ABI namespace.

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 is a COM interface. 不僅如此,—由於其基底是 IInspectableIUriRuntimeClass 是 Windows 執行階段介面。But more than that—since its base is IInspectableIUriRuntimeClass is a Windows Runtime interface. 請注意,HRESULT 會傳回類型,而不是引發例外狀況。Note the HRESULT return type, rather than the raising of exceptions. 並且使用構件,例如 HSTRING 控點 (完成時,將該控點設回 nullptr 是很好的做法)。And the use of artifacts such as the HSTRING handle (it's good practice to set that handle back to nullptr when you're finished with it). 這樣可了解 Windows 執行階段在應用程式二進位層級的樣子,亦即 COM 程式設計的層級。This gives a taste of what the Windows Runtime looks like at the application binary level; in other words, at the COM programming level.

Windows 執行階段是根據元件物件模型 (COM) API。The Windows Runtime is based on Component Object Model (COM) APIs. 您可以使用該方式存取 Windows 執行階段,或者透過「語言投影」存取。You can access the Windows Runtime that way, or you can access it through language projections. 投影會隱藏 COM 的詳細資訊,並針對特定語言提供更自然的程式設計體驗。A projection hides the COM details, and provides a more natural programming experience for a given language.

例如,如果您在 "%WindowsSdkDir%Include\10.0.17134.0\cppwinrt\winrt" 資料夾中尋找 (同樣地,視需求調整您案例的 SDK 版本號碼),然後您會找到 C++/WinRT 語言投影標頭。For example, if you look in the folder "%WindowsSdkDir%Include\10.0.17134.0\cppwinrt\winrt" (again, adjust the SDK version number for your case, if necessary), then you'll find the C++/WinRT language projection headers. 每一個 Windows 命名空間有一個標頭,就像每一個 Windows 命名空間有一個 ABI 標頭。There's a header for each Windows namespace, just like there's one ABI header per Windows namespace. 以下是包括 C++/WinRT 標頭之一的範例。Here's an example of including one of the C++/WinRT headers.

#include <winrt/Windows.Foundation.h>

而且從該標頭,以下是 (簡化) C++/WinRT,相當於我們看到的該 ABI 類型。And, from that header, here (simplified) is the C++/WinRT equivalent of that ABI type we just saw.

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

這裡的介面是新式的標準 C++。The interface here is modern, standard C++. 其會取消 HRESULT (C++/WinRT 視需要引發例外狀況)。It does away with HRESULTs (C++/WinRT raises exceptions if necessary). 而且,存取子函式會傳回簡單的字串物件,其會在範圍的結尾清除。And the accessor function returns a simple string object, which is cleaned up at the end of its scope.

本主題適用的情況,是希望與在應用程式二進位介面 (ABI) 層運作的程式碼互通或移植的情況。This topic is for cases when you want to interop with, or port, code that works at the Application Binary Interface (ABI) layer.

在程式碼中轉換為 ABI 類型和從 ABI 類型轉換Converting to and from ABI types in code

為了安全和簡單起見,對於雙向轉換,只需使用 winrt::com_ptrcom_ptr::as,與 winrt::Windows::Foundation::IUnknown::asFor safety and simplicity, for conversions in both directions you can simply use winrt::com_ptr, com_ptr::as, and winrt::Windows::Foundation::IUnknown::as. 以下是一個程式碼範例 (根據主控台應用程式專案範本),也會示範您可以如何對不同孤立區使用命名空間別名,處理 C++/WinRT 投影與 ABI 之間可能產生的命名空間衝突。Here's a code example (based on the Console App project template), which also illustrates how you can use namespace aliases for the different islands to deal with otherwise potential namespace collisions between the C++/WinRT projection and the 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 函式的實作會呼叫 QueryInterfaceThe implementations of the as functions call QueryInterface. 如果您想要僅呼叫 AddRef 的較低層級轉換,您可以使用 winrt::copy_to_abiwinrt::copy_from_abi 輔助函式。If you want lower-level conversions that only call AddRef, then you can use the winrt::copy_to_abi and winrt::copy_from_abi helper functions. 下一個程式碼範例將這些較低層級轉換新增至上述的程式碼範例。This next code example adds these lower-level conversions to the code example above.

重要

與 ABI 類型交互操作時,所使用的 ABI 類型務必對應至 C++/WinRT 物件的預設介面。When interoperating with ABI types it's critical that the ABI type used corresponds to the default interface of the C++/WinRT object. 否則,ABI 類型的方法叫用,實際上會在預設介面上的相同 vtable 位置中呼叫方法,並產生意料之外的結果。Otherwise, invocations of methods on the ABI type will actually end up calling methods in the same vtable slot on the default interface with very unexpected results. 請注意,winrt::copy_to_abi 不會在編譯時期防範此種情形,因為其會針對所有 ABI 類型使用 void* ,並假設呼叫端已小心翼翼避免不符合類型。Note that winrt::copy_to_abi does not protect against this at compile time since it uses void* for all ABI types and assumes that the caller has been careful not to mis-match the types. 這是為了避免在可能從未使用 ABI 類型的情況下,要求 C++/WinRT 標頭參考 ABI 標頭。This is to avoid requiring C++/WinRT headers to reference ABI headers when ABI types may never be used.

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 標頭定義)。Here are other similarly low-level conversions techniques but using raw pointers to ABI interface types (those defined by the Windows SDK headers) this time.

    // 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 輔助函式。For the lowest-level conversions, which only copy addresses, you can use the winrt::get_abi, winrt::detach_abi, and winrt::attach_abi helper functions.

WINRT_ASSERT 是巨集定義,而且會發展為 _ASSERTEWINRT_ASSERT is a macro definition, and it expands to _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{ static_cast<abi::IStringable*>(winrt::get_abi(uriAsIStringable)) };
    WINRT_ASSERT(non_owning);

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

convert_from_abi 函式convert_from_abi function

這項輔助函式以最低額外負荷將原始 ABI 介面指標轉換成對等 C++/WinRT 物件。This helper function converts a raw ABI interface pointer to an equivalent C++/WinRT object, with minimal overhead.

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 類型的預設介面。The function simply calls QueryInterface to query for the default interface of the requested C++/WinRT type.

如我們所了解,輔助函式不需要從 C++/WinRT 物件轉換至對等 ABI 介面指標。As we've seen, a helper function is not required to convert from a C++/WinRT object to the equivalent ABI interface pointer. 只要使用 winrt::Windows::Foundation::IUnknown::as (或 try_as) 成員函式為要求的介面詢問。Simply use the winrt::Windows::Foundation::IUnknown::as (or try_as) member function to query for the requested interface. astry_as 函式傳回包裝要求 ABI 類型的 winrt::com_ptr 物件。The as and try_as functions return a winrt::com_ptr object wrapping the requested ABI type.

使用 convert_from_abi 的程式碼範例Code example using convert_from_abi

以下是示範實務上此輔助函式的程式碼範例。Here's a code example showing this helper function in practice.

// 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 介面指標Interoperating with ABI COM interface pointers

下方是協助程式函式範本,將說明如何將指定類型的 ABI COM 介面指標複製到其對等的 C++/WinRT 投影智慧指標類型。The helper function template below illustrates how to copy an ABI COM interface pointer of a given type to its equivalent C++/WinRT projected smart pointer type.

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 Implementation Libraries (WIL) 的智慧指標類型中複製。This next helper function template is equivalent, except that it copies from the smart pointer type from the Windows Implementation Libraries (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 元件Also see Consume COM components with C++/WinRT.

不安全的 ABI COM 介面指標交互操作Unsafe interop with ABI COM interface pointers

如下表所示 (除了其他作業之外),指定類型的 ABI COM 介面指標和其對等 C++/WinRT 投影智慧指標類型之間的轉換並不安全。The table that follows shows (in addition to other operations) unsafe conversions between an ABI COM interface pointer of a given type and its equivalent C++/WinRT projected smart pointer type. 針對資料表中的程式碼,假設這些宣告。For the code in the table, assume these declarations.

winrt::Sample s;
ISample* p;

void GetSample(_Out_ ISample** pp);

進一步假設 ISampleSample 的預設介面。Assume further that ISample is the default interface for Sample.

您可以在編譯時使用此程式碼來宣告此假設。You can assert that at compile time with this code.

static_assert(std::is_same_v<winrt::default_interface<winrt::Sample>, winrt::ISample>);
操作Operation 如何執行此動作How to do it 附註Notes
winrt::Sample 擷取 ISample*Extract ISample* from winrt::Sample p = reinterpret_cast<ISample*>(get_abi(s)); s 仍然擁有物件。s still owns the object.
winrt::Sample 中斷連結 ISample*Detach ISample* from winrt::Sample p = reinterpret_cast<ISample*>(detach_abi(s)); s 不再擁有物件。s no longer owns the object.
ISample* 轉送至新的 winrt::SampleTransfer ISample* to new winrt::Sample winrt::Sample s{ p, winrt::take_ownership_from_abi }; s 會取得物件的擁有權。s takes ownership of the object.
ISample* 設定到 winrt::SampleSet ISample* into winrt::Sample *put_abi(s) = p; s 會取得物件的擁有權。s takes ownership of the object. s 先前擁有的所有物件都會流失 (會在偵錯中宣告)。Any object previously owned by s is leaked (will assert in debug).
ISample* 接收到 winrt::SampleReceive ISample* into winrt::Sample GetSample(reinterpret_cast<ISample**>(put_abi(s))); s 會取得物件的擁有權。s takes ownership of the object. s 先前擁有的所有物件都會流失 (會在偵錯中宣告)。Any object previously owned by s is leaked (will assert in debug).
取代 winrt::Sample 中的 ISample*Replace ISample* in winrt::Sample attach_abi(s, p); s 會取得物件的擁有權。s takes ownership of the object. s 先前擁有的物件會釋出。The object previously owned by s is freed.
ISample* 設定到 winrt::SampleCopy ISample* to winrt::Sample copy_from_abi(s, p); s 會對物件建立新的參考。s makes a new reference to the object. s 先前擁有的物件會釋出。The object previously owned by s is freed.
winrt::Sample 複製到 ISample*Copy winrt::Sample to ISample* copy_to_abi(s, reinterpret_cast<void*&>(p)); p 會接收物件的複本。p receives a copy of the object. p 先前擁有的所有物件都會流失。Any object previously owned by p is leaked.

交互操作 ABI 的 GUID 結構Interoperating with the ABI's GUID struct

GUID 會投影為 winrt::guidGUID is projected as winrt::guid. 對於您實作的 API,必須為 GUID 參數使用 winrt::guidFor APIs that you implement, you must use winrt::guid for GUID parameters. 除此之外,在您包含任何 C++/WinRT 標頭之前,只要您包含 unknwn.h (由 <windows.h> 和其他許多標頭檔以隱含方式包含),winrt::guidGUID 之間就會進行自動轉換。Otherwise, there are automatic conversions between winrt::guid and GUID as long as you include unknwn.h (implicitly included by <windows.h> and many other header files) before you include any C++/WinRT headers.

如果您不這麼做,則可以在這兩者之間建立硬式的 reinterpret_castIf you don't do that, then you can hard-reinterpret_cast between them. 針對下列資料表,假設這些宣告。For the table that follows, assume these declarations.

winrt::guid winrtguid;
GUID abiguid;
轉換Conversion 使用 #include <unknwn.h>With #include <unknwn.h> 不使用 #include <unknwn.h>Without #include <unknwn.h>
winrt::guidGUIDFrom winrt::guid to GUID abiguid = winrtguid; abiguid = reinterpret_cast<GUID&>(winrtguid);
GUIDwinrt::guidFrom GUID to winrt::guid winrtguid = abiguid; winrtguid = reinterpret_cast<winrt::guid&>(abiguid);

您可以建構 winrt::guid,如下所示。You can construct a winrt::guid like this.

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

如需示範如何從字串建構 winrt::guid 的 gist,請參閱 make_guid.cppFor a gist showing how to construct a winrt::guid from a string, see make_guid.cpp.

交互操作 ABI 的 HSTRINGInteroperating with the ABI's HSTRING

下表顯示 winrt::hstringHSTRING 之間的轉換,以及其他作業。The table that follows shows conversions between winrt::hstring and HSTRING, and other operations. 針對資料表中的程式碼,假設這些宣告。For the code in the table, assume these declarations.

winrt::hstring s;
HSTRING h;

void GetString(_Out_ HSTRING* value);
操作Operation 如何執行此動作How to do it 附註Notes
hstring 擷取 HSTRINGExtract HSTRING from hstring h = static_cast<HSTRING>(get_abi(s)); s 仍然擁有字串。s still owns the string.
hstring 中斷連結 HSTRINGDetach HSTRING from hstring h = reinterpret_cast<HSTRING>(detach_abi(s)); s 不再擁有字串。s no longer owns the string.
HSTRING 設定到 hstringSet HSTRING into hstring *put_abi(s) = h; s 會取得字串的擁有權。s takes ownership of string. s 先前擁有的所有字串都會流失 (會在偵錯中宣告)。Any string previously owned by s is leaked (will assert in debug).
HSTRING 接收到 hstringReceive HSTRING into hstring GetString(reinterpret_cast<HSTRING*>(put_abi(s))); s 會取得字串的擁有權。s takes ownership of string. s 先前擁有的所有字串都會流失 (會在偵錯中宣告)。Any string previously owned by s is leaked (will assert in debug).
取代 hstring 中的 HSTRINGReplace HSTRING in hstring attach_abi(s, h); s 會取得字串的擁有權。s takes ownership of string. s 先前擁有的字串會釋出。The string previously owned by s is freed.
HSTRING 複製到 hstringCopy HSTRING to hstring copy_from_abi(s, h); s 會建立字串的私用複本。s makes a private copy of the string. s 先前擁有的字串會釋出。The string previously owned by s is freed.
hstring複製到 HSTRINGCopy hstring to HSTRING copy_to_abi(s, reinterpret_cast<void*&>(h)); h 會接收字串的複本。h receives a copy of the string. h 先前擁有的所有字串都會流失。Any string previously owned by h is leaked.

重要 APIImportant APIs