Eduasync part 2: the shape of the caller / async method boundary

For the first few parts of this blog series, we’re not going to write code which actually does anything useful. We’re looking at the API more than the implementation – the bare necessities to get the code to compile. Remember that we’re working without AsyncCtpLibrary.dll – which of course supplies all the necessary types normally.

In this part, we’ll look at the boundary between the caller and the async method itself. Our test code will have a method which does nothing but return – but which is marked with the “async” modifier. The fact that we don’t have an “await” expression in the method causes a compiler warning – which is entirely reasonable, but irrelevant to our particular situation.

Three return types, one basic idea

Async methods are limited to three different return types:

  • void
  • Task (non-generic)
  • Task<T> (generic for any type T)

Each of these return types needs a very slightly different form of compiler support… but the general principle is the same. I’ll show each of the three different types, but the test code itself remains exactly the same except for the return type of the method and the return statement. (If you’re going to return Task<int>, you need to return an int, etc.)

namespace Eduasync
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            DoNothingAsync();
        }

// Warning CS1998 is about a method with no awaits in… exactly what we’re trying to
// achieve!
#pragma warning disable 1998 
        // Return type of void, Task or Task<int>
        private static async void DoNothingAsync()
        {
            // For Task<int> insert return 0; here
        }
#pragma warning restore 1998
    }
}

If you try to compile this code without anything else, the compiler gives an error like this:

Test.cs(14,30): error CS0656: Missing compiler required member ‘System.Runtime.CompilerServices.AsyncVoidMethodBuilder.Create’
Test.cs(14,30): error CS1993: Cannot find Task-related types. Are you missing a reference to ‘AsyncCtpLibrary.dll’ ?

While you’d get a similar error for the async method / awaitable boundary, this one is more demanding: you have to have exactly the right types available. It isn’t just a matter of any library providing support for your current situation (in the same way that Edulinq can live in its own namespace and be an alternative for LINQ to Objects without the compiler caring). These are exact types that the compiler relies on. They’re an implementation detail: they can vary between compilers (so Mono could have a different set of types for example, although I doubt that it will, for the sake of binary compatibility) and they’re not part of the language specification… at least at the moment.

There are three types required: AsyncVoidMethodBuilder, AsyncTaskMethodBuilder and AsyncTaskMethodBuilder<T>. They correspond (fairly obviously) to the return types of void, Task, and Task<T> respectively.

They all look largely the same, but here are my initial "non-implementations":

using System.Threading.Tasks;

namespace System.Runtime.CompilerServices
{
    public struct AsyncVoidMethodBuilder
    {
        public static AsyncVoidMethodBuilder Create()
        {
            return new AsyncVoidMethodBuilder();
        }

        public void SetException(Exception e) {}
        public void SetResult() {}
    }

    public struct AsyncTaskMethodBuilder
    {
        public static AsyncTaskMethodBuilder Create()
        {
            return new AsyncTaskMethodBuilder();
        }

        public void SetException(Exception e) {}
        public void SetResult() {}
        public Task Task { get { return null; } }
    }

    public struct AsyncTaskMethodBuilder<T>
    {
        public static AsyncTaskMethodBuilder<T> Create()
        {
            return new AsyncTaskMethodBuilder<T>();
        }

        public void SetException(Exception e) {}
        public void SetResult(T result) {}
        public Task<T> Task { get { return null; } }
    }
}

(In the Eduasync project these are in separate files and have diagnostic statements in the SetException/SetResult methods. I didn’t want to take up too much space here.)

Note that these must be structs. If they’re not, the compiler will complain. All the methods have to have the exact signatures specified, as far as I can tell – except that everything can be internal if you want it to be (so long as your async methods are in the same assembly of course). In practice these are going to be public, and they are public everywhere in Eduasync.

With these implementations, you can compile and even run async methods – but you will, of course, end up with a null reference returned from any async method with a return type of Task or Task<T>.

In case you’re wondering, the code below shows a little taste of how these types are used. It’s from the version of DoNothingAsync which returns a non-generic Task. This is what the compiler replaces our code with:

private static Task DoNothingAsync()

    <DoNothingAsync>d__0 d__ = new <DoNothingAsync>d__0(0);
    d__.<>t__MoveNextDelegate = new Action(d__.MoveNext);
    d__.$builder = AsyncTaskMethodBuilder.Create();
    d__.MoveNext(); 
    return d__.$builder.Task; 
}

Obviously <DoNothingAsync>d__0 is a compiler-generated type (hence the unspeakable name). I’m not going into the details just yet – that will the topic of several posts in a little while. The main point of this post was just to show you the shape of what the compiler requires. The Create() method and the Task property are used within the rewritten method; the SetResult() and SetException() methods are used within the generated type (the state machine) to indicate the async method completing.

Before too long we’ll implement these types properly (which is reasonably straightforward, given help from the BCL).

Conclusion

The caller / async method boundary is relatively inflexible. The compiler relies on particular types, and you simply can’t make an async method return a different kind of value, such as your own Future<T> type. The guts of how it works are all hidden from you, unless you try to compile without the right libraries around: while the valid return types are part of the specification, the types used by the compiler aren’t. While this is perhaps a little unfortunate in a purist sense, it’s not really a big deal. The "consuming" part of the async method (the boundary between the async method and whatever it’s awaiting) is much more flexible, and more interesting. That’s what we’re going to look at next.

5 thoughts on “Eduasync part 2: the shape of the caller / async method boundary”

  1. I think you’re missing the variable name (d__) in what should be its declaration in the listing of the generated code.

    Like

  2. The code in the last example seems wrong. You would get this exception with it:

    error CS0165: Use of unassigned local variable ‘d__’

    Why would the d__0 type have a delegate that refers to its own method?

    Like

  3. @configurator: You’re half right :) The sample is wrong in terms of the unassigned local variable – I’m fixing that now.

    But it *does* keep a reference to a delegate referring to its own method. It needs to potentially pass that delegate to OnCompleted several times; this way they can create it just the once.

    Like

Leave a comment