September 4th, 2025
0 reactions

How can I write a C++/WinRT IAsyncOperation<T> where T is not a Windows Runtime type?, part 2

Last time, we smuggled an arbitrary C++ value inside an IInspectable but noted the lack of safety. So let’s add some safety.

To ensure that the object is in-process, you can declare the interface as “local”, meaning that the interface is for use only within a single process, and the MIDL compiler will not generate proxies for it. Even easier is just defining the interface manually, without involving the MIDL compiler at all.

struct
DECLSPEC_UUID("⟦some guid⟧")
IValueAsInspectable : ::IUnknown
{
};

The trick here is that manually defining the interface lets us put C++ stuff inside it.

template<typename T> struct ValueAsInspectable;

struct
DECLSPEC_UUID("⟦some guid⟧")
IValueAsInspectable : ::IUnknown
{
    std::type_info const* type;

    template<typename T>
    T& get_value()
    {
        if (*type != typeid(T)) {
            throw std::bad_cast();
        }
        return static_cast<ValueAsInspectable<T>*>(this)
            ->value;
    }
};

template<typename T>
struct ValueAsInspectable :
    winrt::implements<ValueAsInspectable<T>,
    winrt::Windows::Foundation::IInspectable,
    IValueAsInspectable>
{
    T value;

    template<typename...Args>
    ValueAsInspectable(Args&&... args) :
        value{ std::forward<Args>(args)... }
    {
        this->>type = std::addressof(typeid(T));
    }
};

The IValueAsInspectable has a COM interface ID, so it can be queried for. But once you get it, you can call the C++ method get_value<T>(). That method first validates that you are using the matching T, throwing a bad_cast exception if not. If you pass that test, then it returns a reference to the value hiding inside.

And for convenience, we can add these helpers. The first two we have seen already:

template<typename T, typename...Args>
winrt::Windows::Foundation::IInspectable
    MakeValueAsInspectable(Args&&... args)
{
    return winrt::make<ValueAsInspectable<T>>(
        std::forward<Args>(args)...);
}

template<typename T>
winrt::Windows::Foundation::IInspectable
    MakeValueAsInspectable(T&& arg)
{
    return winrt::make<ValueAsInspectable<
        std::remove_reference_t<T>>>(
        std::forward<T>(arg));
}

Next are functions for extracting the value from an object that we believe to be a Value­As­Inspectable<T>.

template<typename T>
T& ValueRefFromInspectable(
    winrt::Windows::Foundation::IInspectable const& arg)
{
    return arg.as<IValueAsInspectable>()->
        get_value<T>();
}

template<typename T>
T CopyValueFromInspectable(
    winrt::Windows::Foundation::IInspectable const& arg)
{
    return ValueRefFromInspectable<T>(arg);
}

template<typename T>
T MoveValueFromInspectable(
    winrt::Windows::Foundation::IInspectable && arg)
{
    return std::move(ValueRefFromInspectable<T>(arg));
}

The basic function is Value­Ref­From­Inspectable<T> which produces an lvalue reference from an inspectable that we assume represents a Value­As­Inspectable<T>. (If we’re wrong, it throws a bad_cast exception.) Building on that are two functions which either copy or move the value out of the Value­As­Inspectable<T>.

I gave the function that returns an lvalue name that emphasizes that you get a reference from the inspectable, hoping to prevent people from getting a reference to an expiring inspectable:

// Code in italics is wrong
// Don't do this.
auto& value = ValueRefFromInspectablt<Widget>(
    co_await DoSomethingAsync());

Note also that if you modify the value reference or move the value out of the inpectable, this affects the underlying object, which means that other people that have a reference to the same inspectable will see the object change state.

void MutateTheWidget(winrt::Windows::Foundation::IInspectable obj)
{
    auto& widget = ValueRefFromInspectable<Widget>(obj);
    ⟦ do something that modifies the widget ⟧
}

winrt::fire_and_forget Sample()
{
    auto obj = co_await DoSomethingAsync();
    MutateTheWidget(obj);
    auto& widget = ValueRefFromInspectable<Widget>(obj);
    // this code sees that the widget has been mutated
}

But really, if you need a coroutine that produces a non-Windows Runtime type, then use a non-Windows Runtime coroutine library. I’m personally partial to wil::task (and its COM-aware buddy wil::com_task), seeing as I wrote it.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

0 comments