Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ public static partial class AsyncHelpers
[Intrinsic]
private static void AsyncSuspend(Continuation continuation) => throw new UnreachableException();

[Intrinsic]
[BypassReadyToRun]
internal static Continuation? AsyncCallContinuation() => throw new UnreachableException(); // Unconditionally expanded intrinsic

// Used during suspensions to hold the continuation chain and on what we are waiting.
// Methods like FinalizeTaskReturningThunk will unlink the state and wrap into a Task.
private struct RuntimeAsyncAwaitState
Expand Down
3 changes: 0 additions & 3 deletions src/coreclr/System.Private.CoreLib/src/System/StubHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1610,9 +1610,6 @@ internal static void MulticastDebuggerTraceHelper(object o, int count)

[Intrinsic]
internal static IntPtr NextCallReturnAddress() => throw new UnreachableException(); // Unconditionally expanded intrinsic

[Intrinsic]
internal static Continuation? AsyncCallContinuation() => throw new UnreachableException(); // Unconditionally expanded intrinsic
} // class StubHelpers

#if FEATURE_COMINTEROP
Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/jit/gentree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2265,7 +2265,7 @@ bool GenTreeCall::HasSideEffects(Compiler* compiler, bool ignoreExceptions, bool
// those cases the JIT does not know (and does not need to know) which arg is
// the async continuation.
//
// The VM also uses the StubHelpers.AsyncCallContinuation() intrinsic in the
// The VM also uses the AsyncHelpers.AsyncCallContinuation() intrinsic in the
// stubs discussed above. The JIT must take care in those cases to still mark
// the preceding call as an async call; this is required for correct LSRA
// behavior and GC reporting around the returned async continuation. This is
Expand Down
10 changes: 5 additions & 5 deletions src/coreclr/jit/importercalls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3323,7 +3323,7 @@ GenTree* Compiler::impIntrinsic(CORINFO_CLASS_HANDLE clsHnd,
return new (this, GT_LABEL) GenTree(GT_LABEL, TYP_I_IMPL);
}

if (ni == NI_System_StubHelpers_AsyncCallContinuation)
if (ni == NI_System_Runtime_CompilerServices_AsyncHelpers_AsyncCallContinuation)
{
GenTree* node = new (this, GT_ASYNC_CONTINUATION) GenTree(GT_ASYNC_CONTINUATION, TYP_REF);
node->SetHasOrderingSideEffect();
Expand Down Expand Up @@ -10872,6 +10872,10 @@ NamedIntrinsic Compiler::lookupNamedIntrinsic(CORINFO_METHOD_HANDLE method)
{
result = NI_System_Runtime_CompilerServices_AsyncHelpers_Await;
}
else if (strcmp(methodName, "AsyncCallContinuation") == 0)
{
result = NI_System_Runtime_CompilerServices_AsyncHelpers_AsyncCallContinuation;
}
}
else if (strcmp(className, "StaticsHelpers") == 0)
{
Expand Down Expand Up @@ -11129,10 +11133,6 @@ NamedIntrinsic Compiler::lookupNamedIntrinsic(CORINFO_METHOD_HANDLE method)
{
result = NI_System_StubHelpers_NextCallReturnAddress;
}
else if (strcmp(methodName, "AsyncCallContinuation") == 0)
{
result = NI_System_StubHelpers_AsyncCallContinuation;
}
}
}
else if (strcmp(namespaceName, "Text") == 0)
Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/jit/namedintrinsiclist.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ enum NamedIntrinsic : unsigned short
NI_System_RuntimeType_get_TypeHandle,
NI_System_StubHelpers_GetStubContext,
NI_System_StubHelpers_NextCallReturnAddress,
NI_System_StubHelpers_AsyncCallContinuation,

NI_Array_Address,
NI_Array_Get,
Expand All @@ -126,6 +125,7 @@ enum NamedIntrinsic : unsigned short

NI_System_Runtime_CompilerServices_AsyncHelpers_AsyncSuspend,
NI_System_Runtime_CompilerServices_AsyncHelpers_Await,
NI_System_Runtime_CompilerServices_AsyncHelpers_AsyncCallContinuation,

NI_System_Runtime_CompilerServices_StaticsHelpers_VolatileReadAsByref,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ public partial class AsyncResumptionStub : ILStubMethod

public AsyncResumptionStub(MethodDesc owningMethod)
{
Debug.Assert(owningMethod.IsAsyncVariant()
|| (owningMethod.IsAsync && !owningMethod.Signature.ReturnsTaskOrValueTask()));
Debug.Assert(owningMethod.IsAsyncCall());
_owningMethod = owningMethod;
}

Expand All @@ -41,13 +40,62 @@ private MethodSignature InitializeSignature()

public override MethodIL EmitIL()
{
var emitter = new ILEmitter();
ILCodeStream codeStream = emitter.NewCodeStream();
ILEmitter ilEmitter = new ILEmitter();
ILCodeStream ilStream = ilEmitter.NewCodeStream();

// TODO: match getAsyncResumptionStub from CoreCLR VM
codeStream.EmitCallThrowHelper(emitter, Context.GetHelperEntryPoint("ThrowHelpers"u8, "ThrowNotSupportedException"u8));
// Ported from jitinterface.cpp CEEJitInfo::getAsyncResumptionStub
if (!_owningMethod.Signature.IsStatic)
{
if (_owningMethod.OwningType.IsValueType)
{
ilStream.EmitLdc(0);
ilStream.Emit(ILOpcode.conv_u);
}
else
{
ilStream.Emit(ILOpcode.ldnull);
}
}

return emitter.Link(this);
foreach (var param in _owningMethod.Signature)
Copy link
Member

@VSadov VSadov Nov 8, 2025

Choose a reason for hiding this comment

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

On the JIT side here, between this and other arguments, we also put:

  • generic context arg, if present.
    It is set to 0, and will not be used by the calee. Continuation has captured the real value and that will be used instead. But we need to match what the calee expects, so we need to push 0 for the context.
  • continuation.
    it is our arg0. Per the ABI of Async* calls it goes right before the other arguments.
    (unless this is x86)

On x86, as usual, there is a difference and the hidden args go after formal ones and in reverse order.

--
*Note - the resume stub is an ordinary function, but what we are resuming is an Async method, thus we shuffle the continuation from our arg0 to its predefined/hidden position in the calee signature.

{
var local = ilEmitter.NewLocal(param);
ilStream.EmitLdLoca(local);
ilStream.Emit(ILOpcode.initobj, ilEmitter.NewToken(param));
ilStream.EmitLdLoc(local);
}
ilStream.Emit(ILOpcode.ldftn, ilEmitter.NewToken(_owningMethod));
Copy link
Member

@VSadov VSadov Nov 8, 2025

Choose a reason for hiding this comment

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

Not sure. Maybe this is correct in NAOT, but ldftn is a bit suspicious.

If this is a metadata token for the method that we are resuming, you may end up calling the Task-returning variant.
In the JIT counterpart we load the actual address of the JITed target method.
As in:

        pCode->EmitLDC((DWORD_PTR)m_finalCodeAddressSlot);
        pCode->EmitLDIND_I();

cc: @MichalStrehovsky - will ldftn do the right thing here?

Copy link
Member

@jkotas jkotas Nov 8, 2025

Choose a reason for hiding this comment

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

This should be emitted as a regular call and the JIT/EE interface and/or the JIT itself should make sure that the call does the right thing.

(Once we figure out what to do here, we may want to switch the JIT counterpart to the same plan.)

Copy link
Member

Choose a reason for hiding this comment

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

Yes, this should be a regular call. The call will be to a thing with RuntimeAsync calling convention; we can make tokens for that and we will make such token here. We probably don't need to pass generic context explicitly either.

But the question is what this will do in the JIT. My expectation is that it will take similar paths as a AsyncExplicitImpl method call.

Copy link
Member

Choose a reason for hiding this comment

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

But the question is what this will do in the JIT. My expectation is that it will take similar paths as a AsyncExplicitImpl method call.

Runtime async method calls pass a null continuation in the JIT. This needs to pass an actual continuation.

Copy link
Member

Choose a reason for hiding this comment

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

"Resume" in a way is the opposite of "Call". In Call we pass formal arguments, generic context, etc.., but continuation is null. In Resume only continuation is what is not default/null.

Doing CALLI with a special-crafted signature is the technique to do a low level "unsafe cast" through call conventions in IL. I have seen it in other places (like instantiating stubs?).

Copy link
Member

Choose a reason for hiding this comment

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

My expectation is that it will take similar paths as a AsyncExplicitImpl method call.

Note - what we are resuming may actually be AsyncExplicitImpl.

Anything with Async call conv may need resuming. Whether it has a variant with non-async call convention is unimportant here. Just the part is that it is Async (i.e. emitted as a state machine, has async call conv).

Copy link
Member

@jkotas jkotas Nov 8, 2025

Choose a reason for hiding this comment

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

I have seen it in other places (like instantiating stubs?).

I do not think that this trick is used anywhere in AOT compilers (just looked through all Emit(ILOpcode.calli - none of them look like that).

AsyncCallContinuation() intrinsic gets the async continuation from the last call. Can we have a counterpart that sets the async continuation for the next call?

We can try going the ldftn route, but I expect that it will hit problems with generics and the generated code won't be the best at the end if we manage to make it work. Managed function pointers in NAOT are tagged pointers. Regular calli of a managed function pointer compiles into if (ptr & tag_bit) { ptr[0](ptr[1] /* instantiating arg */, regular args); } else { ptr(regular args) }. We would probably need to suppress this for this ldftn + calli pair.

Copy link
Member

@VSadov VSadov Nov 8, 2025

Choose a reason for hiding this comment

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

Could CALLI trick work the same as in JIT case if we clear the tag after LDFTN?

(This all assumes that we can know if the calee/resumee has a generic context parameter and can push 0 to its position)

Copy link
Member

Choose a reason for hiding this comment

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

As another wild thought - I think this IL is never exposed outside of internals of VM/JIT/AOT. Perhaps we could use an IL prefix to tag a call as a resuming call?

Ex:

// load continuation 
   ldarg 0     
// the prefix means "consume one arg as a continuation argument, pass everything else as default"
// could be applied to calli as well
// callee must have async callconv
   resume.              
   call  <token>     

Copy link
Member

Choose a reason for hiding this comment

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

Could CALLI trick work the same as in JIT case if we clear the tag after LDFTN?

We can certainly try to make LDFTN + CALLI work and special-case it in enough places to make it do what we want. My gut feeling is that it won't be pretty.

Perhaps we could use an IL prefix to tag a call as a resuming call?

I think it is equivalent to AsyncCallContinuation setter intrinsic, just with a different encoding. We have prior art to use calls to encode internal custom modifiers of call instructions (e.g. NextCallReturnAddress or AsyncCallContinuation), so I would continue to do so here as well.

ilStream.Emit(ILOpcode.calli, ilEmitter.NewToken(this.Signature));
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.Signature the signature of the stub or the method that we are resuming?

Copy link
Member Author

Choose a reason for hiding this comment

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

this.Signature is the stub signature. This should be changed to _owningMethod.Signature.


bool returnsVoid = _owningMethod.Signature.ReturnType != Context.GetWellKnownType(WellKnownType.Void);
Internal.IL.Stubs.ILLocalVariable resultLocal = default;
if (!returnsVoid)
{
resultLocal = ilEmitter.NewLocal(_owningMethod.Signature.ReturnType);
ilStream.EmitStLoc(resultLocal);
}

MethodDesc asyncCallContinuation = Context.SystemModule.GetKnownType("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8)
.GetKnownMethod("AsyncCallContinuation"u8, null);
Comment on lines +78 to +79
Copy link
Member

Choose a reason for hiding this comment

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

For this to work you need to update JIT and the location in CoreCLR's corelib too.

(Cc @eduardo-vp since it's used in his PR too and I told him to look into getAsyncResumptionStub next. Since this one is now taken, #121443 is adding yet another IL thunk, so Eduardo, you could take that one.)

TypeDesc continuation = Context.SystemModule.GetKnownType("System.Runtime.CompilerServices"u8, "Continuation"u8);
var newContinuationLocal = ilEmitter.NewLocal(continuation);
ilStream.Emit(ILOpcode.call, ilEmitter.NewToken(asyncCallContinuation));
ilStream.EmitStLoc(newContinuationLocal);

if (!returnsVoid)
{
var doneResult = ilEmitter.NewCodeLabel();
ilStream.EmitLdLoc(newContinuationLocal);
ilStream.Emit(ILOpcode.brtrue, doneResult);
ilStream.EmitLdArg(1);
ilStream.EmitLdLoc(resultLocal);
ilStream.Emit(ILOpcode.stobj, ilEmitter.NewToken(_owningMethod.Signature.ReturnType));
ilStream.EmitLabel(doneResult);
}
ilStream.EmitLdLoc(newContinuationLocal);
ilStream.Emit(ILOpcode.ret);

return ilEmitter.Link(this);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
<Compile Include="..\..\Common\System\FormattingHelpers.cs" Link="Common\FormattingHelpers.cs" />
<Compile Include="..\..\Common\TypeSystem\IL\ILProvider.cs" Link="IL\ILProvider.cs" />
<Compile Include="..\..\Common\TypeSystem\IL\ILReader.cs" Link="IL\ILReader.cs" />
<Compile Include="..\..\Common\TypeSystem\IL\Stubs\AsyncResumptionStub.cs" Link="IL\Stubs\AsyncResumptionStub.cs" />
<Compile Include="..\..\Common\TypeSystem\IL\Stubs\AsyncResumptionStub.Sorting.cs" Link="IL\Stubs\AsyncResumptionStub.Sorting.cs" />
<Compile Include="..\..\Common\TypeSystem\IL\Stubs\AsyncResumptionStub.Mangling.cs" Link="IL\Stubs\AsyncResumptionStub.Mangling.cs" />
<Compile Include="..\..\Common\TypeSystem\IL\Stubs\ComparerIntrinsics.cs" Link="IL\Stubs\ComparerIntrinsics.cs" />
<Compile Include="..\..\Common\TypeSystem\IL\Stubs\InterlockedIntrinsics.cs" Link="IL\Stubs\InterlockedIntrinsics.cs" />
<Compile Include="..\..\Common\TypeSystem\IL\Stubs\RuntimeHelpersIntrinsics.cs" Link="IL\Stubs\RuntimeHelpersIntrinsics.cs" />
Expand Down
6 changes: 3 additions & 3 deletions src/coreclr/vm/corelib.h
Original file line number Diff line number Diff line change
Expand Up @@ -747,8 +747,9 @@ DEFINE_METHOD(ASYNC_HELPERS, COMPLETED_TASK, CompletedTask, NoSi
DEFINE_METHOD(ASYNC_HELPERS, CAPTURE_EXECUTION_CONTEXT, CaptureExecutionContext, NoSig)
DEFINE_METHOD(ASYNC_HELPERS, RESTORE_EXECUTION_CONTEXT, RestoreExecutionContext, NoSig)
DEFINE_METHOD(ASYNC_HELPERS, CAPTURE_CONTINUATION_CONTEXT, CaptureContinuationContext, NoSig)
DEFINE_METHOD(ASYNC_HELPERS, CAPTURE_CONTEXTS, CaptureContexts, NoSig)
DEFINE_METHOD(ASYNC_HELPERS, RESTORE_CONTEXTS, RestoreContexts, NoSig)
DEFINE_METHOD(ASYNC_HELPERS, CAPTURE_CONTEXTS, CaptureContexts, NoSig)
DEFINE_METHOD(ASYNC_HELPERS, RESTORE_CONTEXTS, RestoreContexts, NoSig)
DEFINE_METHOD(ASYNC_HELPERS, ASYNC_CALL_CONTINUATION, AsyncCallContinuation, SM_RetContinuation)

#ifdef TARGET_BROWSER
DEFINE_METHOD(ASYNC_HELPERS, HANDLE_ASYNC_ENTRYPOINT, HandleAsyncEntryPoint, SM_TaskOfInt_RetInt)
Expand Down Expand Up @@ -1098,7 +1099,6 @@ DEFINE_METHOD(STUBHELPERS, VALIDATE_BYREF, Validate
DEFINE_METHOD(STUBHELPERS, GET_STUB_CONTEXT, GetStubContext, SM_RetIntPtr)
DEFINE_METHOD(STUBHELPERS, LOG_PINNED_ARGUMENT, LogPinnedArgument, SM_IntPtr_IntPtr_RetVoid)
DEFINE_METHOD(STUBHELPERS, NEXT_CALL_RETURN_ADDRESS, NextCallReturnAddress, SM_RetIntPtr)
DEFINE_METHOD(STUBHELPERS, ASYNC_CALL_CONTINUATION, AsyncCallContinuation, SM_RetContinuation)
DEFINE_METHOD(STUBHELPERS, SAFE_HANDLE_ADD_REF, SafeHandleAddRef, SM_SafeHandle_RefBool_RetIntPtr)
DEFINE_METHOD(STUBHELPERS, SAFE_HANDLE_RELEASE, SafeHandleRelease, SM_SafeHandle_RetVoid)

Expand Down
Loading