Skip to content

Convert async context helpers from getAsyncInfo method handles to JIT helpers#125079

Open
jtschuster wants to merge 3 commits intodotnet:mainfrom
jtschuster:AsyncInfoHelpers
Open

Convert async context helpers from getAsyncInfo method handles to JIT helpers#125079
jtschuster wants to merge 3 commits intodotnet:mainfrom
jtschuster:AsyncInfoHelpers

Conversation

@jtschuster
Copy link
Member

Splits off work from #124203

Replace the method handle fields in CORINFO_ASYNC_INFO for async context helpers (CaptureContexts, RestoreContexts, CaptureExecutionContext, RestoreExecutionContext, RestoreContextsOnSuspension, CaptureContinuationContext) with proper CORINFO_HELP_* JIT helpers.

The previous approach passed method handles via getAsyncInfo and used gtNewCallNode(CT_USER_FUNC, ...) with getFunctionEntryPoint/setEntryPoint in crossgen2. This created MethodEntrypoint nodes in the R2R dependency graph, which made previously-uncompilable methods compilable and exposed a bug where IL 'jmp' instructions generate R2R indirect jumps that the delay-load thunk cannot resolve.

Using JIT helpers avoids this — helpers go through the ReadyToRun helper fixup table and don't add method entry points to the dependency graph.

Changes:

  • Add 6 new CORINFO_HELP_ASYNC_* helpers and DYNAMICJITHELPER entries
  • Add corresponding ReadyToRun helper IDs (0x123-0x128) and renumber AllocContinuation helpers to 0x120-0x122 for sequential layout
  • Convert JIT async.cpp to use gtNewHelperCallNode
  • Convert interpreter to use INTOP_CALL_HELPER_V_SS/SSS for CaptureContexts/RestoreContexts, and resolve suspend data method handles at compile time via getHelperFtn to avoid GC-unsafe CoreLibBinder lookups at runtime
  • Add helper mappings in crossgen2, NativeAOT, and R2RDump
  • Remove method handle fields from CORINFO_ASYNC_INFO and all places that populate/serialize them (VM, managed JitInterface, SuperPMI)

… helpers

Replace the method handle fields in CORINFO_ASYNC_INFO for async context
helpers (CaptureContexts, RestoreContexts, CaptureExecutionContext,
RestoreExecutionContext, RestoreContextsOnSuspension,
CaptureContinuationContext) with proper CORINFO_HELP_* JIT helpers.

The previous approach passed method handles via getAsyncInfo and used
gtNewCallNode(CT_USER_FUNC, ...) with getFunctionEntryPoint/setEntryPoint
in crossgen2. This created MethodEntrypoint nodes in the R2R dependency
graph, which made previously-uncompilable methods compilable and exposed
a bug where IL 'jmp' instructions generate R2R indirect jumps that the
delay-load thunk cannot resolve.

Using JIT helpers avoids this — helpers go through the ReadyToRun helper
fixup table and don't add method entry points to the dependency graph.

Changes:
- Add 6 new CORINFO_HELP_ASYNC_* helpers and DYNAMICJITHELPER entries
- Add corresponding ReadyToRun helper IDs (0x123-0x128) and renumber
    AllocContinuation helpers to 0x120-0x122 for sequential layout
- Convert JIT async.cpp to use gtNewHelperCallNode
- Convert interpreter to use INTOP_CALL_HELPER_V_SS/SSS for
    CaptureContexts/RestoreContexts, and resolve suspend data method
    handles at compile time via getHelperFtn to avoid GC-unsafe
    CoreLibBinder lookups at runtime
- Add helper mappings in crossgen2, NativeAOT, and R2RDump
- Remove method handle fields from CORINFO_ASYNC_INFO and all places
    that populate/serialize them (VM, managed JitInterface, SuperPMI)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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 converts the JIT’s async context save/restore plumbing from passing AsyncHelpers.* method handles via getAsyncInfo to using dedicated CORINFO_HELP_ASYNC_* JIT helpers, avoiding unwanted MethodEntrypoint R2R dependencies and the associated jmp/delay-load thunk resolution issue described in the PR.

Changes:

  • Introduces six new async-related CORINFO_HELP_ASYNC_* helpers and wires them through ReadyToRun helper IDs and DynamicJitHelper mappings.
  • Updates CoreCLR JIT (async.cpp) and the interpreter to emit helper calls instead of user-function calls through method handles.
  • Removes async helper method-handle fields from CORINFO_ASYNC_INFO and updates VM, tooling, and SuperPMI serialization accordingly.

Reviewed changes

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

Show a summary per file
File Description
src/coreclr/vm/jitinterface.cpp Stops populating removed async helper method handles in getAsyncInfo.
src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp Removes recording/replay of async helper method handles.
src/coreclr/tools/superpmi/superpmi-shared/agnostic.h Updates SuperPMI agnostic async info struct to match new layout.
src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs Maps new CorInfoHelpFunc values to R2R helper IDs.
src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs Adds textual decoding for new async R2R helper IDs.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs Maps new CorInfoHelpFunc values to R2R helper IDs for crossgen2.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs Removes unnecessary AsyncHelpers metadata rooting previously needed for getAsyncInfo method handles.
src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs Switches async infra dependency rooting from method entrypoints to helper entrypoints.
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/JitHelper.cs Adds ReadyToRun helper -> CoreLib entrypoint mapping for new async helpers.
src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs Removes async helper method-handle fields from managed CORINFO_ASYNC_INFO.
src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs Stops filling removed async helper method-handle fields in managed getAsyncInfo.
src/coreclr/tools/Common/JitInterface/CorInfoHelpFunc.cs Adds six new CORINFO_HELP_ASYNC_* values.
src/coreclr/tools/Common/Internal/Runtime/ReadyToRunConstants.cs Adds new async R2R helper IDs and renumbers continuation alloc helpers.
src/coreclr/jit/utils.cpp Defines helper call properties for the new async helpers.
src/coreclr/jit/async.cpp Uses gtNewHelperCallNode for async context capture/restore instead of user-func calls.
src/coreclr/interpreter/compiler.cpp Uses interpreter “call helper” opcodes for context capture/restore; resolves helper method handles at compile time for suspend/resume paths.
src/coreclr/inc/readytorunhelpers.h Registers new async helpers in the ReadyToRun helper mapping list.
src/coreclr/inc/readytorun.h Adds new async ReadyToRun helper IDs and renumbers existing continuation alloc helper IDs.
src/coreclr/inc/jithelpers.h Adds DYNAMICJITHELPER entries for the new async helpers.
src/coreclr/inc/corinfo.h Adds new helper enums and removes async helper method-handle fields from CORINFO_ASYNC_INFO.

Comment on lines +475 to +483
READYTORUN_HELPER_AllocContinuation = 0x120,
READYTORUN_HELPER_AllocContinuationClass = 0x121,
READYTORUN_HELPER_AllocContinuationMethod = 0x122,
READYTORUN_HELPER_AsyncCaptureContexts = 0x123,
READYTORUN_HELPER_AsyncRestoreContexts = 0x124,
READYTORUN_HELPER_AsyncCaptureExecCtx = 0x125,
READYTORUN_HELPER_AsyncRestoreExecCtx = 0x126,
READYTORUN_HELPER_AsyncRestoreCtxOnSusp = 0x127,
READYTORUN_HELPER_AsyncCaptureContinCtx = 0x128,
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

Renumbering existing ReadyToRunHelper values (AllocContinuation* moved from 0x113–0x115 to 0x120–0x122) is a ReadyToRun file-format compatibility break: older R2R images containing helper fixups with the previous IDs will no longer map in MapReadyToRunHelper() and can fail at runtime. Keep existing numeric IDs stable and only append new helper IDs (or add explicit backward-compat handling for the old values) instead of renumbering.

Copilot uses AI. Check for mistakes.
Comment on lines -173 to -176
CORINFO_CALL_INFO callInfo = {};
callInfo.hMethod = captureCall->gtCallMethHnd;
callInfo.methodFlags = info.compCompHnd->getMethodAttribs(callInfo.hMethod);
impMarkInlineCandidate(captureCall, MAKE_METHODCONTEXT(callInfo.hMethod), false, &callInfo, compInlineContext);
Copy link
Member

Choose a reason for hiding this comment

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

Can you please open a follow-up issue about reallowing these helpers to be inlined?

@jkotas
Copy link
Member

jkotas commented Mar 3, 2026

exposed a bug where IL 'jmp' instructions generate R2R indirect jumps that the delay-load thunk cannot resolve.

Why are not regular method calls exposed to this bug? Should we fix this bug instead?

@jtschuster
Copy link
Member Author

Why are not regular method calls exposed to this bug? Should we fix this bug instead?

I believe it's due to the expectations that getFunctionEntryPoint returns a callable entrypoint of the method, but crossgen returns a DelayLoad method call. Calling the codegen a bug probably isn't right, I think the real issue was crossgen trying to implement getFunctionEntryPoint when it doesn't make sense. But am I wrong and crossgen should be able to implement it?

@jkotas
Copy link
Member

jkotas commented Mar 3, 2026

It sounds like the problem solved by updateEntryPointForTailCall.

Copy link
Member

@janvorli janvorli left a comment

Choose a reason for hiding this comment

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

Interpreter compiler changes LGTM.

READYTORUN_HELPER_InitClass = 0x116,
READYTORUN_HELPER_InitInstClass = 0x117,

READYTORUN_HELPER_AllocContinuation = 0x120,
Copy link
Member

Choose a reason for hiding this comment

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

Is there any reason for skipping some values of the enum?

Copy link
Member

Choose a reason for hiding this comment

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

Pre-existing scheme - it gives us a chance to keep one-off additions logically together.

@jtschuster
Copy link
Member Author

It sounds like the problem solved by updateEntryPointForTailCall.

Setting isJumpableImportRequired=true for MethodEntrypoint doesn't work without additional changes to the JIT. We might be able to make some changes similar to #56669 to make getFunctionEntrypoint work, but I'm not familiar enough with the JIT to know for sure.

@jtschuster
Copy link
Member Author

jtschuster commented Mar 4, 2026

I created a couple alternatives to explore if we want to avoid using JIT helpers.

@jtschuster
Copy link
Member Author

I'm also exploring if it be viable to have getAsyncInfo return mdTokens for the AsyncHelper methods and having the JIT to call getCallInfo which works fine in crossgen. Is there any reason I should avoid trying that?

@jakobbotsch
Copy link
Member

I would probably suggest going with jtschuster#7 and then fixing JMP in the backend (can be done as a separate change). Alternatively we can abort R2R when we see CEE_JMP via implReadyToRunUnsupported, I think it would be fine too -- this IL instruction is rare.

Another option if the getFunctionEntryPoint implementation is questionable could be to return the lookup for the helpers from getAsyncInfo too.

I would not suggest returning tokens from getAsyncInfo. It is not going to be straightforward to handle what getCallInfo can return at the points where we need the helpers, and we cannot easily call into our usual helpers to create calls from these places.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

5 participants