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::), 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.
Any reason a deduction guide wouldn’t work instead of having two versions of the class?