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::
), 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 DoSomethingAsync
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 ValueAsInspectable<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 ValueAsInspectable
, 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 ValueAsInspectable
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 MakeValueAsInspectable
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.