July 8th, 2020

Cancelling a Windows Runtime asynchronous operation, part 6: C++/WinRT-generated asynchronous operations

Last time, we learned that C++/WinRT defers to the underlying asynchronous operation to report the cancellation in whatever way it sees fit. Today, we’ll look at the case that the asynchronous operation was generated by the C++/WinRT library.

When you invoke the Cancel() on a C++/WinRT asynchronous operation, this is the code that runs:

struct promise_base : ...
{
    ...

    void Cancel() noexcept
    {
        winrt::delegate<> cancel;
        {
            slim_lock_guard const guard(m_lock);
            if (m_status == AsyncStatus::Started)
            {
                m_status = AsyncStatus::Canceled;
                cancel = std::move(m_cancel);
            }
        }
        if (cancel)
        {
            cancel();
        }
    }
};

The promise transitions into the Canceled, and if the coroutine had registered a cancellation callback, it is invoked.

Whenever the coroutine associated with the promise performs a co_await, the await_transform kicks in (sorry, I haven’t explained this yet, but trust me), and that’s where the C++/WinRT library gets a chance to abandon the operation:

template <typename Expression>
Expression&& await_transform(Expression&& expression)
{
    if (Status() == AsyncStatus::Canceled)
    {
        throw winrt::hresult_canceled();
    }
    return std::forward<Expression>(expression);
}

The thrown hresult_canceled exception is captured into the operation for later rethrowing.

The C++/WinRT library also checks for cancellation when the coroutine runs to completion:

struct promise_type final : ...
{
    ...

    void return_void()
    {
        ...
        if (this->m_status == AsyncStatus::Started)
        {
            this->m_status = AsyncStatus::Completed;
        }
        else
        {
            WINRT_ASSERT(this->m_status == AsyncStatus::Canceled);
            this->m_exception = make_exception_ptr(winrt::hresult_canceled());
        }
        ...
    }
};

If the operation has been cancelled, then we manufacture a fake hresult_canceled exception and save it in the m_exception.

So we see that whether the operation’s cancellation is detected by await_transform or by return_void (or return_value for coroutines that produce a value), we end up with an hresult_canceled exception stashed in the operation.

And it is this exception that comes back out when somebody asks for the result of the asynchronous activity:

struct promise_type final : ...
{
    ...

    void GetResults()
    {
        ...
        if (this->m_status == AsyncStatus::Completed)
        {
            return;
        }
        this->rethrow_if_failed();
        ...
    }

    void rethrow_if_failed() const
    {
        if (m_status == AsyncStatus::Error || m_status == AsyncStatus::Canceled)
        {
            std::rethrow_exception(m_exception);
        }
    }
};

If the operation was canceled, then we reach rethrow_if_failed which rethrows the captured exception, which we saw earlier is going to be an hresult_canceled.

But C++/WinRT is not the only source of IAsync­Action and IAsync­Operation objects. Next time, we’ll look at another major source: WRL.

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.

  • Neil Rashbrook

    Presumably because I don’t know about await_transform yet, I’m confused as to why the cancellation needs to be checked in two places.

    • Raymond ChenMicrosoft employee Author

      The await-transform lets us cancel the coroutine as soon as it performs its next co_await. Otherwise, we wouldn’t cancel it until it completed, which could be a long time coming.

  • word merchant

    All very smart, but it doesn’t explain why Teams is so awful.