Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Where are the generic parameters saved for Async calls? Where to find its name or other information?

Here is my test code: the extension method GetInstructions is from here: https://gist.github.com/jbevain/104001

using System;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            typeof(TestClass)
             .GetMethods()
             .Where(method => method.Name == "Say" || method.Name == "Hello")
             .ToList()
             .ForEach(method =>
                {
                    var calls = method.GetInstructions()
                    .Select(x => x.Operand as MethodInfo)
                    .Where(x => x != null)
                    .ToList();

                    Console.WriteLine(method);
                    calls.ForEach(call =>
                    {
                        Console.WriteLine($"\t{call}");
                        call.GetGenericArguments().ToList().ForEach(arg => Console.WriteLine($"\t\t{arg.FullName}"));
                    });
                });

            Console.ReadLine();
        }
    }
    class TestClass
    {
        public async Task Say()
        {
            await HelloWorld.Say<IFoo>();
            HelloWorld.Hello<IBar>();
        }

        public void Hello()
        {
            HelloWorld.Say<IFoo>().RunSynchronously();
            HelloWorld.Hello<IBar>();
        }
    }

    class HelloWorld
    {
        public static async Task Say<T>() where T : IBase
        {
            await Task.Run(() => Console.WriteLine($"Hello from {typeof(T)}.")).ConfigureAwait(false);
        }

        public static void Hello<T>() where T : IBase
        {
            Console.WriteLine($"Hello from {typeof(T)}.");
        }
    }
    interface IBase
    {
        Task Hello();
    }

    interface IFoo : IBase
    {

    }

    interface IBar : IBase
    {

    }
}

Here is run result as the screenshot shown:

System.Threading.Tasks.Task Say()
        System.Runtime.CompilerServices.AsyncTaskMethodBuilder Create()
        Void Start[<Say>d__0](<Say>d__0 ByRef)
                ConsoleApp1.TestClass+<Say>d__0
        System.Threading.Tasks.Task get_Task()
Void Hello()
        System.Threading.Tasks.Task Say[IFoo]()
                ConsoleApp1.IFoo
        Void RunSynchronously()
        Void Hello[IBar]()
                ConsoleApp1.IBar

NON-ASYNC calls got correct generic parameters, but ASYNC calls cannot.

enter image description here

My question is: where are the generic parameters stored for ASYNC calls?

Thanks a lot.

like image 319
Dongdong Avatar asked Sep 09 '19 19:09

Dongdong


People also ask

Which of the following keyword does async method uses to label suspension point?

The marked async method can use await to designate suspension points.

What happens when you call an async method?

The call to the async method starts an asynchronous task. However, because no Await operator is applied, the program continues without waiting for the task to complete. In most cases, that behavior isn't expected.

Can async methods declare out parameters?

The only way to have supported out-by-reference parameters would be if the async feature were done by a low-level CLR rewrite instead of a compiler-rewrite.

What is async keyword in C#?

The async keyword turns a method into an async method, which allows you to use the await keyword in its body. When the await keyword is applied, it suspends the calling method and yields control back to its caller until the awaited task is complete. await can only be used inside an async method.


2 Answers

The async methods are not that easy.

The C# compiler will generate a comprehensive state machine out of an async method. So the body of the TestClass.Say method will be completely overwritten by the compiler. You can read this great blog post if you want to dive deeper into the async state machinery.

Back to your question.

The compiler will replace the method body with something like this:

<Say>d__0 stateMachine = new <Say>d__0();
stateMachine.<>4__this = this;
stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.<>1__state = -1;
AsyncTaskMethodBuilder <>t__builder = stateMachine.<>t__builder;
<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;

<Say>d__0 in this code is a compiler-generated type. It has special characters it its name to prevent you from being able to use this type in your code.

<Say>d__0 is an IAsyncStateMachine implementation. The main logic is contained in its MoveNext method.

It will look similar to this:

TaskAwaiter awaiter;
if (state != 0)
{
    awaiter = HelloWorld.Say<IFoo>().GetAwaiter();
    if (!awaiter.IsCompleted)
    {
        // ...
        builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
        return;
    }
}
else
{
    awaiter = this.awaiter;
    state = -1;
}

awaiter.GetResult();
HelloWorld.Hello<IBar>();

Note that your HelloWorld.Say<IFoo>() call is now here, in this method, not in your original TestClass.Say.

So, to get the generic type information from your method, you will need to inspect the MoveNext state machine method instead of the original TestClass.Say. Search for the call instructions there.

Something like this:

Type asyncStateMachine = 
    typeof(TestClass)
    .GetNestedTypes(BindingFlags.NonPublic)
    .FirstOrDefault(
        t => t.GetCustomAttribute<CompilerGeneratedAttribute>() != null 
        && typeof(IAsyncStateMachine).IsAssignableFrom(t));

MethodInfo method = asyncStateMachine.GetMethod(
    nameof(IAsyncStateMachine.MoveNext),
    BindingFlags.NonPublic | BindingFlags.Instance);

List<MethodInfo> calls = method.GetInstructions()
    .Select(x => x.Operand as MethodInfo)
    .Where(x => x != null)
    .ToList();

// etc

Output:

Void MoveNext()
        System.Threading.Tasks.Task Say[IFoo]()
                ConsoleApp1.IFoo
        System.Runtime.CompilerServices.TaskAwaiter GetAwaiter()
        Boolean get_IsCompleted()
        Void AwaitUnsafeOnCompleted[TaskAwaiter,<Say>d__0](System.Runtime.CompilerServices.TaskAwaiter ByRef, <Say>d__0 ByRef)
                System.Runtime.CompilerServices.TaskAwaiter
                ConsoleApp1.TestClass+<Say>d__0
        Void GetResult()
        Void Hello[IBar]()
                ConsoleApp1.IBar
        Void SetException(System.Exception)
        Void SetResult()

Note that this code depends on current IAsyncStatMachine implementation internals. If the C# compiler changes that internal implementation, this code might break.

like image 138
dymanoid Avatar answered Oct 14 '22 16:10

dymanoid


You can try getting the generic method info and that way you can find the IFoo generic type argument from this (code taken from the msdn):

private static void DisplayGenericMethodInfo(MethodInfo mi)
    {
        Console.WriteLine("\r\n{0}", mi);

        Console.WriteLine("\tIs this a generic method definition? {0}", 
            mi.IsGenericMethodDefinition);

        Console.WriteLine("\tIs it a generic method? {0}", 
            mi.IsGenericMethod);

        Console.WriteLine("\tDoes it have unassigned generic parameters? {0}", 
            mi.ContainsGenericParameters);

        // If this is a generic method, display its type arguments.
        //
        if (mi.IsGenericMethod)
        {
            Type[] typeArguments = mi.GetGenericArguments();

            Console.WriteLine("\tList type arguments ({0}):", 
                typeArguments.Length);

            foreach (Type tParam in typeArguments)
            {
                // IsGenericParameter is true only for generic type
                // parameters.
                //
                if (tParam.IsGenericParameter)
                {
                    Console.WriteLine("\t\t{0}  parameter position {1}" +
                        "\n\t\t   declaring method: {2}",
                        tParam,
                        tParam.GenericParameterPosition,
                        tParam.DeclaringMethod);
                }
                else
                {
                    Console.WriteLine("\t\t{0}", tParam);
                }
            }
        }
    }
like image 31
Jon Avatar answered Oct 14 '22 16:10

Jon