SharpDevelop Community

Get your problems solved!
Welcome to SharpDevelop Community Sign in | Join | Help
in Search

Daniel Grunwald

Decompiling Async/Await

Just after the ILSpy 2.0 release, I started adding support for decompiling C# 5 async/await to ILSpy.

You can get the async-enabled ILSpy build from our build server.

The async support is not yet complete; for example decompilation fails if the IL evaluation stack is not empty at the point of the await expression.

The decompilation logic highly depends on the patterns produced by the C# 5 compiler - it only works with code compiled with the C# compiler in the .NET 4.5 beta release, not with any previous CTPs. Also, it is likely that ILSpy will need adjustments for the final C# 5 compiler.

While testing, I found that the .NET 4.5 beta BCL was not compiled with the beta compiler - where the beta compiler uses multiple awaiter fields, the BCL code uses a single field of type object and uses arrays of length 1. This is similar to the code generated by the .NET 4.5 developer preview, so my guess is that Microsoft used some internal version in between the developer preview and the beta for compiling the .NET 4.5 beta BCL. For more information, take a look at Jon Skeet's description of the async codegen changes.
This means the ILSpy cannot decompile async methods in the .NET 4.5 beta BCL. This problem should disappear with the next .NET 4.5 release (.NET 4.5 RC?).

So how does ILSpy decompile async methods, then? Consider the compiler-generated code of the move next method:

// Async.$AwaitInLoopCondition$d__17
void IAsyncStateMachine.MoveNext()
{
    try
    {
        int num = this.$1__state;
        TaskAwaiter<bool> taskAwaiter;
        if (num == 0)
        {
            taskAwaiter = this.$u__$awaiter18;
            this.$u__$awaiter18 = default(TaskAwaiter<bool>);
            this.$1__state = -1;
            goto IL_7C;
        }
        IL_23:
        taskAwaiter = this.$4__this.SimpleBoolTaskMethod().GetAwaiter();
        if (!taskAwaiter.IsCompleted)
        {
            this.$1__state = 0;
            this.$u__$awaiter18 = taskAwaiter;
            this.$t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<bool>, Async.$AwaitInLoopCondition$d__17>(ref taskAwaiter, ref this);
            return;
        }
        IL_7C:
        bool arg_8B_0 = taskAwaiter.GetResult();
        taskAwaiter = default(TaskAwaiter<bool>);
        if (arg_8B_0)
        {
            Console.WriteLine("Body");
            goto IL_23;
        }
    }
    catch (Exception exception)
    {
        this.$1__state = -2;
        this.$t__builder.SetException(exception);
        return;
    }
    this.$1__state = -2;
    this.$t__builder.SetResult();
}

The state machine works similar to the one used by yield return; so we could reuse a lot of the code from the yield return decompiler.
Each try block begins with a state dispatcher: depending on the value of this.$1__state, the code jumps to the appropriate location. If the async method involves exception handling, there will be a separate state dispatcher at the beginning of each try block.
In this case, there are only two states: the initial state (state = -1) and the state at the await expression (state = 0). The state dispatcher consists only of the two statements "int num = this.$1__state; if (num == 0)". We rely on the fact that in the actual IL code, the state dispatcher is a contiguous sequence of IL instructions, in front of any of the method's actual code.

Note that the async/await decompiler step runs on the ILAst very early in the decompiler pipeline, immediately after the yield return transform, which is prior to any control flow analysis. We're basically still dealing with IL instructions here; but I'm explaining it in terms of C# as that is easier to read (and makes the code much shorter).

The analysis of the state dispatcher works using symbolic execution; it is described in more detail in the yield return decompiler explanation. In our example, the result of the analysis is that the beginning of the first if statement is reached for state==0, and label IL_23 is reached for all other states.

With this information, we start cleaning up the control flow of the method. We look for any 'return;' statements and analyze the instructions directly in front:

            this.$1__state = 0;
            this.$u__$awaiter18 = taskAwaiter;
            this.$t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<bool>, Async.$AwaitInLoopCondition$d__17>(ref taskAwaiter, ref this);
            return;

We then replace this piece code with an instruction that represents the AwaitUnsafeOnCompleted call (represented as "await ref taskAwaiter;" in the following code), followed by a goto to the label for the target state (using the information gained from the symbolic execution). We also remove the boilerplate associated with the $t__builder and the state dispatcher. For demonstration purposes, I'll skip the remaining steps of the async/await decompiler and resume the pipeline to decompile the ILAst to C#, producing the following code:

public async void AwaitInLoopCondition()
{
    while (true)
    {
        TaskAwaiter<bool> taskAwaiter = this.$4__this.SimpleBoolTaskMethod().GetAwaiter();
        if (!taskAwaiter.IsCompleted)
        {
            await ref taskAwaiter;
            taskAwaiter = this.$u__$awaiter18;
            this.$u__$awaiter18 = default(TaskAwaiter<bool>);
            this.$1__state = -1;
        }
        bool arg_8B_0 = taskAwaiter.GetResult();
        taskAwaiter = default(TaskAwaiter<bool>);
        if (!arg_8B_0)
        {
            break;
        }
        Console.WriteLine("Body");
    }
}

As you can see, this transformation has simplified the control flow of the method dramatically.

We now just perform some finishing touches on the method:

  • Access to the state machine fields is replaced with local variable access, e.g. "this.$4__this" becomes "this".
  • We detect the "GetAwaiter() / if (!taskAwaiter.IsCompleted) / GetResult() / clear awaiter" pattern and replace it with a simple await expression

Mind that all of this isn't done on the C# representation, but in an early stage of the ILAst pipeline. After some simplifications (variable inlining, copy propagation), the resulting ILAst looks like this:

br(IL_23)
IL_16:
call(Console::WriteLine, ldstr("Body"))
IL_23:
brtrue(IL_16, await(callvirt(Async::SimpleBoolTaskMethod, ldloc(this))))
ret()

Apart from the 'await' opcode, this is exactly the same as the while-loop would look in a non-async method. The remainder of the decompiler pipeline will detect the loop and translate it to the C# code you've seen in the introductory screenshot.

Published Apr 16 2012, 02:23 PM by DanielGrunwald
Filed under: ,

Comments

No Comments
Powered by Community Server (Commercial Edition), by Telligent Systems
Don't contact us via this (fleischfalle@alphasierrapapa.com) email address.