Взаимодействие между C++/WinRT и ABI

В этом разделе показано, как преобразовать объекты двоичного интерфейса приложения SDK (ABI) и C++/WinRT . Эти методы можно использовать для взаимодействия между кодом, использующими эти два способа программирования с помощью среда выполнения Windows, или использовать их при постепенном перемещении кода из ABI в C++/WinRT.

Как правило, C++/WinRT представляет типы ABI в виде void*, чтобы вам не нужно было подключать файлы заголовков платформы.

Note

В примерах кода мы используем reinterpret_cast (а не static_cast), чтобы подчеркнуть, что это по своей сути небезопасные приведения типов.

Что такое среда выполнения Windows ABI и какие типы ABI?

Класс среда выполнения Windows (класс среды выполнения) — это абстракция. Эта абстракция определяет двоичный интерфейс (двоичный интерфейс приложения или ABI), который позволяет различным языкам программирования взаимодействовать с объектом. Независимо от языка программирования взаимодействие клиентского кода с объектом среда выполнения Windows происходит на самом низком уровне, с конструкциями клиентского языка, переведенными в вызовы ABI объекта.

Заголовки Windows SDK в папке "%WindowsSdkDir%Include\10.0.17134.0\winrt" (при необходимости измените номер версии SDK в вашем случае) — это файлы заголовков ABI среда выполнения Windows. Они были созданы компилятором MIDL. Ниже приведен пример включения одного из этих заголовков.

#include <windows.foundation.h>

Ниже приведен упрощенный пример одного из типов ABI, которые вы найдете в этом конкретном заголовке пакета SDK. Обратите внимание на пространство имен ABI; Windows::Foundation и все остальные пространства имен Windows объявляются заголовками пакета SDK в пространстве имен ABI.

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 основан на API-интерфейсах объектной модели компонентов (COM). Вы можете получить доступ к среда выполнения Windows таким образом или получить к нему доступ через проекции языка. Проекция скрывает сведения COM и обеспечивает более естественный интерфейс программирования для заданного языка.

Например, если вы посмотрите в папке "%WindowsSdkDir%Include\10.0.17134.0\cppwinrt\winrt" (опять же, измените номер версии пакета SDK для вашего дела, если это необходимо), то вы найдете заголовки проекции языка C++/WinRT. Существует заголовок для каждого пространства имен Windows, как и один заголовок ABI для каждого пространства имен Windows. Ниже приведен пример включения одного из заголовков 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 и из них в коде

Для безопасности и простоты при преобразованиях в обоих направлениях можно просто использовать 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. Следующий пример кода добавляет эти преобразования нижнего уровня в приведенный выше пример кода.

Это важно

При взаимодействии с типами ABI важно, чтобы используемый тип ABI соответствовал интерфейсу по умолчанию объекта C++/WinRT. В противном случае вызовы методов типа ABI фактически будут вызывать методы в том же слоте vtable в интерфейсе по умолчанию с очень неожиданными результатами. Обратите внимание, что winrt::copy_to_abi не защищает от этого на этапе компиляции, поскольку использует void* для всех типов ABI и предполагает, что вызывающая сторона позаботилась о том, чтобы не перепутать типы. Это позволяет избежать необходимости использовать заголовки C++/WinRT для ссылки на заголовки ABI, если типы 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 возвращают объект 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

Шаблон вспомогательной функции ниже иллюстрирует копирование указателя интерфейса COM ABI заданного типа в эквивалентный тип проецируемого смарт-указателя 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 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;
}

См. также раздел "Использование COM-компонентов с помощью C++/WinRT".

Небезопасное взаимодействие с указателями интерфейса ABI COM

В следующей таблице показаны (помимо других операций) небезопасные преобразования между указателем интерфейса COM ABI заданного типа и его эквивалентным типом интеллектуального указателя 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
Извлечение ISample* из winrt::Sample p = reinterpret_cast<ISample*>(get_abi(s)); s по-прежнему владеет объектом.
Отключение ISample* от winrt::Sample 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, считается утечкой (в режиме отладки сработает assert).
Преобразование ISample* в winrt::Sample GetSample(reinterpret_cast<ISample**>(put_abi(s))); s берет на себя владение объектом. Любой объект, которым ранее владел s, считается утёкшим (в отладочной сборке будет срабатывать assert).
Замена ISample* в winrt::Sample 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, считается утраченным.

Взаимодействие со структурой GUID в ABI

GUID (/previous-versions/aa373931(v%3Dvs.80)) проецируется как winrt::guid. Для реализуемых API необходимо использовать winrt::guid для параметров GUID. В противном случае существуют автоматические преобразования между winrt::guid и GUID, при условии, что вы включаете unknwn.h (он неявно включается через <windows.h> и многие другие файлы заголовков) до включения каких-либо заголовков C++/WinRT.

Если этого не сделать, то можно жёстко пере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.

Взаимодействие с HSTRING ABI

В следующей таблице показаны преобразования между 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, приводит к утечке (в отладочной сборке сработает assert).
Получение HSTRING в hstring GetString(reinterpret_cast<HSTRING*>(put_abi(s))); s берет на себя владение строкой. Любая строка, ранее принадлежавшая s, будет утечкой; в отладочной сборке сработает assert.
Замена 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
Распаковка смарт-объекта до тех пор, пока не найден необработанный указатель строки Юникода, завершаемый пустым значением wil::str_raw_ptr
Получите строку, на которую ссылается объект интеллектуального указателя; или пустую строку L"", если интеллектуальный указатель пуст wil::string_get_not_null
Объединение любого числа строк wil::str_concat
Получение строки из строки формата printf и соответствующего списка параметров wil::str_printf

Важные API