One of the features of C++/WinRT is that if you co_await an IAsyncAction or IAsyncOperation, the C++/WinRT library returns to the original COM apartment before resuming the coroutine. This behavior is generally desirable because you expect that COM objects prior to performing a co_await are still usable after it returns.
This task is accomplished with the assistance of IContextCallback.
Here’s the basic idea:¹
inline int32_t __stdcall resume_apartment_callback(
com_callback_args* args) noexcept
{
coroutine_handle<>::from_address(args->data)();
return 0;
};
void resume_apartment(
com_ptr<IContextCallback> const& context,
std::coroutine_handle<> handle)
{
com_callback_args args{};
args.data = handle.address();
check_hresult(
context->ContextCallback(resume_apartment_callback,
&args,
guid_of<ICallbackWithNoReentrancyToApplicationSTA>(),
5, nullptr));
}
To resume a coroutine synchronously in a particular context, we use the IContextÂCallback:: method to ask COM to run a particular function in that desired context. We convert the coroutine handle to a pointer to use as our reference data, and in the callback, we convert the pointer back to a coroutine handle so we can invoke it, thereby resuming the coroutine.
We can use this to build the apartment_ object.
struct apartment_context
{
apartment_context() = default;
apartment_context(std::nullptr_t) : context(nullptr) { }
operator bool() const noexcept { return context != nullptr; }
bool operator!() const noexcept { return context == nullptr; }
com_ptr<IContextCallback> context =
capture<IContextCallback>(WINRT_IMPL_CoGetObjectContext);
};
struct apartment_awaiter
{
apartment_context const& context;
bool await_ready() const noexcept
{
return false;
}
void await_suspend(coroutine_handle<> handle)
{
apartment_context extend_lifetime = context;
resume_apartment(context.context, handle);
}
void await_resume() const noexcept
{
}
};
apartment_awaiter operator co_await(apartment_context const& context)
{
return { context };
}
To construct an apartment_, we call CoÂGetÂObjectÂContext (through the C++/WinRT alias) to obtain an IContextÂCallback.
There is also a nullptr constructor if you want to declare an empty apartment_context. Empty contexts aren’t usable, but they are useful: They let you declare a variable and initialize it with a proper context later.
To co_await an apartment_, we construct an apartment_ which remembers the context being awaited, and the await_ method uses it to call resume_.
We can now add COM context support to our oversimplified Windows Runtime awaiter.
template <typename Async>
struct await_adapter
{
await_adapter(Async const& async) : async(async) { }
Async const& async;
bool await_ready() const noexcept
{
return false;
}
void await_suspend(std::experimental::coroutine_handle<> handle) const
{
auto extend_lifetime = async;
async.Completed([
handle,
context = apartment_context()
](auto&& ...)
{
resume_apartment(context.context, handle);
});
}
auto await_resume() const
{
return async.GetResults();
}
};
We capture an apartment_ in the lambda and use resume_ to resume the coroutine in that captured context.
This code is still flawed, though. We’ll continue the discussion next time.
¹ The C++/WinRT library does not #include <windows.h>. All of the dependencies on Windows are wrapped inside parallel declarations within the C++/WinRT library. The com_ structure, for example, is an ABI-equivalent version of the ComCallData structure.
0 comments