Skip to content
← Todas las publicaciones
· Delta1 Labs Decompilación.NETGuía

En qué se convierte async/await al decompilar: leer la máquina de estados generada

Decompila un método async y no verás async/await: verás una estructura generada por el compilador con un switch en MoveNext(). Esto es cómo el compilador de C# transforma async en una máquina de estados, cómo leer el tipo <Método>d__N que te muestra un decompilador, y cómo un buen decompilador reconstruye el original.

Escribe un método async pequeño, compílalo, abre el ensamblado en un decompilador y no encontrarás tu método. En su lugar hay una estructura con un nombre impronunciable y un método MoveNext() construido alrededor de un switch. Esto no es corrupción ni ofuscación: es exactamente lo que async/await es. El runtime no tiene ninguna noción de await; el compilador de C# lo implementa reescribiendo tu método como una máquina de estados. Entender esa reescritura es la diferencia entre confundirse con un decompilador y leerlo con fluidez.

El método que escribiste

public async Task<int> GetLengthAsync(HttpClient http, string url)
{
    HttpResponseMessage resp = await http.GetAsync(url);
    string body = await resp.Content.ReadAsStringAsync();
    return body.Length;
}

Dos awaits, un par de locales. Fácil de leer, porque await oculta la parte difícil: suspender el método en cada await, volver al llamador y reanudar más tarde en cualquier hilo que complete la tarea esperada, con todas tus locales intactas.

Lo que genera el compilador

El compilador divide GetLengthAsync en dos piezas. El método que declaraste se convierte en un stub delgado que configura e inicia una máquina de estados:

public Task<int> GetLengthAsync(HttpClient http, string url)
{
    var sm = new <GetLengthAsync>d__0();
    sm.http = http;
    sm.url = url;
    sm.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
    sm.<>1__state = -1;
    sm.<>t__builder.Start(ref sm);
    return sm.<>t__builder.Task;
}

El trabajo real se traslada al tipo generado. Su nombre —<GetLengthAsync>d__0— usa corchetes angulares precisamente porque no es un identificador legal de C#, de modo que nunca puede chocar con nada que escribas. Lleva tus parámetros y locales como campos, un entero de estado, el builder que posee el Task<int> devuelto, y un campo awaiter por cada await:

private struct <GetLengthAsync>d__0 : IAsyncStateMachine
{
    public int <>1__state;
    public AsyncTaskMethodBuilder<int> <>t__builder;
    public HttpClient http;
    public string url;
    private HttpResponseMessage <resp>5__1;
    private TaskAwaiter<HttpResponseMessage> <>u__1;
    private TaskAwaiter<string> <>u__2;

    void IAsyncStateMachine.MoveNext() { /* your method, rewritten */ }
    void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine s) =>
        <>t__builder.SetStateMachine(s);
}

Fíjate en que resp se convirtió en un campo (<resp>5__1), no en una local. Todo cuyo valor deba sobrevivir a un await tiene que elevarse (hoisting) a la máquina de estados, porque el marco de pila del método ha desaparecido mientras está suspendido.

MoveNext: tu método, cortado en cada await

MoveNext() es el núcleo. Cada await se convierte en un punto donde el método puede suspenderse y reentrar más tarde, así que el cuerpo se organiza como un switch sobre el campo de estado. Un MoveNext decompilado se ve aproximadamente así (simplificado, con el manejo de excepciones que exige el builder):

void IAsyncStateMachine.MoveNext()
{
    int state = <>1__state;
    int result;
    try
    {
        TaskAwaiter<HttpResponseMessage> awaiter1;
        if (state != 0)
        {
            awaiter1 = http.GetAsync(url).GetAwaiter();
            if (!awaiter1.IsCompleted)
            {
                <>1__state = 0;                 // remember where we are
                <>u__1 = awaiter1;
                <>t__builder.AwaitUnsafeOnCompleted(ref awaiter1, ref this); // suspend
                return;
            }
        }
        else                                    // resumed into state 0
        {
            awaiter1 = <>u__1;
            <>1__state = -1;
        }
        <resp>5__1 = awaiter1.GetResult();      // the awaited value

        // ... second await on ReadAsStringAsync() follows the same shape, state 1 ...

        result = /* body.Length */ 0;
    }
    catch (Exception ex)
    {
        <>1__state = -2;                        // final
        <>t__builder.SetException(ex);
        return;
    }
    <>1__state = -2;
    <>t__builder.SetResult(result);             // completes the Task<int>
}

Lee los valores de estado y todo se vuelve legible. -1 significa «ejecutándose, no suspendido». 0 y 1 marcan los dos awaits: cuando una tarea esperada no está ya completa, la máquina guarda su estado, aparca el awaiter, llama a AwaitUnsafeOnCompleted (que programa que MoveNext vuelva a ejecutarse cuando la tarea termine) y retorna. Al reanudar, el switch/if salta directo a la rama correspondiente, lee el resultado del awaiter y continúa. -2 es el estado terminal, donde se le dice al builder el resultado final o la excepción, que es cómo se completa tu Task<int>.

-1await #10await #21return-2SetResult / SetException

Por qué el decompilador puede devolverte async/await

Como este patrón es tan regular —un tipo que implementa IAsyncStateMachine, un campo builder, un switch de estado, campos awaiter— un decompilador puede detectarlo y ejecutar la reescritura a la inversa, reconstruyendo el GetLengthAsync que realmente escribiste, con sus expresiones await incluidas. Esa vista reconstruida es la que quieres la mayoría de las veces: muestra la intención. Glass.NET hace esto, y también te deja bajar al <GetLengthAsync>d__0 generado en bruto cuando necesitas ver exactamente qué produjo el compilador.

Ambas vistas valen la pena. El async reconstruido es cómo lees la lógica rápido. La máquina de estados en bruto es cómo razonas sobre el coste: cada await es una posible suspensión y una continuación programada, cada local elevada es un campo mantenido vivo a través de ella, y un método que hace await en un bucle apretado genera una transición de estado en cada vuelta. Cuando persigues una asignación de memoria o un salto de hilo inesperado, leer la máquina —no el async ordenado— es donde está la respuesta.

Así que la próxima vez que un decompilador te muestre una estructura <Algo>d__N con un switch en MoveNext, no estás viendo algo roto. Estás viendo async/await sin ropa, y ahora sabes leerlo.

Prueba Nebula.NET

Endurece tu código .NET en minutos — empieza con la edición gratuita.