From 6d122da3e662ff108c07a4b737df4949c3768eb9 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 14 Aug 2025 15:09:29 +0200 Subject: [PATCH 1/4] JIT: Handle async calls implemented via `ldvirtftn` properly Handling was missing to mark these as async and to insert async continuations. --- src/coreclr/jit/compiler.h | 4 + src/coreclr/jit/importercalls.cpp | 179 +++++++++++++-------- src/tests/async/Directory.Build.targets | 2 +- src/tests/async/async-gvm/async-gvm.cs | 56 +++++++ src/tests/async/async-gvm/async-gvm.csproj | 8 + 5 files changed, 185 insertions(+), 64 deletions(-) create mode 100644 src/tests/async/async-gvm/async-gvm.cs create mode 100644 src/tests/async/async-gvm/async-gvm.csproj diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index 7c4cab961356b0..90a74bd016dbe2 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -4599,6 +4599,10 @@ class Compiler CORINFO_CALL_INFO* callInfo, IL_OFFSET rawILOffset); + void impSetupAndSpillForAsyncCall(GenTreeCall* call, OPCODE opcode, unsigned prefixFlags); + + void impInsertAsyncContinuationForLdvirtftnCall(GenTreeCall* call); + CORINFO_CLASS_HANDLE impGetSpecialIntrinsicExactReturnType(GenTreeCall* call); GenTree* impFixupCallStructReturn(GenTreeCall* call, CORINFO_CLASS_HANDLE retClsHnd); diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 3b41968fd26a43..bf6b77ee1a4dc6 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -384,8 +384,18 @@ var_types Compiler::impImportCall(OPCODE opcode, // take the call now.... call = gtNewIndCallNode(nullptr, callRetTyp, di); + if (sig->isAsyncCall()) + { + impSetupAndSpillForAsyncCall(call->AsCall(), opcode, prefixFlags); + } + impPopCallArgs(sig, call->AsCall()); + if (call->AsCall()->IsAsync()) + { + impInsertAsyncContinuationForLdvirtftnCall(call->AsCall()); + } + GenTree* thisPtr = impPopStack().val; thisPtr = impTransformThis(thisPtr, pConstrainedResolvedToken, callInfo->thisTransform); assert(thisPtr != nullptr); @@ -681,68 +691,7 @@ var_types Compiler::impImportCall(OPCODE opcode, if (sig->isAsyncCall()) { - AsyncCallInfo asyncInfo; - - if ((prefixFlags & PREFIX_IS_TASK_AWAIT) != 0) - { - JITDUMP("Call is an async task await\n"); - - asyncInfo.ExecutionContextHandling = ExecutionContextHandling::SaveAndRestore; - asyncInfo.SaveAndRestoreSynchronizationContextField = true; - - if ((prefixFlags & PREFIX_TASK_AWAIT_CONTINUE_ON_CAPTURED_CONTEXT) != 0) - { - asyncInfo.ContinuationContextHandling = ContinuationContextHandling::ContinueOnCapturedContext; - JITDUMP(" Continuation continues on captured context\n"); - } - else - { - asyncInfo.ContinuationContextHandling = ContinuationContextHandling::ContinueOnThreadPool; - JITDUMP(" Continuation continues on thread pool\n"); - } - } - else if (opcode == CEE_CALLI) - { - // Used for unboxing/instantiating stubs - JITDUMP("Call is an async calli\n"); - } - else - { - JITDUMP("Call is an async non-task await\n"); - // Only expected non-task await to see in IL is one of the AsyncHelpers.AwaitAwaiter variants. - // These are awaits of custom awaitables, and they come with the behavior that the execution context - // is captured and restored on suspension/resumption. - // We could perhaps skip this for AwaitAwaiter (but not for UnsafeAwaitAwaiter) since it is expected - // that the safe INotifyCompletion will take care of flowing ExecutionContext. - asyncInfo.ExecutionContextHandling = ExecutionContextHandling::AsyncSaveAndRestore; - } - - // For tailcalls the contexts does not need saving/restoring: they will be - // overwritten by the caller anyway. - // - // More specifically, if we can show that - // Thread.CurrentThread._executionContext is not accessed between the - // call and returning then we can omit save/restore of the execution - // context. We do not do that optimization yet. - if (tailCallFlags != 0) - { - asyncInfo.ExecutionContextHandling = ExecutionContextHandling::None; - asyncInfo.ContinuationContextHandling = ContinuationContextHandling::None; - asyncInfo.SaveAndRestoreSynchronizationContextField = false; - } - - call->AsCall()->SetIsAsync(new (this, CMK_Async) AsyncCallInfo(asyncInfo)); - - if (asyncInfo.ExecutionContextHandling == ExecutionContextHandling::SaveAndRestore) - { - compMustSaveAsyncContexts = true; - - // In this case we will need to save the context after the arguments are evaluated. - // Spill the arguments to accomplish that. - // (We could do this via splitting in SaveAsyncContexts, but since we need to - // handle inline candidates we won't gain much.) - impSpillSideEffects(true, CHECK_SPILL_ALL DEBUGARG("Async await with execution context save and restore")); - } + impSetupAndSpillForAsyncCall(call->AsCall(), opcode, prefixFlags); } // Now create the argument list. @@ -6729,7 +6678,7 @@ bool Compiler::impCanPInvokeInlineCallSite(BasicBlock* block) // call passes a combination of legality and profitability checks. // // If GTF_CALL_UNMANAGED is set, increments info.compUnmanagedCallCountWithGCTransition - +// void Compiler::impCheckForPInvokeCall( GenTreeCall* call, CORINFO_METHOD_HANDLE methHnd, CORINFO_SIG_INFO* sig, unsigned mflags, BasicBlock* block) { @@ -6850,6 +6799,110 @@ void Compiler::impCheckForPInvokeCall( } } +//------------------------------------------------------------------------ +// impSetupAndSpillForAsyncCall: +// Register a call as being async and set up context handling information depending on the IL. +// Also spill IL arguments if necessary. +// +// Arguments: +// call - The call +// opcode - The IL opcode for the call +// prefixFlags - Flags containing context handling information from IL +// +void Compiler::impSetupAndSpillForAsyncCall(GenTreeCall* call, OPCODE opcode, unsigned prefixFlags) +{ + AsyncCallInfo asyncInfo; + + if ((prefixFlags & PREFIX_IS_TASK_AWAIT) != 0) + { + JITDUMP("Call is an async task await\n"); + + asyncInfo.ExecutionContextHandling = ExecutionContextHandling::SaveAndRestore; + asyncInfo.SaveAndRestoreSynchronizationContextField = true; + + if ((prefixFlags & PREFIX_TASK_AWAIT_CONTINUE_ON_CAPTURED_CONTEXT) != 0) + { + asyncInfo.ContinuationContextHandling = ContinuationContextHandling::ContinueOnCapturedContext; + JITDUMP(" Continuation continues on captured context\n"); + } + else + { + asyncInfo.ContinuationContextHandling = ContinuationContextHandling::ContinueOnThreadPool; + JITDUMP(" Continuation continues on thread pool\n"); + } + } + else if (opcode == CEE_CALLI) + { + // Used for unboxing/instantiating stubs + JITDUMP("Call is an async calli\n"); + } + else + { + JITDUMP("Call is an async non-task await\n"); + // Only expected non-task await to see in IL is one of the AsyncHelpers.AwaitAwaiter variants. + // These are awaits of custom awaitables, and they come with the behavior that the execution context + // is captured and restored on suspension/resumption. + // We could perhaps skip this for AwaitAwaiter (but not for UnsafeAwaitAwaiter) since it is expected + // that the safe INotifyCompletion will take care of flowing ExecutionContext. + asyncInfo.ExecutionContextHandling = ExecutionContextHandling::AsyncSaveAndRestore; + } + + // For tailcalls the contexts does not need saving/restoring: they will be + // overwritten by the caller anyway. + // + // More specifically, if we can show that + // Thread.CurrentThread._executionContext is not accessed between the + // call and returning then we can omit save/restore of the execution + // context. We do not do that optimization yet. + if ((prefixFlags & PREFIX_TAILCALL) != 0) + { + asyncInfo.ExecutionContextHandling = ExecutionContextHandling::None; + asyncInfo.ContinuationContextHandling = ContinuationContextHandling::None; + asyncInfo.SaveAndRestoreSynchronizationContextField = false; + } + + call->AsCall()->SetIsAsync(new (this, CMK_Async) AsyncCallInfo(asyncInfo)); + + if (asyncInfo.ExecutionContextHandling == ExecutionContextHandling::SaveAndRestore) + { + compMustSaveAsyncContexts = true; + + // In this case we will need to save the context after the arguments are evaluated. + // Spill the arguments to accomplish that. + // (We could do this via splitting in SaveAsyncContexts, but since we need to + // handle inline candidates we won't gain much.) + impSpillSideEffects(true, CHECK_SPILL_ALL DEBUGARG("Async await with execution context save and restore")); + } +} + +//------------------------------------------------------------------------ +// impInsertAsyncContinuationForLdvirtftnCall: +// Insert the async continuation argument for a call the EE asked to be +// performed via ldvirtftn. +// +// Arguments: +// call - The call +// +// Remarks: +// Should be called before the 'this' arg is inserted, but after other IL args +// have been inserted. +// +void Compiler::impInsertAsyncContinuationForLdvirtftnCall(GenTreeCall* call) +{ + assert(call->AsCall()->IsAsync()); + + if (Target::g_tgtArgOrder == Target::ARG_ORDER_R2L) + { + call->AsCall()->gtArgs.PushFront(this, NewCallArg::Primitive(gtNewNull(), TYP_REF) + .WellKnown(WellKnownArg::AsyncContinuation)); + } + else + { + call->AsCall()->gtArgs.PushBack(this, NewCallArg::Primitive(gtNewNull(), TYP_REF) + .WellKnown(WellKnownArg::AsyncContinuation)); + } +} + //------------------------------------------------------------------------ // SpillRetExprHelper: iterate through arguments tree and spill ret_expr to local variables. // diff --git a/src/tests/async/Directory.Build.targets b/src/tests/async/Directory.Build.targets index 5a4d413a9e1f62..9ceeb612a526e2 100644 --- a/src/tests/async/Directory.Build.targets +++ b/src/tests/async/Directory.Build.targets @@ -6,7 +6,7 @@ - true + diff --git a/src/tests/async/async-gvm/async-gvm.cs b/src/tests/async/async-gvm/async-gvm.cs new file mode 100644 index 00000000000000..1c3c948028eb8d --- /dev/null +++ b/src/tests/async/async-gvm/async-gvm.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Xunit; + +public class AsyncGvm +{ + interface I2 + { + Task M0(); + Task M1(object a0, object a1, object a2, object a3, object a4, object a5, object a6, object a7, object a8); + } + + class Class2 : I2 + { + public async Task M0() + { + await Task.Yield(); + return typeof(T).ToString(); + } + + public async Task M1(object a0, object a1, object a2, object a3, object a4, object a5, object a6, object a7, object a8) + { + await Task.Yield(); + return typeof(T).ToString(); + } + } + + static I2 o2; + static async Task CallClass2M0() + { + o2 = new Class2(); + return await o2.M0(); + } + + static async Task CallClass2M1() + { + o2 = new Class2(); + return await o2.M1(default, default, default, default, default, default, default, default, default); + } + + [Fact] + public static void NoArgGVM() + { + Assert.Equal("System.String", CallClass2M0().Result); + } + + [Fact] + public static void ManyArgGVM() + { + Assert.Equal("System.String", CallClass2M1().Result); + } +} diff --git a/src/tests/async/async-gvm/async-gvm.csproj b/src/tests/async/async-gvm/async-gvm.csproj new file mode 100644 index 00000000000000..9367a79b2edbb1 --- /dev/null +++ b/src/tests/async/async-gvm/async-gvm.csproj @@ -0,0 +1,8 @@ + + + True + + + + + From c6dd1426270fca409c0751d12862be2a1364e4ae Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 14 Aug 2025 15:18:15 +0200 Subject: [PATCH 2/4] Merge with existing test --- src/tests/async/async-gvm/async-gvm.cs | 56 ------------------- src/tests/async/async-gvm/async-gvm.csproj | 8 --- .../inst-unbox-thunks/inst-unbox-thunks.cs | 46 +++++++++++++++ 3 files changed, 46 insertions(+), 64 deletions(-) delete mode 100644 src/tests/async/async-gvm/async-gvm.cs delete mode 100644 src/tests/async/async-gvm/async-gvm.csproj diff --git a/src/tests/async/async-gvm/async-gvm.cs b/src/tests/async/async-gvm/async-gvm.cs deleted file mode 100644 index 1c3c948028eb8d..00000000000000 --- a/src/tests/async/async-gvm/async-gvm.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using Xunit; - -public class AsyncGvm -{ - interface I2 - { - Task M0(); - Task M1(object a0, object a1, object a2, object a3, object a4, object a5, object a6, object a7, object a8); - } - - class Class2 : I2 - { - public async Task M0() - { - await Task.Yield(); - return typeof(T).ToString(); - } - - public async Task M1(object a0, object a1, object a2, object a3, object a4, object a5, object a6, object a7, object a8) - { - await Task.Yield(); - return typeof(T).ToString(); - } - } - - static I2 o2; - static async Task CallClass2M0() - { - o2 = new Class2(); - return await o2.M0(); - } - - static async Task CallClass2M1() - { - o2 = new Class2(); - return await o2.M1(default, default, default, default, default, default, default, default, default); - } - - [Fact] - public static void NoArgGVM() - { - Assert.Equal("System.String", CallClass2M0().Result); - } - - [Fact] - public static void ManyArgGVM() - { - Assert.Equal("System.String", CallClass2M1().Result); - } -} diff --git a/src/tests/async/async-gvm/async-gvm.csproj b/src/tests/async/async-gvm/async-gvm.csproj deleted file mode 100644 index 9367a79b2edbb1..00000000000000 --- a/src/tests/async/async-gvm/async-gvm.csproj +++ /dev/null @@ -1,8 +0,0 @@ - - - True - - - - - diff --git a/src/tests/async/inst-unbox-thunks/inst-unbox-thunks.cs b/src/tests/async/inst-unbox-thunks/inst-unbox-thunks.cs index a3e82e3aa89d22..8a727672443835 100644 --- a/src/tests/async/inst-unbox-thunks/inst-unbox-thunks.cs +++ b/src/tests/async/inst-unbox-thunks/inst-unbox-thunks.cs @@ -137,4 +137,50 @@ public static void ManyArgGenericInstantiating() { Assert.Equal("System.String", CallStruct1M1b().Result); } + + interface I2 + { + Task M0(); + Task M1(object a0, object a1, object a2, object a3, object a4, object a5, object a6, object a7, object a8); + } + + class Class2 : I2 + { + public async Task M0() + { + await Task.Yield(); + return typeof(T).ToString(); + } + + public async Task M1(object a0, object a1, object a2, object a3, object a4, object a5, object a6, object a7, object a8) + { + await Task.Yield(); + return typeof(T).ToString(); + } + } + + static I2 o2; + static async Task CallClass2M0() + { + o2 = new Class2(); + return await o2.M0(); + } + + static async Task CallClass2M1() + { + o2 = new Class2(); + return await o2.M1(default, default, default, default, default, default, default, default, default); + } + + [Fact] + public static void NoArgGVM() + { + Assert.Equal("System.String", CallClass2M0().Result); + } + + [Fact] + public static void ManyArgGVM() + { + Assert.Equal("System.String", CallClass2M1().Result); + } } From 4b80fedc622b92d62d379be3e60a324532e6f6a6 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 14 Aug 2025 15:39:43 +0200 Subject: [PATCH 3/4] Enable testing --- src/coreclr/inc/clrconfigvalues.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/inc/clrconfigvalues.h b/src/coreclr/inc/clrconfigvalues.h index 882a88839a933e..5db25e3b4d7420 100644 --- a/src/coreclr/inc/clrconfigvalues.h +++ b/src/coreclr/inc/clrconfigvalues.h @@ -709,7 +709,7 @@ RETAIL_CONFIG_DWORD_INFO(EXTERNAL_EnableRiscV64Zbb, W("EnableRiscV64 #endif // Runtime-async -RETAIL_CONFIG_DWORD_INFO(UNSUPPORTED_RuntimeAsync, W("RuntimeAsync"), 0, "Enables runtime async method support") +RETAIL_CONFIG_DWORD_INFO(UNSUPPORTED_RuntimeAsync, W("RuntimeAsync"), 1, "Enables runtime async method support") /// /// Uncategorized From 4b246ab462ebf51fc9ce2550f02239a4de017476 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 14 Aug 2025 20:40:43 +0200 Subject: [PATCH 4/4] Disable testing --- src/coreclr/inc/clrconfigvalues.h | 2 +- src/tests/async/Directory.Build.targets | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coreclr/inc/clrconfigvalues.h b/src/coreclr/inc/clrconfigvalues.h index 5db25e3b4d7420..882a88839a933e 100644 --- a/src/coreclr/inc/clrconfigvalues.h +++ b/src/coreclr/inc/clrconfigvalues.h @@ -709,7 +709,7 @@ RETAIL_CONFIG_DWORD_INFO(EXTERNAL_EnableRiscV64Zbb, W("EnableRiscV64 #endif // Runtime-async -RETAIL_CONFIG_DWORD_INFO(UNSUPPORTED_RuntimeAsync, W("RuntimeAsync"), 1, "Enables runtime async method support") +RETAIL_CONFIG_DWORD_INFO(UNSUPPORTED_RuntimeAsync, W("RuntimeAsync"), 0, "Enables runtime async method support") /// /// Uncategorized diff --git a/src/tests/async/Directory.Build.targets b/src/tests/async/Directory.Build.targets index 9ceeb612a526e2..5a4d413a9e1f62 100644 --- a/src/tests/async/Directory.Build.targets +++ b/src/tests/async/Directory.Build.targets @@ -6,7 +6,7 @@ - + true