From 6111367532faf47a969aac3c84c9979ec2f1adec Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Wed, 22 Jan 2025 15:35:33 -0800 Subject: [PATCH] Update runtime async spec Updates from the runtime side: https://github.com/dotnet/runtime/pull/110420. --- docs/compilers/CSharp/Runtime Async Design.md | 154 +++++++++++------- 1 file changed, 99 insertions(+), 55 deletions(-) diff --git a/docs/compilers/CSharp/Runtime Async Design.md b/docs/compilers/CSharp/Runtime Async Design.md index e577604505e9f..91238b94eba2d 100644 --- a/docs/compilers/CSharp/Runtime Async Design.md +++ b/docs/compilers/CSharp/Runtime Async Design.md @@ -27,20 +27,32 @@ We use the following helper APIs to indicate suspension points to the runtime, i ```cs namespace System.Runtime.CompilerServices; -// These methods are used to await things that cannot use runtime async signature form // TODO: Clarify which of these should be preferred? Should we always emit the `Unsafe` version when awaiting something that implements `ICriticalNotifyCompletion`? namespace System.Runtime.CompilerServices; public static class RuntimeHelpers { - [RuntimeAsyncMethod] + // These methods are used to await things that cannot use the Await helpers below + [MethodImpl(MethodImplOptions.Async)] public static Task AwaitAwaiterFromRuntimeAsync(TAwaiter awaiter) where TAwaiter : INotifyCompletion; - [RuntimeAsyncMethod] + [MethodImpl(MethodImplOptions.Async)] public static Task UnsafeAwaitAwaiterFromRuntimeAsync(TAwaiter awaiter) where TAwaiter : ICriticalNotifyCompletion; + + // These methods are used to directly await method calls + // TODO: Do we only use these when directly awaiting a method call, or do we always use these when a `Task` is awaited, even if that task is a local? + [MethodImpl(MethodImplOptions.Async)] + public static void Await(Task task); + [MethodImpl(MethodImplOptions.Async)] + public static T Await(Task task); + [MethodImpl(MethodImplOptions.Async)] + public static void Await(ValueTask task); + [MethodImpl(MethodImplOptions.Async)] + public static T Await(ValueTask task); } ``` -We presume the following `MethodImplOptions` bit is present. This is used to indicate to the JIT that it should generate an async state machine for the method. +We presume the following `MethodImplOptions` bit is present. This is used to indicate to the JIT that it should generate an async state machine for the method. This bit is not allowed to be used manually on any method; it is added by the compiler +to an `async` method. ```cs namespace System.Runtime.CompilerServices; @@ -77,6 +89,8 @@ public class RuntimeAsyncMethodGenerationAttribute(bool runtimeAsync) : Attribut As mentioned previously, we try to expose as little of this to initial binding as possible. The one major exception to this is our handling of the `MethodImplOption.Async`; we do not let this be applied to user code, and will issue an error if a user tries to do this by hand. +TODO: We may need special handling for the implementation of the `RuntimeHelpers.Await` methods in corelib to permit usage of `MethodImplOptions.Async` directly, as they will not be `async` as we think of it in C#. + Compiler generated async state machines and runtime generated async share some of the same building blocks. Both need to have `await`s with in `catch` and `finally` blocks rewritten to pend the exceptions, perform the `await` outside of the `catch`/`finally` region, and then have the exceptions restored as necessary. @@ -132,8 +146,15 @@ class C await C.M(); ``` +Translated C#: + +```cs +System.Runtime.CompilerServices.RuntimeHelpers.Await(C.M()); +``` + ```il -call modreq(class [System.Runtime]System.Threading.Tasks.Task) void C::M() +call [System.Runtime]System.Threading.Tasks.Task C::M() +call void [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task) ``` --------------------------- @@ -148,9 +169,17 @@ class C } ``` +Translated C#: + +```cs +var c = new C(); +System.Runtime.CompilerServices.RuntimeHelpers.Await(c.M()); +``` + ```il newobj instance void C::.ctor() -callvirt instance modreq(class [System.Runtime]System.Threading.Tasks.Task) void C::M() +callvirt instance class [System.Runtime]System.Threading.Tasks.Task C::M() +call void [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task) ``` #### Await a concrete `T` `Task`-returning method @@ -164,8 +193,15 @@ class C } ``` +Translated C#: + +```cs +int i = System.Runtime.CompilerServices.RuntimeHelpers.Await(C.M()); +``` + ```il -call modreq(class [System.Runtime]System.Threading.Tasks.Task`1) int32 C::M() +call class [System.Runtime]System.Threading.Tasks.Task`1 C::M() +call int32 [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task`1) stloc.0 ``` @@ -181,9 +217,17 @@ class C } ``` +Translated C#: + +```cs +var c = new C(); +int i = System.Runtime.CompilerServices.RuntimeHelpers.Await(c.M()); +``` + ```il newobj instance void C::.ctor() -callvirt instance modreq(class [System.Runtime]System.Threading.Tasks.Task`1) int32 C::M() +callvirt instance class [System.Runtime]System.Threading.Tasks.Task`1 C::M() +call int32 [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task`1) stloc.0 ``` @@ -390,7 +434,7 @@ catch (Exception e) if (pendingCatch == 1) { - /* Runtime-Async Call */ C.M(); + System.Runtime.CompilerServices.RuntimeHelpers.Await(C.M()); throw pendingException; } ``` @@ -402,29 +446,31 @@ if (pendingCatch == 1) [1] class [System.Runtime]System.Exception pendingException ) + IL_0000: ldc.i4.0 + IL_0001: stloc.0 .try { - IL_0000: newobj instance void [System.Runtime]System.Exception::.ctor() - IL_0005: throw - } + IL_0002: newobj instance void [System.Runtime]System.Exception::.ctor() + IL_0007: throw + } // end .try catch [System.Runtime]System.Exception { - IL_0006: stloc.1 - IL_0007: ldc.i4.1 - IL_0008: stloc.0 - IL_0009: leave.s IL_000b - } + IL_0008: ldc.i4.1 + IL_0009: stloc.0 + IL_000a: stloc.1 + IL_000b: leave.s IL_000d + } // end handler - IL_000b: ldloc.0 - IL_000c: ldc.i4.1 - IL_000d: bne.un.s IL_0017 + IL_000d: ldloc.0 + IL_000e: ldc.i4.1 + IL_000f: bne.un.s IL_001d - IL_000f: ldloc.1 - IL_0010: call modreq(class [System.Runtime]System.Threading.Tasks.Task) void C::M() - IL_0015: pop - IL_0016: throw + IL_0011: call class [System.Runtime]System.Threading.Tasks.Task C::M() + IL_0016: call void System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task) + IL_001b: ldloc.1 + IL_001c: throw - IL_0017: ret + IL_001d: ret } ``` @@ -459,7 +505,7 @@ catch (Exception e) pendingException = e; } -/* Runtime-Async Call */ C.M(); +System.Runtime.CompilerServices.RuntimeHelpers.Await(C.M()); if (pendingException != null) { @@ -477,22 +523,22 @@ if (pendingException != null) { IL_0000: newobj instance void [System.Runtime]System.Exception::.ctor() IL_0005: throw - } + } // end .try catch [System.Runtime]System.Exception { IL_0006: stloc.0 IL_0007: leave.s IL_0009 - } + } // end handler - IL_0009: call modreq(class [System.Runtime]System.Threading.Tasks.Task) void C::M() - IL_000e: pop - IL_000f: ldloc.0 - IL_0010: brfalse.s IL_0014 + IL_0009: call class [System.Runtime]System.Threading.Tasks.Task C::M() + IL_000e: call void System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task) + IL_0013: ldloc.0 + IL_0014: brfalse.s IL_0018 - IL_0012: ldloc.0 - IL_0013: throw + IL_0016: ldloc.0 + IL_0017: throw - IL_0014: ret + IL_0018: ret } ``` @@ -515,36 +561,34 @@ Translated C#: int[] a = new int[] { }; int _tmp1 = C.M2(); int _tmp2 = a[_tmp1]; -int _tmp3 = /* Runtime-Async Call */ C.M1(); +int _tmp3 = System.Runtime.CompilerServices.RuntimeHelpers.Await(C.M1()); a[_tmp1] = _tmp2 + _tmp3; ``` ```il { .locals init ( - [0] int32[] a, - [1] int32 _tmp1, - [2] int32 _tmp2, - [3] int32 _tmp3 + [0] int32 _tmp1, + [1] int32 _tmp2, + [2] int32 _tmp3 ) IL_0000: ldc.i4.0 IL_0001: newarr [System.Runtime]System.Int32 - IL_0006: stloc.0 - IL_0007: call int32 C::M2() - IL_000c: stloc.1 + IL_0006: call int32 C::M2() + IL_000b: stloc.0 + IL_000c: dup IL_000d: ldloc.0 - IL_000e: ldloc.1 - IL_000f: ldelem.i4 - IL_0010: stloc.2 - IL_0011: call modreq(class [System.Runtime]System.Threading.Tasks.Task) int32 C::M1() - IL_0016: stloc.3 - IL_0017: ldloc.0 - IL_0018: ldloc.1 - IL_0019: ldloc.2 - IL_001a: ldloc.3 - IL_001b: add - IL_001c: stelem.i4 - IL_001d: ret + IL_000e: ldelem.i4 + IL_000f: stloc.1 + IL_0010: call class [System.Runtime]System.Threading.Tasks.Task`1 C::M1() + IL_0015: call !!0 System.Runtime.CompilerServices.RuntimeHelpers::Await(class [System.Runtime]System.Threading.Tasks.Task`1) + IL_001a: stloc.2 + IL_001b: ldloc.0 + IL_001c: ldloc.1 + IL_001d: ldloc.2 + IL_001e: add + IL_001f: stelem.i4 + IL_0020: ret } ```