Ce que async/await donne à la décompilation : lire la machine à états générée
Décompilez une méthode async et vous ne voyez pas async/await — vous voyez une structure générée par le compilateur avec un switch dans MoveNext(). Voici comment le compilateur C# transforme async en machine à états, comment lire le type <Méthode>d__N que montre un décompilateur, et comment un bon décompilateur reconstruit l'original.
Écrivez une petite méthode async, compilez-la, ouvrez l”assembly dans un décompilateur, et vous ne trouverez pas votre méthode. À sa place, une structure au nom imprononçable et une méthode MoveNext() bâtie autour d”un switch. Ce n”est ni une corruption ni de l”obfuscation : c”est exactement ce qu”async/await est. Le runtime n”a aucune notion d”await ; le compilateur C# l”implémente en réécrivant votre méthode en machine à états. Comprendre cette réécriture fait la différence entre être dérouté par un décompilateur et le lire couramment.
La méthode que vous avez écrite
public async Task<int> GetLengthAsync(HttpClient http, string url)
{
HttpResponseMessage resp = await http.GetAsync(url);
string body = await resp.Content.ReadAsStringAsync();
return body.Length;
}
Deux awaits, deux ou trois variables locales. Facile à lire — parce qu”await cache le plus dur : suspendre la méthode à chaque await, revenir à l”appelant, et reprendre plus tard sur le thread qui complète la tâche attendue, avec toutes vos locales intactes.
Ce que le compilateur génère
Le compilateur scinde GetLengthAsync en deux morceaux. La méthode que vous avez déclarée devient un stub mince qui configure et démarre une machine à états :
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;
}
Le vrai travail passe dans le type généré. Son nom — <GetLengthAsync>d__0 — utilise des chevrons précisément parce que ce n”est pas un identifiant C# valide, de sorte qu”il ne peut jamais entrer en collision avec ce que vous écrivez. Il porte vos paramètres et vos locales sous forme de champs, un entier d”état, le builder qui possède la Task<int> renvoyée, et un champ awaiter par 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);
}
Remarquez que resp est devenu un champ (<resp>5__1), pas une locale. Tout ce dont la valeur doit survivre à un await doit être hoisté sur la machine à états, car le cadre de pile de la méthode a disparu tant qu”elle est suspendue.
MoveNext : votre méthode, coupée à chaque await
MoveNext() en est le cœur. Chaque await devient un point où la méthode peut se suspendre puis re-rentrer plus tard, donc le corps est organisé en switch sur le champ d”état. Un MoveNext décompilé ressemble à peu près à ceci (simplifié, avec la gestion d”exceptions qu”exige le 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>
}
Lisez les valeurs d”état et tout devient lisible. -1 signifie « en cours d”exécution, non suspendu ». 0 et 1 marquent les deux awaits : quand une tâche attendue n”est pas déjà complète, la machine enregistre son état, gare l”awaiter, appelle AwaitUnsafeOnCompleted (qui planifie la ré-exécution de MoveNext quand la tâche se termine) et retourne. À la reprise, le switch/if saute directement à la branche correspondante, lit le résultat de l”awaiter et continue. -2 est l”état terminal, où l”on indique au builder le résultat final ou l”exception — ce qui complète votre Task<int>.
Pourquoi le décompilateur peut vous rendre async/await
Comme ce motif est si régulier — un type implémentant IAsyncStateMachine, un champ builder, un switch d”état, des champs awaiter — un décompilateur peut le détecter et exécuter la réécriture à l”envers, reconstruisant le GetLengthAsync que vous avez réellement écrit, expressions await comprises. Cette vue reconstruite est celle que vous voulez la plupart du temps : elle montre l”intention. Glass.NET le fait, et vous laisse aussi descendre au <GetLengthAsync>d__0 généré brut quand vous devez voir exactement ce que le compilateur a produit.
Les deux vues sont payantes. L”async reconstruit, c”est comme ça qu”on lit la logique vite. La machine à états brute, c”est comme ça qu”on raisonne sur le coût : chaque await est une suspension potentielle et une continuation planifiée, chaque locale hoistée est un champ maintenu en vie à travers elle, et une méthode qui await dans une boucle serrée génère une transition d”état à chaque tour. Quand vous traquez une allocation ou un saut de thread inattendu, lire la machine — pas l”async bien rangé — c”est là qu”est la réponse.
Alors la prochaine fois qu”un décompilateur vous montre une structure <Quelque-chose>d__N avec un switch dans MoveNext, vous ne regardez pas quelque chose de cassé. Vous regardez async/await sans ses habits — et désormais vous savez le lire.
Essayez Nebula.NET
Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.