Skip to content

Conversation

@jtschuster
Copy link
Member

@jtschuster jtschuster commented Oct 30, 2025

AsyncMethodVariant is isolated to CorInfoImpl.cs and only used in jit compilation.

MethodWithGCInfo and MethodWithToken (Code and R2R method signature providing nodes, respectively) have an additional AsyncVariant flag. When constructed in CorInfoImpl, an AsyncMethodVariant instance should unwrap the underlying method and pass AsyncVariant=true.

AsyncMethodKind is also added to MethodDesc to determine whether the method is AsyncCallConv or not, TaskReturning or void-returning, and thunk or impl.

…sc classes

- EcmaMethod represents the variant encoded in metadata, either async or Task-returning. The Signature is adjusted accordingly to align with the calling convention.
- AsyncMethodDesc is replaced with AsyncMethodThunk, which is the Async variant thunk of a Task-returning implementation method.
- TaskReturningAsyncThunk is the variant of an async implementation method
- AsyncMethodData was borrowed from the runtime implementation and added to the managedTypeSystem.
- IsTaskReturning is now on MethodDesc and uses the AsyncMethodData to determine whether the type returns Task/ValueTask. It has different semantics than the runtime method of the same name, as this returns true for Async methods too.
- GetAsyncOtherVariant() is also borrowed from the runtime implementation. This makes the factory unnecessary, as the unique "other variant" can be retrieved from the definition without creating a new instance each time.
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 refactors the async method handling infrastructure in the CoreCLR AOT compiler's type system. It moves async method variant management from the JIT interface layer into the core type system, making async method thunks first-class citizens with proper lifetimes.

Key changes:

  • Moved async method thunk creation from JitInterface to the core type system with new AsyncMethodThunk and TaskReturningAsyncThunk classes
  • Introduced AsyncMethodData and AsyncMethodKind to track method async characteristics and signatures
  • Updated EcmaMethod, MethodForInstantiatedType, and InstantiatedMethod to support async variant tracking

Reviewed Changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
ILCompiler.TypeSystem.csproj Adds new async thunk source files to the project
ILCompiler.ReadyToRun.csproj Removes old JitInterface async method files
EcmaMethod.cs Adds async method data tracking and variant generation, reformats constants
TaskReturningAsyncThunk.cs New thunk class for async-callable variants of Task-returning methods
MethodForInstantiatedType.cs Adds async variant support for instantiated type methods
MethodDesc.cs Adds core async infrastructure: ReturnsTaskOrValueTask(), CreateAsyncSignature(), AsyncMethodData, AsyncMethodKind, GetAsyncOtherVariant()
MethodDelegator.cs Makes AsyncMethodData abstract for delegator implementations
InstantiatedMethod.cs Adds async variant support for generic method instantiations
AsyncMethodThunk.cs New thunk class for async variants of Task-returning methods
AsyncMethodDescFactory.cs Deleted - factory no longer needed with new approach
AsyncMethodDesc.cs Deleted - replaced by new thunk classes in type system

@jtschuster jtschuster requested a review from Copilot October 30, 2025 22:39
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 11 out of 11 changed files in this pull request and generated 2 comments.

@jtschuster jtschuster requested a review from Copilot October 31, 2025 00:03
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 23 out of 23 changed files in this pull request and generated 1 comment.

@MichalStrehovsky
Copy link
Member

What considerations lead to turning EcmaMethod into the "impl" variant of async? I understand there are going to be warts no matter how we implement this, but on the surface EcmaMethod not having the signature that it has in the metadata will lead to a lot of warts. My instinct would be to keep the signature as-is.

This is not the first time we're in a situation where a single method can have different entrypoints with different signature. We have similar situation with unboxing thunks (R2R + NAOT), instantiating thunks (R2R), special default interface method instantiating thunks (NAOT), or even p/invokes (NAOT, distinguishing the marshalled and "naked" entrypoints),

In the CoreCLR type system the distinction between "entry point" and "method" is kind of fuzzy (even before async) - it's all kinds of MethodDescs.

In our managed compilers, we represent entry points using ISymbolNode - often actual code bytes that one can refer to with a reloc. Methods are represented with MethodDescs. Typically there's just one MethodDesc for a single method (modulo generics). Sometimes we make more than one MethodDesc for a single method, but those are always short lived and don't leak out from the component that knows how to handle it (like the MethodDesc that is getting deleted in this PR). We don't want to end up with these "weird" MethodDescs in parts of the system that shouldn't have to deal with the weirdness.

For example, with unboxing thunks, most of the system doesn't care about their existence. Some parts of the system deal with them by carrying a pair of MethodDesc + bool. And we have a MethodDesc for them that is local to the JitInterface but doesn't enter the rest of the compiler.

In native AOT, there's also an instantiating unboxing MethodDesc that is similar to how I think of async so far:

if (TypeSystemContext.IsSpecialUnboxingThunkTargetMethod(method))
{
return MethodEntrypoint(TypeSystemContext.GetRealSpecialUnboxingThunkTargetMethod(method));
}
else if (TypeSystemContext.IsDefaultInterfaceMethodImplementationThunkTargetMethod(method))
{
return MethodEntrypoint(TypeSystemContext.GetRealDefaultInterfaceMethodImplementationThunkTargetMethod(method));
}

This one also exists, but its existence is pretty limited because very few components will get to the MethodDesc itself (we basically only hand it out to RyuJIT).

It was added in dotnet/corert#2566 and the implementation is still pretty much the same.


The runtime async methods look like normal method when looking at them in IL. The callsites to them look regular. They are weird when one looks at the method body. So my instinct would be not to make EcmaMethod for these methods weird right off the bat. Most components will probably not care about handling these specially. If we make the signature on EcmaMethod weird, suddenly a lot of components that don't really need to deal with async will have to deal with async. For example:

Resolving a MemberRef that was created before runtime async into a definition that is runtime async will need special casing in signature comparisons:

public override EcmaMethod GetMethod(ReadOnlySpan<byte> name, MethodSignature signature, Instantiation substitution)
{
var metadataReader = this.MetadataReader;
foreach (var handle in _typeDefinition.GetMethods())
{
if (metadataReader.StringEquals(metadataReader.GetMethodDefinition(handle).Name, name))
{
var method = _module.GetMethod(handle, this);
if (signature == null || signature.Equals(method.Signature.ApplySubstitution(substitution)))
return method;
}
}
return null;
}

Reflection metadata generation will need to undo the async transform:

public override MethodSignature HandleMethodSignature(Cts.MethodSignature signature)
{
// TODO: if Cts.MethodSignature implements Equals/GetHashCode, we could enable pooling here.
var result = new MethodSignature
{
CallingConvention = GetSignatureCallingConvention(signature),
GenericParameterCount = signature.GenericParameterCount,
ReturnType = HandleType(signature.ReturnType),
// TODO-NICE: VarArgParameters
};
result.Parameters.Capacity = signature.Length;
for (int i = 0; i < signature.Length; i++)
{
result.Parameters.Add(HandleType(signature[i]));
}
return result;
}

We'll need to special case async in virtual method resolution. Etc.

My initial instinct would be to leave the metadata as it is in the IL file and make very local MethodDescs as needed. And extra flags where needed:

public DynamicHelperCellKey(MethodWithToken method, bool isUnboxingStub, bool isInstantiatingStub)

_metadataSignature = signature;
}

public override MethodSignature Signature
Copy link
Member

Choose a reason for hiding this comment

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

I agree with @MichalStrehovsky . This should stay as is.

Copy link
Member

@jkotas jkotas Oct 31, 2025

Choose a reason for hiding this comment

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

The managed type system "features" have been designed as incremental. E.g. adding *CodeGen* or *Sorting* does not change how any of the core methods work. This change would be violating this design.

@jtschuster jtschuster changed the title Use EcmaMethod as the async "impl" variant and create async "thunk" MethodDesc classes Rename AsyncMethodDesc to AsyncMethodVariant, add "AsyncVariant" flag to MethodWithToken and MethodWithGCInfo Nov 4, 2025
READYTORUN_METHOD_SIG_Constrained = 0x20,
READYTORUN_METHOD_SIG_OwnerType = 0x40,
READYTORUN_METHOD_SIG_UpdateContext = 0x80,
READYTORUN_METHOD_SIG_AsyncThunkVariant = 0x100,
Copy link
Member

Choose a reason for hiding this comment

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

Should this be just AsyncVariant (ie cover both Thunk or impl)?

This enum is part of R2R file format. It is used to encode references to methods in other assemblies. This encoding should be resilient to compatible changes in the implementation of other assemblies. If we encode AsyncThunkVariant here, it does not sound like we will be resilient to the implementations in other assemblies changing between async v1 and async v2.

Copy link
Member

Choose a reason for hiding this comment

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

Is READYTORUN_METHOD_SIG_SlotInsteadOfToken used? Could we reuse it (bumping the R2R version of course)?


using System;
using System.Diagnostics;
using System.Threading;
Copy link
Member

Choose a reason for hiding this comment

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

Are the changes in this file still needed?

get
{
#if READYTORUN
return _methodCodeNode.GetJitMethod(_asyncMethodFactory);
Copy link
Member

Choose a reason for hiding this comment

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

Would it be better to inline the contents of GetJitMethod to here? Presumably MethodWithGCInfo doesn't really need to know about AsyncMethodVariantFactory.

READYTORUN_METHOD_SIG_Constrained = 0x20,
READYTORUN_METHOD_SIG_OwnerType = 0x40,
READYTORUN_METHOD_SIG_UpdateContext = 0x80,
READYTORUN_METHOD_SIG_AsyncThunkVariant = 0x100,
Copy link
Member

Choose a reason for hiding this comment

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

Is READYTORUN_METHOD_SIG_SlotInsteadOfToken used? Could we reuse it (bumping the R2R version of course)?

if (info->pResolvedTokenVirtualMethod != null)
{
methodWithTokenDecl = ComputeMethodWithToken(decl, ref *info->pResolvedTokenVirtualMethod, null, false);
methodWithTokenDecl = ComputeMethodWithToken(decl, ref *info->pResolvedTokenVirtualMethod, null, false, asyncVariant: false);
Copy link
Member

@MichalStrehovsky MichalStrehovsky Nov 4, 2025

Choose a reason for hiding this comment

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

I think the asyncVariant: false here are not quite correct (applies to all uses in this method)

I would make this throw RequiresRuntimeJitException if decl.IsAsync or impl.IsAsync. for now.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah good idea, I was putting false for places I wasn't sure (since we don't even try to compile async methods yet), but throwing would make more sense.

{
get
{
return _asyncSignature ??= _wrappedMethod.Signature.CreateAsyncSignature();
Copy link
Member

Choose a reason for hiding this comment

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

Inline CreateAsyncSignature to here? It doesn't feel like a MethodSignature concern.

Comment on lines +14 to +18
private static class MethodSignatureFlagsExtensions
{
public const MethodSignatureFlags ReturnsTaskOrValueTask = (MethodSignatureFlags)(1 << 30); // Not a real flag, just a cached value
public const MethodSignatureFlags ReturnsTaskOrValueTaskMask = (MethodSignatureFlags)(1 << 31); // Not a real flag, just a cached value
}
Copy link
Member

Choose a reason for hiding this comment

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

This cache might be premature, I wouldn't do it yet.

I think we already have a bug that CreateAsyncSignature will reuse the cache portion of flags despite updating ReturnType. Stashing bits in the flags is problematic.

Comment on lines +145 to +154
public bool IsAsyncVariant
{
get
{
return AsyncMethodKind is
AsyncMethodKind.AsyncVariantImpl or
AsyncMethodKind.AsyncVariantThunk;
}
}

Copy link
Member

Choose a reason for hiding this comment

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

Is this used? Looks like we also have an extension method with the same name. Maybe the extension method is not needed?

Comment on lines +169 to +181
/// <summary>
/// Is this method callable as an async method? (i.e. uses Async calling convention)
/// </summary>
public bool IsAsyncCallConv
{
get
{
return AsyncMethodKind is
AsyncMethodKind.AsyncVariantImpl or
AsyncMethodKind.AsyncVariantThunk or
AsyncMethodKind.AsyncExplicitImpl;
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Do we need this? Is calling this.IsAsyncCallConv different from this.Signature.IsAsyncCallConv?

Comment on lines +94 to +95
InitializeSignature();

Copy link
Member

Choose a reason for hiding this comment

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

This is a deoptimization, we should undo this.

(The return InitializeSignature(); form is tail-callable so JIT avoids building a frame for the method. The form in this PR is no longer tail callable, we need to build a frame to be able to call the method and return back.)

Comment on lines +168 to +174
if (method.IsAsync)
{
// We should not be creating any AsyncMethodVariants yet.
// This hasn't been implemented.
Debug.Assert(method.AsyncMethodKind is AsyncMethodKind.RuntimeAsync or AsyncMethodKind.AsyncExplicitImpl);
return null;
}
Copy link
Member

Choose a reason for hiding this comment

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

Do we compile any async method bodies with this in place?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, we skip all async methods with this.

/// </summary>
public abstract partial class MethodDelegator : MethodDesc
{
public abstract override AsyncMethodKind AsyncMethodKind { get; }
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public abstract override AsyncMethodKind AsyncMethodKind { get; }
public override AsyncMethodKind AsyncMethodKind => _wrappedMethod.AsyncMethodKind;

@VSadov
Copy link
Member

VSadov commented Nov 4, 2025

What considerations lead to turning EcmaMethod into the "impl" variant of async? I understand there are going to be warts no matter how we implement this, but on the surface EcmaMethod not having the signature that it has in the metadata will lead to a lot of warts. My instinct would be to keep the signature as-is.

I think the most "weird" part about async variants is that before when we had synthetic "siblings" we typically had synthetic signatures matched to synthetic IL, but with async we can have pairs where metadata signature and IL from metadata are cross-matched.

I think the signatures are more important as more scenarios deal with how methods look "outside" rather than with what is in their IL bodies. There are scenarios, like reflection, where async variants simply "do not exist".
From that point it does make more sense for EcmaMethod to have the same signature as in metadata. It would need to support scenarios where EcmaMethod is matched to IL that is not in metadata, but that is likely less "weird" than if we have it the other way.

/// <summary>
/// Either the AsyncMethodImplVariant or AsyncMethodThunkVariant of a method marked .IsAsync.
/// </summary>
public partial class AsyncMethodVariant : MethodDelegator, IJitHashableOnly
Copy link
Member

Choose a reason for hiding this comment

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

We should make this the same between R2R and NAOT

More context: #121295 (comment) and #121295 (comment)

I think single MethodDesc type is fine, but it does not sound like that it should be JIT-lifetime only.

namespace Internal.TypeSystem
{
/// <summary>
/// Either the AsyncMethodImplVariant or AsyncMethodThunkVariant of a method marked .IsAsync.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// Either the AsyncMethodImplVariant or AsyncMethodThunkVariant of a method marked .IsAsync.
/// MethodDesc that represents async calling convention entrypoint of a Task-returning method.

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.

4 participants