Skip to content

Conversation

@jtschuster
Copy link
Member

Emits Task-returning async method thunks in crossgen2 as MethodSignatures with the AsyncVariant flag set

Adds GetPrimaryMethodDesc to get the most natural "primary" method desc for MethodDesc's that point to the same Ecma/Instantiated method. This is used in the places where we cast to EcmaMethod to get relevant metadata information.

GCRefMap emission also needed to be updated to account for the continuation parameter. Runtime's FakeGcScanRoots (used to verify the ReadyToRun images GCRefMap in debug config) also needed to updated to handle the continuation parameter.

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 support for emitting Task-returning async method thunks in crossgen2 by encoding them as MethodSignatures with the AsyncVariant flag set. The changes introduce a new GetPrimaryMethodDesc helper method to navigate between different MethodDesc variants (async, unboxing, P/Invoke) and their primary metadata representations. The GC reference map emission is updated to account for the additional async continuation parameter.

Key changes:

  • Adds AsyncVariant flag to ReadyToRunMethodSigFlags enum (requiring uint instead of byte to accommodate 0x100 value)
  • Implements GetPrimaryMethodDesc extension method to resolve various MethodDesc wrappers to their primary ECMA method
  • Updates ArgIterator and GCRefMapBuilder to handle async continuation parameter in both managed and native code
  • Modifies ReadyToRunILProvider to emit Task-returning thunks for async methods

Reviewed changes

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

Show a summary per file
File Description
src/coreclr/vm/frames.cpp Adds async continuation parameter handling in FakeGcScanRoots for GC scanning
src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs Adds display support for ASYNC flag in method signatures
src/coreclr/tools/aot/ILCompiler.ReadyToRun/TypeSystem/MethodDescExtensions.cs New file introducing GetPrimaryMethodDesc helper to unwrap MethodDesc variants
src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs Updates token resolution to use GetPrimaryMethodDesc and removes version bubble checks
src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj Adds new MethodDescExtensions.cs file to project
src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/ReadyToRunILProvider.cs Implements Task-returning async thunk emission for async methods
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunTableManager.cs Updates module resolution to use GetPrimaryMethodDesc
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs Updates async variant assertion to use new helper method
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs Adds IsPrimaryMethodDesc check in debug code and minor formatting fix
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/SignatureBuilder.cs Adds AsyncVariant flag emission in method signatures
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/MethodFixupSignature.cs Replaces unboxing check with IsPrimaryMethodDesc for fixup optimization
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/GCRefMapBuilder.cs Adds async continuation parameter to GC reference map generation
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ArgIterator.cs Implements async continuation argument offset calculation and allocation logic
src/coreclr/tools/Common/TypeSystem/IL/Stubs/AsyncResumptionStub.cs Exposes TargetMethod property for GetPrimaryMethodDesc navigation
src/coreclr/tools/Common/Internal/Runtime/ReadyToRunConstants.cs Changes ReadyToRunMethodSigFlags from byte to uint and adds AsyncVariant flag
src/coreclr/tools/Common/Compiler/AsyncMethodVariant.cs Adds GetAsyncVariant and GetTargetOfAsyncVariant extension methods

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

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 16 out of 16 changed files in this pull request and generated no new comments.

@jkotas
Copy link
Member

jkotas commented Dec 19, 2025

/azp run runtime-coreclr crossgen2 outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@jtschuster
Copy link
Member Author

/azp run runtime-coreclr crossgen2 outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@jtschuster
Copy link
Member Author

/azp run runtime-coreclr crossgen2 outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

codestream.EndTry(tryFilterRegion);
}
// Catch
// Filter - check if the caught object is a System.Exception
Copy link
Member

Choose a reason for hiding this comment

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

Should we make this workaround for R2R encoding limitation specific to R2R to avoid leaving perf on the table in NAOT?

What would it take to fix the encoding limitation in R2R?

Copy link
Member Author

@jtschuster jtschuster Jan 11, 2026

Choose a reason for hiding this comment

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

Should we make this workaround for R2R encoding limitation specific to R2R to avoid leaving perf on the table in NAOT?

Good idea, I'll add an option to switch between using a filter or typed catch.

What would it take to fix the encoding limitation in R2R?

It looks like crossgen encodes EH info as CORINFO_EH_CLAUSE, which is also in a few places in the runtime.

public struct CORINFO_EH_CLAUSE
{
public CORINFO_EH_CLAUSE_FLAGS Flags;
public uint TryOffset;
public uint TryLength;
public uint HandlerOffset;
public uint HandlerLength;
public uint ClassTokenOrOffset;
/* union
{
DWORD ClassToken; // use for type-based exception handlers
DWORD FilterOffset; // use for filter-based exception handlers (COR_ILEXCEPTION_FILTER is set)
};*/
}

struct READYTORUN_EXCEPTION_CLAUSE
{
CorExceptionFlag Flags;
DWORD TryStartPC;
DWORD TryEndPC;
DWORD HandlerStartPC;
DWORD HandlerEndPC;
union {
mdToken ClassToken;
DWORD FilterOffset;
};
};

struct CORCOMPILE_EXCEPTION_CLAUSE
{
CorExceptionFlag Flags;
DWORD TryStartPC;
DWORD TryEndPC;
DWORD HandlerStartPC;
DWORD HandlerEndPC;
union {
mdToken ClassToken;
DWORD FilterOffset;
};
};

struct EE_ILEXCEPTION_CLAUSE {
//Flags is not marked as volatile since it is always accessed
// from within a critical section
CorExceptionFlag Flags;
DWORD TryStartPC;
DWORD TryEndPC;
DWORD HandlerStartPC;
DWORD HandlerEndPC;
union {
void* TypeHandle;
mdToken ClassToken;
DWORD FilterOffset;
};
};

We could add a ModuleOverride flag or something similar for the R2R struct. Or we could make a class token of 0 represent "catch anything".

Copy link
Member

Choose a reason for hiding this comment

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

The token in EH clause from R2R image is resolved here:

Module* pModule = pMD->GetModule();
_ASSERTE(pModule != NULL);
SigTypeContext typeContext(pMD);
mdToken typeTok = pEHClause->ClassToken;
return ClassLoader::LoadTypeDefOrRefOrSpecThrowing(pModule, typeTok, &typeContext,
ClassLoader::ReturnNullIfNotFound);

We just need to ensure that the encoding produced by crossgen2 can be resolved in this method.

Or we could make a class token of 0 represent "catch anything".

Yes, it would be an option. There is an interesting detail in how to handle exceptions that do not inherit from System.Exception. These exceptions may or may not get wrapped as RuntimeWrappedException depending on per-assembly settings. What is the behavior we want for these stubs? Do we want them to be wrapped unconditionally to compat with async v1?

I see that you have added code in the filter to specifically handle this case. Do we have any test for it?

Copy link
Member Author

Choose a reason for hiding this comment

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

It doesn't look like we have tests for throwing non-exceptions.

It looks like async state machines catch System.Exception, not object.

In the stubs, the catch block passes the exception to TaskFromException(), so I'd expect we should rethrow / not catch anything that isn't a System.Exception.

We also could consider a new CorExceptionFlag enum value for "catch System.Exception".

Copy link
Member

Choose a reason for hiding this comment

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

The goal here is to match async v1 behavior. This implies that we need to test/validate the behavior of traditional async with regards to the RuntimeWrappedException behavior and match up to whatever it is (for cases where the async method is defined in an assemblies with both of the relevant options. Sounds like a test hole we need to fill.

Copy link
Member

Choose a reason for hiding this comment

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

I've opened #123194 to track adding tests for this

Copy link
Member

Choose a reason for hiding this comment

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

It looks like async state machines catch System.Exception, not object.

Do you mean async v1 state machines? They catch System.Exception in corelib. Corelib is marked with RuntimeCompatibilityAttribute(WrapNonExceptionThrows = true). It means they will catch non-exceptions wrapped in RuntimeWrappedException.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think I fully understand all the implications of WrapNonExceptionThrows on async v1 vs async v2. Do we want a passing test suite as a prereq for merging this PR? And do you have a preference for adding a CorExceptionFlag or treating ClassToken == 0 as System.Exception, or finding another solution? I lean towards using a new CorExceptionFlag for R2R.

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 want a passing test suite as a prereq for merging this PR?

David opened #123194 on it. It should not block this PR. I assume we may have problems in the existing JIT version as well.

I lean towards using a new CorExceptionFlag for R2R.

Fine with me.

@jtschuster
Copy link
Member Author

/azp run runtime-coreclr crossgen2 outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

With these changes, we no longer need separate IL for nativeaot and readytorun
@jtschuster
Copy link
Member Author

/azp run runtime-coreclr crossgen2 outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@jtschuster
Copy link
Member Author

/azp run runtime-coreclr crossgen2 outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

* Rework async thunks generation to cross module generics infra
- Add ability for code generating an ILStub to mark that the tokens might be generated
- Funnel through various details to ensure that we trigger creation of the ManifestModuleWrappedMethodIL as needed
- Adjust method fixup signature generation to allow for VersionsWithMethodBody code which is in the MutableModule
- Add a few todos about work we might do to allow cross module compilation of runtime async code.
  - Disable this for now though, since its much to complex to test without a working end-to-end scenario (See code in CrossModuleInlineableUncached

* Revert logic which attempts to treat System.Exception in EH clause specially
@jtschuster
Copy link
Member Author

/azp run runtime-coreclr crossgen2 outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@jtschuster
Copy link
Member Author

/azp run runtime-coreclr crossgen2 outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

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.

5 participants