C++/WinRT: Producing Async Objects

Previous: Handling Async Completion

Now that we’ve explored the async interfaces and some completion mechanics in general, let’s turn our attention to creating or producing implementations of those four async interfaces. As we’ve already learned, implementing WinRT interfaces with C++/WinRT is very simple. I might for example implement IAsyncAction as follows:

struct MyAsync : implements<MyAsync, IAsyncAction, IAsyncInfo>
{
    // IAsyncInfo members...
    uint32_t Id() const;
    AsyncStatus Status() const;
    HRESULT ErrorCode() const;
    void Cancel() const;
    void Close() const;

    // IAsyncAction members...
    void Completed(AsyncActionCompletedHandler const& handler) const;
    AsyncActionCompletedHandler Completed() const;
    void GetResults() const;
}; 

The difficulty comes when you consider how you might implement those methods. While it is not hard to imagine some implementation, it is almost impossible to do it correctly without first reverse engineering how the existing language projections actually implement them. You see, the WinRT async pattern only works if everyone implements these interfaces using a very specific state machine and in exactly the same way. Each language projection makes the same assumptions about how this state machine is implemented and if you happen to implement it in a slightly different way, then bad things will happen.

Thankfully, you don’t have to worry about this because each language projection, with the exception of C++/CX, already implements this correctly for you. Here’s a complete implementation of IAsyncAction thanks to C++/WinRT’s coroutine support:

IAsyncAction CopyAsync()
{
    co_return;
}

Now this isn’t a particularly interesting implementation, but it is very educational and a good example of just how much C++/WinRT is doing for you. Since this is a complete implementation, we can use it to exercise some of what we’ve learned thus far. The CopyAsync function above is a coroutine. The coroutine’s return type is used to stitch together an implementation of both IAsyncAction and IAsyncInfo and the C++ compiler brings it to life at just the right moment. We’ll explore some of those details later, but for now let’s observe how this coroutine works. Consider the following console app:

IAsyncAction CopyAsync()
{
    co_return;
}

int main()
{
    IAsyncAction async = CopyAsync();

    async.get();
}

The main function calls the CopyAsync function, which returns an IAsyncAction. If you forget for a moment what the CopyAsync function’s body or definition looks like, it should be evident that it is just a function that returns an IAsyncAction object. We can therefore use it in all the ways that we’ve already learned.

A coroutine (of this sort) must have a co_return statement or a co_await statement. It may of course have multiple, but it must have at least one of these in order to actually be a coroutine. As you might expect, a co_return statement does not introduce any kind of suspension or asynchrony. Therefore, this CopyAsync function produces an IAsyncAction that completes immediately or synchronously. I can illustrate this as follows:

IAsyncAction Async()
{
    co_return;
}

int main()
{
    IAsyncAction async = Async();
    assert(async.Status() == AsyncStatus::Completed);
}

The assertion is guaranteed to be true. There is no race here. Since CopyAsync is just a function, the caller is blocked until it returns and the first opportunity for it to return happens to be the co_return statement. What this means is that if you have some async contract that you need to implement, but the implementation does not actually need to introduce any asynchrony, then it can simply return the value directly and without blocking or introducing a context switch. Consider a function that downloads and then returns a cached value:

hstring m_cache;

IAsyncOperation<hstring> ReadAsync()
{
    if (m_cache.empty())
    {
        // Download and cache value...
    }

    co_return m_cache;
}

int main()
{
    hstring message = ReadAsync().get();
    printf("%ls\n", message.c_str());
}

The first time ReadAsync is called, the cache is presumably empty, and the result is downloaded. Presumably this will suspend the coroutine itself while this takes place. We’ll talk more about how exactly suspension works a little later. Suspension implies that execution returns to the caller. The caller is handed an async object that has not in fact completed, hence the need to somehow wait for completion.

The beauty of coroutines is that there’s a single abstraction both for producing async objects and for consuming those same async objects. An API or component author might implement an async method as described above, but an API consumer or app developer may also use coroutines to call and wait for their completion. Let’s now rewrite the main function above to use a coroutine to do the waiting:

IAsyncAction MainAsync()
{
    hstring result = co_await ReadAsync();
    printf("%ls\n", result.c_str());
}

int main()
{
    MainAsync().get();
}

I have essentially taken the body of the old main function and moved it into the MainAsync coroutine. The main function uses the get method to prevent the app from terminating while the app completes asynchronously. The MainAsync function has something new and that’s the co_await statement. Rather than using the get method to block the calling thread until ReadAsync completes, the co_await statement is used to wait for the ReadAsync function to complete in a cooperative or non-blocking manner. This is what I meant by a suspension point. The co_await statement represents a suspension point. This app only calls ReadAsync once, but you can imagine it being called multiple times in a more interesting app. The first time it gets called, the MainAsync coroutine will actually suspend and return control to its caller. The second time its called, it will not suspend at all but rather return the value directly.

Coroutines are very new to many C++ developers so don’t feel bad if this still seems rather magical. We’ll continue to explore coroutines over the next few installments and these concepts should become quite clear. The good news is that you already know enough to begin to make effective use of coroutines to consume async APIs provided by Windows. For example, you should be able to reason about how the following console app works:

#include "winrt/Windows.Web.Syndication.h"

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

IAsyncAction MainAsync()
{
    Uri uri(L"https://kennykerr.ca/feed");
    SyndicationClient client;
    SyndicationFeed feed = co_await client.RetrieveFeedAsync(uri);

    for (auto&& item : feed.Items())
    {
        hstring title = item.Title().Text();

        printf("%ls\n", title.c_str());
    }
}

int main()
{
    init_apartment();
    MainAsync().get();
}

Give it a try right now and see just how much fun it is to use modern C++ on Windows.