January 23rd, 2023

Inside C++/WinRT: Coroutine completions: The oversimplified version

C++/WinRT coroutines use the Completed delegate property to be notified when an asynchronous operation is complete. There are multiple parts of the completion handler. Today we’ll look at an oversimplified version, and then we will gradually build it up.

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
    {
        async.Completed([handle](auto&& ...)
        {
            handle.resume();
        });
    }

    auto await_resume() const
    {
        return async.GetResults();
    }
};

To co_await an IAsyncAction or IAsyncOperation, we register a completion callback that resumes the awaiting coroutine. When the coroutine resumes, it will call await_resume() to obtain the result of the co_await, and we propagate the result of the GetResults() method.

We don’t particularly care about the parameters passed to the completion delegate: Those tell us whether the asynchronous work completed successfully, was cancelled, or failed outright, but we don’t need to remember that information because GetResults() will report the information again: If the asynchronous work did not complete successfully, then GetResults() will thrown an exception describing why it was not successful.

As things go, this is a fairly standard implementation of a coroutine awaiter, although there is a race condition we need to fix:

    void await_suspend(std::experimental::coroutine_handle<> handle) const
    {
        auto extend_lifetime = async;
        async.Completed([handle](auto&& ...)
        {
            handle.resume();
        });
    }

If the completion handler runs before Completed() returns, then we end up destroying the async while there is still an active call on it. This is a problem I called out at the start of my coroutine series. To fix this, we make a local copy of the async to extend its lifetime to the end of the await_suspend function.

This is just the starting point for C++/WinRT coroutine completion handlers. Next time, we’ll add apartment-preserving behavior.

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.

3 comments

Discussion is closed. Login to edit/delete existing comments.

  • 紅樓鍮 · Edited

    Is it undefined behavior to simply have destroyed during the execution of a non-static member function, even if that function no longer accesses ?

    <code>

    If this is not UB, then shouldn't need ; we can assume itself is written correctly to not use (which is our ) after scheduling for to execute, and we don't need to extend 's lifetime ourselves because we don't access any more.

    Read more
    • sugrob 9000

      That aside, calling the destructor from within a non-static member function is, on its own, legal. Of course, this is extremely prone to making subsequent code exhibit UB, such as in this case.

    • Raymond ChenMicrosoft employee Author

      There is no requirement that Async::Completed avoid *this after calling the handler. In fact, if the Async is remote, it will definitely access *this, because will have to tear down the channel and proxy.