Skip to content

[Async v2] Implement async method variant handling in AddMethod and UpdateMethod#125397

Draft
tommcdon wants to merge 1 commit intodotnet:mainfrom
tommcdon:dev/tommcdon/implement_addmethoddesc
Draft

[Async v2] Implement async method variant handling in AddMethod and UpdateMethod#125397
tommcdon wants to merge 1 commit intodotnet:mainfrom
tommcdon:dev/tommcdon/implement_addmethoddesc

Conversation

@tommcdon
Copy link
Member

This change implements a TODO item in EEClass::AddMethodDesc to support Runtime Async

@tommcdon tommcdon added this to the 11.0.0 milestone Mar 10, 2026
@tommcdon tommcdon self-assigned this Mar 10, 2026
Copilot AI review requested due to automatic review settings March 10, 2026 18:34
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

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 runtime-async method variant support in the Edit-and-Continue (EnC) path by extending EEClass::AddMethodDesc/EEClass::AddMethod to carry async metadata (flags + optional alternate signature) and by updating EnC method update logic to consider async method variants.

Changes:

  • Extend EEClass::AddMethodDesc to accept async flags and an optional async-variant signature, and plumb that into MethodTableBuilder::InitMethodDesc.
  • Add async return-type classification and async-variant creation logic to EEClass::AddMethod.
  • Update EditAndContinueModule::UpdateMethod to also reset the entrypoint for the method’s async counterpart.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
src/coreclr/vm/encee.cpp Resets entrypoints for async counterparts during EnC method updates.
src/coreclr/vm/class.h Extends AddMethodDesc signature to accept async flags and optional async signature.
src/coreclr/vm/class.cpp Implements async return classification and passes async flags/signature into EnC-added MethodDesc creation.

You can also share your feedback on Copilot code review. Take the survey.

MethodDesc* pNewMDUnused;
if (FAILED(AddMethodDesc(pMTMaybe, methodDef, dwImplFlags, dwMemberAttrs, &pNewMDUnused)))
if (FAILED(AddMethodDesc(pMTMaybe, methodDef, dwImplFlags, dwMemberAttrs,
primaryAsyncFlags, NULL, 0, &pNewMDUnused)))
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

When the added method is Task/ValueTask-returning and the owning type is a generic type definition, the code updates existing instantiated MethodTables by calling AddMethodDesc only for the primary variant. If an async variant is required, it also needs to be added for each existing instantiation; otherwise GetAsyncOtherVariant / GetParallelMethodDesc(... AsyncOtherVariant) on those instantiations can fail to find the counterpart (since it enumerates introduced methods on that MethodTable).

Suggested change
primaryAsyncFlags, NULL, 0, &pNewMDUnused)))
0, NULL, 0, &pNewMDUnused)))

Copilot uses AI. Check for mistakes.
}
else if (IsMiAsync(dwImplFlags))
{
// IsMiAsync but not task-returning: infrastructure async method (e.g. Await helpers)
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

For IsMiAsync methods that are not Task/ValueTask-returning, MethodTableBuilder enforces that these infrastructure async methods can only be declared in the system module (otherwise it throws BADFORMAT). EEClass::AddMethod currently sets AsyncMethodFlags::AsyncCall without any equivalent validation, which would let EnC introduce invalid async-call methods on user types. Consider mirroring the loader check and rejecting such edits when the declaring type isn't implemented in the system module.

Suggested change
// IsMiAsync but not task-returning: infrastructure async method (e.g. Await helpers)
// IsMiAsync but not task-returning: infrastructure async method (e.g. Await helpers)
// These infrastructure async methods are only permitted on types implemented in the system module,
// mirroring the validation performed by MethodTableBuilder during normal type loading.
if (!pModule->IsSystem())
{
LOG((LF_ENC, LL_INFO100,
"EEClass::AddMethod rejecting infrastructure async method (methodDef: 0x%08x) on non-system module\n",
methodDef));
return COR_E_BADIMAGEFORMAT;
}

Copilot uses AI. Check for mistakes.
Comment on lines +377 to +383
if (pMethod->HasAsyncOtherVariant())
{
MethodDesc* pAsyncOther = pMethod->GetAsyncOtherVariantNoCreate();
if (pAsyncOther != NULL)
{
pAsyncOther->ResetCodeEntryPointForEnC();
}
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

GetAsyncOtherVariantNoCreate() does not appear to exist anywhere in the repo (only reference is this new call), so this will not compile. If the intent is to avoid GC/allocation in this GC_NOTRIGGER path, use an existing no-create API (e.g., FindOrCreateAssociatedMethodDesc(... allowCreate: FALSE, AsyncOtherVariant)), or add a GetAsyncOtherVariantNoCreate helper alongside GetAsyncVariantNoCreate.

Copilot uses AI. Check for mistakes.
Comment on lines +585 to +598
bool isAsyncTaskReturning = IsMiAsync(dwImplFlags) && IsTaskReturning(returnKind);

// Create the primary MethodDesc (task-returning variant for async, or the only variant for non-async)
AsyncMethodFlags primaryAsyncFlags = AsyncMethodFlags::None;
if (isAsyncTaskReturning)
{
// For IsMiAsync + task-returning: the task-returning variant is a thunk,
// the real IL body belongs to the async variant.
primaryAsyncFlags = AsyncMethodFlags::ReturnsTaskOrValueTask | AsyncMethodFlags::Thunk;
}
else if (IsMiAsync(dwImplFlags))
{
// IsMiAsync but not task-returning: infrastructure async method (e.g. Await helpers)
primaryAsyncFlags = AsyncMethodFlags::AsyncCall;
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

Async variant handling here only triggers when IsMiAsync(dwImplFlags) is set, but MethodTableBuilder creates Task/ValueTask async variants for any Task/ValueTask-returning method (and uses IsMiAsync only to decide which side is the thunk). As written, adding a normal Task/Task<T>/ValueTask/ValueTask<T> method via EnC would skip setting ReturnsTaskOrValueTask and would not create the async variant, diverging from the normal loader behavior.

Suggested change
bool isAsyncTaskReturning = IsMiAsync(dwImplFlags) && IsTaskReturning(returnKind);
// Create the primary MethodDesc (task-returning variant for async, or the only variant for non-async)
AsyncMethodFlags primaryAsyncFlags = AsyncMethodFlags::None;
if (isAsyncTaskReturning)
{
// For IsMiAsync + task-returning: the task-returning variant is a thunk,
// the real IL body belongs to the async variant.
primaryAsyncFlags = AsyncMethodFlags::ReturnsTaskOrValueTask | AsyncMethodFlags::Thunk;
}
else if (IsMiAsync(dwImplFlags))
{
// IsMiAsync but not task-returning: infrastructure async method (e.g. Await helpers)
primaryAsyncFlags = AsyncMethodFlags::AsyncCall;
const bool returnsTaskOrValueTask = IsTaskReturning(returnKind);
const bool isMiAsync = IsMiAsync(dwImplFlags);
// Create the primary MethodDesc (task-returning variant for async, or the only variant for non-async)
AsyncMethodFlags primaryAsyncFlags = AsyncMethodFlags::None;
// Any Task/ValueTask-returning method should participate in async variant handling,
// matching MethodTableBuilder behavior. IsMiAsync only determines which side is the thunk.
if (returnsTaskOrValueTask)
{
primaryAsyncFlags |= AsyncMethodFlags::ReturnsTaskOrValueTask;
}
if (isMiAsync)
{
if (returnsTaskOrValueTask)
{
// For IsMiAsync + task-returning: the task-returning variant is a thunk,
// the real IL body belongs to the async variant.
primaryAsyncFlags |= AsyncMethodFlags::Thunk;
}
else
{
// IsMiAsync but not task-returning: infrastructure async method (e.g. Await helpers)
primaryAsyncFlags |= AsyncMethodFlags::AsyncCall;
}

Copilot uses AI. Check for mistakes.

if (returnKind == MethodReturnKind::NonGenericTaskReturningMethod)
{
// "... Task ... Method(args);" → "... void ... Method(args);"
Copy link
Member

@jkotas jkotas Mar 11, 2026

Choose a reason for hiding this comment

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

This is duplicating code from regular type loader. Can it be refactored to avoid the duplication?

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.

3 participants