Home / News / How can I write a C++/WinRT IAsyncOperation where T is not a Windows Runtime types?, part 1

How can I write a C++/WinRT IAsyncOperation where T is not a Windows Runtime types?, part 1

Here’s a sarcastic version of the given text:
I often get asked about how one could use C++/WinRT’s IAsyncOperation<T> where T is not a Windows Runtime type. This is like asking how one could use a calculator that doesn’t have a built-in add function, right? The IAsyncOperation<T> is a tool designed to work with Windows Runtime types, but it’s not suitable for all types.
The T in IAsyncOperation<T> must be a Windows Runtime type because the algorithm for generating an interface ID for IAsyncOperation<T> is specific to Windows Runtime types. This means that if you try to use it with a different type, you’ll encounter errors and runtime errors.
But fret not, because there’s a solution out there that will break the rule and get you everywhere! Instead of using IAsyncOperation<T>, you can use a different coroutine library that supports arbitrary C++ types. These libraries, such as cpp

I often get asked about how one could use C++/WinRT’s IAsyncOperation<T> where T is not a Windows Runtime type.

The T in IAsyncOperation<T> must be a Windows Runtime type because the algorithm for generating an interface ID for IAsyncOperation<T> works only on Windows Runtime types.

But all hope is not lost. You just have to decide which rule you want to break.

The best solution is to break the rule “I’m using IAsyncOperation<T>.” Instead, use some other coroutine library that supports arbitrary C++ types. Available options include cppcoro::task<T>, wil::task<T> (based on my simple_task, but expanded to support COM via wil::com_task<T>), and concurrency::task<T>.

Another option is not to use the T to return the result, but rather pass the output parameter as an explicit input.

winrt::IAsyncAction DoSomethingAsync(std::shared_ptr<std::optional<Widget>> result)
{
    ⟦ calculations... ⟧

    // Store the result
    result.get()->emplace(blah);

    co_return;
}

This is the most general case in which the Widget is not inexpensively default-constructible. If the Widget is cheap to construct, you can avoid the std::optional:

winrt::IAsyncAction DoSomethingAsync(std::shared_ptr<Widget> result)
{
    ⟦ calculations... ⟧

    // Store the result
    *result = blah;

    co_return;
}

It’s important that we use a shared_ptr to ensure that the result lifetime extends to the point we store it. You might be tempted to do something like this:

// Code in italics is wrong
winrt::IAsyncAction DoSomethingAsync(Widget& result)
{
    ⟦ calculations... ⟧

    // Store the result
    result = blah;

    co_return;
}

The problem is that you have no guarantee that the caller will keep the result value through to the completion of the coroutine. If the caller returns before Do­Something­Async completes (say because it doesn’t await the result, or it encounters an exception before it can await the result), then the “Store the result” will be writing to an already-destroyed object and will corrupt memory.

Another option is to use IAsyncOperation<T> but break the rule that T is the thing you want to return. You can instead return an IInspectable that is hiding another value inside it.

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

    template<template...Args>
    ValueAsInspectable(Args&&... args) :
        value{ std::forward<Args>(args)... } {}
};

You can then use one of these “values disguised as an IInspectable” as your operation’s result.

winrt::IAsyncOperation<winrt::IInspectable> DoSomethingAsync()
{
    ⟦ calculations... ⟧

    co_return winrt::make<ValueAsInspectable<Widget>>(blah, blah);
}

winrt::fire_and_forget Sample()
{
    auto obj = co_await DoSomethingAsync();
    auto& widget = winrt::get_self<ValueAsInspectable<Widget>>(obj)->value;
    widget.Toggle();
}

We make a ValueAsInspectable<Widget>> and put a Widget inside it, constructed from the parameters blah, blah. Upon receiving it, we use the fact that we know that this IInspectable is really a Value­As­Inspectable<­Widget>, and we use get_self to access the backing C++ class and obtain a reference to the value hiding inside.

Note that this reference’s lifetime is controlled by the returned object, so you don’t want to do this:

// Code in italics is wrong
    auto& widget = winrt::get_self<ValueAsInspectable<Widget>>(
            co_await DoSomethingAsync()
        )->value;

Because that saves a reference to an object inside an IInspectable that is about to be destroyed.

This wrapper technique has the downside of requiring you to trust that the IInspectable returned by DoSomething really is wrapping a Widget in the same process. If it’s a remote object, or if it’s wrapping something else, or isn’t a wrapper at all, then you’re grabbing a reference to garbage.

We’ll work on addressing these deficiencies next time, but if you have control over both the producer and consumer of the Value­As­Inspectable, then this version may be sufficient.

We can add some helpers to make it a bit more ergonomic.

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));
}

The first helper lets you write MakeValueAsInspectable(blah), and the type of the Value­As­Inspectable will match the type you passed in (after removing references). This lets you write

co_return MakeValueAsInspectable(42);

instead of

co_return MakeValueAsInspectable<int>(42);

The second overload doesn’t add any value but at least creates consistency, so you can just use Make­Value­As­Inspectable everywhere.

co_return MakeValueAsInspectable(42);
co_return MakeValueAsInspectable<Widget>(blah, blah);

Okay, next time, we’ll look at those safety improvements.

The post How can I write a C++/WinRT <CODE>IAsyncOperation<T></CODE> where <CODE>T</CODE> is not a Windows Runtime types?, part 1 appeared first on The Old New Thing.

Tagged:

Leave a Reply

Your email address will not be published. Required fields are marked *