Skip to content

Conversation

@jtschuster
Copy link
Member

Updates the AsyncResumptionStub to emit IL. The implementation was ported from the runtime's getAsyncResumptionStub

CORINFO_METHOD_HANDLE CEEJitInfo::getAsyncResumptionStub(void** entryPoint)
{
CONTRACTL{
THROWS;
GC_TRIGGERS;
MODE_PREEMPTIVE;
} CONTRACTL_END;
MethodDesc* md = m_pMethodBeingCompiled;
LoaderAllocator* loaderAlloc = md->GetLoaderAllocator();
AllocMemTracker amTracker;
Signature stubSig = BuildResumptionStubSignature(md->GetLoaderAllocator(), &amTracker);
MetaSig msig(md);
Signature calliSig = BuildResumptionStubCalliSignature(msig, md->GetMethodTable(), md->GetLoaderAllocator(), &amTracker);
SigTypeContext emptyCtx;
ILStubLinker sl(md->GetModule(), stubSig, &emptyCtx, NULL, ILSTUB_LINKER_FLAG_NONE);
ILCodeStream* pCode = sl.NewCodeStream(ILStubLinker::kDispatch);
int numArgs = 0;
if (msig.HasThis())
{
if (md->GetMethodTable()->IsValueType())
{
pCode->EmitLDC(0);
pCode->EmitCONV_U();
}
else
{
pCode->EmitLDNULL();
}
numArgs++;
}
#ifndef TARGET_X86
if (msig.HasGenericContextArg())
{
pCode->EmitLDC(0);
numArgs++;
}
// Continuation
pCode->EmitLDARG(0);
numArgs++;
#endif
msig.Reset();
CorElementType ty;
while ((ty = msig.NextArg()) != ELEMENT_TYPE_END)
{
TypeHandle tyHnd = msig.GetLastTypeHandleThrowing();
DWORD loc = pCode->NewLocal(LocalDesc(tyHnd));
pCode->EmitLDLOCA(loc);
pCode->EmitINITOBJ(pCode->GetToken(tyHnd));
pCode->EmitLDLOC(loc);
numArgs++;
}
#ifdef TARGET_X86
// Continuation
pCode->EmitLDARG(0);
if (msig.HasGenericContextArg())
{
pCode->EmitLDC(0);
numArgs++;
}
numArgs++;
#endif
#ifdef FEATURE_TIERED_COMPILATION
// Resumption stubs are uniquely coupled to the code version (since the
// continuation is), so we need to make sure we always keep calling the
// same version here.
PrepareCodeConfig* config = GetThread()->GetCurrentPrepareCodeConfig();
NativeCodeVersion ncv = config->GetCodeVersion();
if (ncv.GetOptimizationTier() == NativeCodeVersion::OptimizationTier1OSR)
{
#ifdef FEATURE_ON_STACK_REPLACEMENT
// The OSR version needs to resume in the tier0 version. The tier0
// version will handle setting up the frame that the OSR version
// expects and then delegating back into the OSR version (knowing to do
// so through information stored in the continuation).
_ASSERTE(m_pPatchpointInfoFromRuntime != NULL);
pCode->EmitLDC((DWORD_PTR)m_pPatchpointInfoFromRuntime->GetTier0EntryPoint());
#else // !FEATURE_ON_STACK_REPLACEMENT
_ASSERTE(!"Unexpected optimization tier with OSR disabled");
#endif // FEATURE_ON_STACK_REPLACEMENT
}
else
#endif // FEATURE_TIERED_COMPILATION
{
{
m_finalCodeAddressSlot = (PCODE*)amTracker.Track(m_pMethodBeingCompiled->GetLoaderAllocator()->GetHighFrequencyHeap()->AllocMem(S_SIZE_T(sizeof(PCODE))));
}
pCode->EmitLDC((DWORD_PTR)m_finalCodeAddressSlot);
pCode->EmitLDIND_I();
}
pCode->EmitCALLI(pCode->GetSigToken(calliSig.GetRawSig(), calliSig.GetRawSigLen()), numArgs, msig.IsReturnTypeVoid() ? 0 : 1);
DWORD resultLoc = UINT_MAX;
TypeHandle resultTypeHnd;
if (!msig.IsReturnTypeVoid())
{
resultTypeHnd = msig.GetRetTypeHandleThrowing();
resultLoc = pCode->NewLocal(LocalDesc(resultTypeHnd));
pCode->EmitSTLOC(resultLoc);
}
TypeHandle continuationTypeHnd = CoreLibBinder::GetClass(CLASS__CONTINUATION);
DWORD newContinuationLoc = pCode->NewLocal(LocalDesc(continuationTypeHnd));
pCode->EmitCALL(METHOD__STUBHELPERS__ASYNC_CALL_CONTINUATION, 0, 1);
pCode->EmitSTLOC(newContinuationLoc);
if (!msig.IsReturnTypeVoid())
{
ILCodeLabel* doneResult = pCode->NewCodeLabel();
pCode->EmitLDLOC(newContinuationLoc);
pCode->EmitBRTRUE(doneResult);
pCode->EmitLDARG(1); // resultLoc
pCode->EmitLDLOC(resultLoc);
pCode->EmitSTOBJ(pCode->GetToken(resultTypeHnd));
pCode->EmitLabel(doneResult);
}
pCode->EmitLDLOC(newContinuationLoc);
pCode->EmitRET();
MethodDesc* result =
ILStubCache::CreateAndLinkNewILStubMethodDesc(
md->GetLoaderAllocator(),
md->GetLoaderModule()->GetILStubCache()->GetOrCreateStubMethodTable(md->GetLoaderModule()),
ILSTUB_ASYNC_RESUME,
md->GetModule(),
stubSig.GetRawSig(), stubSig.GetRawSigLen(),
&emptyCtx,
&sl);
amTracker.SuppressRelease();
ILStubResolver *pResolver = result->AsDynamicMethodDesc()->GetILStubResolver();
pResolver->SetStubTargetMethodDesc(m_pMethodBeingCompiled);
const char* optimizationTierName = "UnknownTier";
#ifdef FEATURE_TIERED_COMPILATION
switch (ncv.GetOptimizationTier())
{
case NativeCodeVersion::OptimizationTier0: optimizationTierName = "Tier0"; break;
case NativeCodeVersion::OptimizationTier1: optimizationTierName = "Tier1"; break;
case NativeCodeVersion::OptimizationTier1OSR: optimizationTierName = "Tier1OSR"; break;
case NativeCodeVersion::OptimizationTierOptimized: optimizationTierName = "Optimized"; break;
case NativeCodeVersion::OptimizationTier0Instrumented: optimizationTierName = "Tier0Instrumented"; break;
case NativeCodeVersion::OptimizationTier1Instrumented: optimizationTierName = "Tier1Instrumented"; break;
default: break;
}
#endif // FEATURE_TIERED_COMPILATION
#ifdef _DEBUG
LPCUTF8 methodName = m_pMethodBeingCompiled->GetName();
size_t stubNameLen = STRING_LENGTH("IL_STUB_AsyncResume__");
stubNameLen += strlen(methodName);
stubNameLen += strlen(optimizationTierName);
stubNameLen++; // "\n"
AllocMemTracker amTrackerName;
char* allocedMem = (char*)amTrackerName.Track(m_pMethodBeingCompiled->GetLoaderAllocator()->GetLowFrequencyHeap()->AllocMem(S_SIZE_T(stubNameLen)));
sprintf_s(allocedMem, stubNameLen, "IL_STUB_AsyncResume_%s_%s", m_pMethodBeingCompiled->GetName(), optimizationTierName);
result->AsDynamicMethodDesc()->SetMethodName((LPCUTF8)allocedMem);
amTrackerName.SuppressRelease();
LOG((LF_STUBS, LL_INFO1000, "ASYNC: Resumption stub %s created\n", allocedMem));
sl.LogILStub(CORJIT_FLAGS());
#endif
*entryPoint = (void*)result->GetMultiCallableAddrOfCode();
return CORINFO_METHOD_HANDLE(result);
}

@jtschuster jtschuster self-assigned this Nov 7, 2025
Copilot AI review requested due to automatic review settings November 7, 2025 21:48
@github-actions github-actions bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Nov 7, 2025
@am11 am11 added area-NativeAOT-coreclr and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Nov 7, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements the async resumption stub for native AOT compilation by porting the CoreCLR VM implementation from jitinterface.cpp to C#. The stub is used to resume async methods after suspension.

  • Replaces the placeholder ThrowNotSupportedException implementation with a complete IL emission that matches CoreCLR's getAsyncResumptionStub
  • Adds proper caching of the generated IL and comparison logic for the stub method
  • Updates the constructor assertion to use a simplified IsAsyncCall() method
Comments suppressed due to low confidence (2)

src/coreclr/tools/Common/TypeSystem/IL/Stubs/AsyncResumptionStub.cs:148

  • This CompareToImpl method duplicates the existing implementation in AsyncResumptionStub.Sorting.cs (lines 13-16). Both implementations are identical and will cause a compilation error due to duplicate method definitions in the same partial class.
    src/coreclr/tools/Common/TypeSystem/IL/Stubs/AsyncResumptionStub.cs:127
  • The brtrue instruction requires an address on the stack, but EmitLdLoca loads the address of the local variable. This should be EmitLdLoc instead to load the value of the continuation for the branch condition. The CoreCLR reference implementation uses EmitLDLOC (line 14772 in jitinterface.cpp).

@jtschuster
Copy link
Member Author

Created #121458 to keep the corelib changes in a separate commit.

…ub.cs

Co-authored-by: Vladimir Sadov <vsadov@microsoft.com>
@MichalStrehovsky
Copy link
Member

Pushed changes that generates a dummy MethodDesc with an explicit Continuation parameter which is called in the async resumption stubs. It looks like ilc compiles basic methods, but throws during scanning with "Scanning Failure". Using this in ReadyToRun will require more work on the Task-returning stubs to resolve the required intrinsics, so this PR will focus on ilc. I'll work on the ilc issues tomorrow.

Optimizations are currently broken. Make sure not to run ilc with -O, it will not work. Drop <Optimize>true</Optimize> from the async tests if you're running them.

MichalStrehovsky added a commit that referenced this pull request Nov 17, 2025
…r different MethodDescs

Ran into this as I was trying something with #121456. That PR introduces a situation where two MethodDescs could map to the same EntryPoint. We didn't previously have that since MethodEntrypointOrTentativeMethod doesn't support unboxing thunks (that do a similar trick).
Copy link
Member

@MichalStrehovsky MichalStrehovsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this would work otherwise!

MichalStrehovsky added a commit that referenced this pull request Nov 17, 2025
…r different MethodDescs (#121703)

Ran into this as I was trying something with #121456. That PR introduces
a situation where two MethodDescs could map to the same EntryPoint. We
didn't previously have that since MethodEntrypointOrTentativeMethod
doesn't support unboxing thunks (that do a similar trick).

Without this fix we would end up with two `TentativeInstanceMethodNode`
that point to the same EntryPoint and that doesn't lead to anything
good. These need to be 1:1 with entrypoints.

Cc @dotnet/ilc-contrib
@jtschuster jtschuster requested a review from Copilot November 18, 2025 18:11
@jtschuster jtschuster enabled auto-merge (squash) November 18, 2025 18:16
@jtschuster jtschuster merged commit 3a95842 into dotnet:main Nov 18, 2025
97 of 100 checks passed
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment on lines +220 to +223
if (_signature is null)
return InitializeSignature();

return _signature;
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Signature property should follow the null-coalescing pattern like other properties in the codebase. Use return _signature ??= InitializeSignature(); instead of explicit null check and separate assignment.

Suggested change
if (_signature is null)
return InitializeSignature();
return _signature;
return _signature ??= InitializeSignature();

Copilot uses AI. Check for mistakes.
/// This method should be marked IsAsync=false and HasInstantiation=false. These are defaults
/// for MethodDesc and so aren't explicitly set in the code below.
/// </summary>
internal sealed partial class ExplicitContinuationAsyncMethod : MethodDesc
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ExplicitContinuationAsyncMethod class lacks a mangling partial class file. Based on the codebase pattern (AsyncResumptionStub, AsyncMethodVariant, etc.), internal call methods that can be referenced should implement IPrefixMangledMethod. Consider adding an AsyncResumptionStub.Mangling.cs partial class for ExplicitContinuationAsyncMethod to provide proper name mangling support.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generate AsyncResumptionStub for the Jit at compile time.

6 participants