Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
54c4cc1
RuntimeAsync for interpreter phase 1
davidwrighton Nov 13, 2025
024ab55
More fixes
davidwrighton Nov 18, 2025
0064807
Current test tweaks
davidwrighton Nov 18, 2025
cda2252
Fix this pointer in generics issue
davidwrighton Nov 18, 2025
495abdf
Don't overwrite values that shouldn't be overwritten.
davidwrighton Nov 18, 2025
9dc46af
Add support for direct Async2 to Async2 calls, and fix incorrect hand…
davidwrighton Nov 19, 2025
08ea4fd
Add forward support to call stub generator for async methods
davidwrighton Nov 19, 2025
a3c1fdd
Merge branch 'main' of https://github.com/dotnet/runtime into Runtime…
davidwrighton Nov 19, 2025
9fb7e05
Progress towards async calling convention handling in the callstub ge…
davidwrighton Nov 20, 2025
24a3f5c
It all passes the tests!
davidwrighton Nov 20, 2025
9ae0d3b
Remove unnecessary data/dvar from HANDLE_CONTINUATION_RESUME
davidwrighton Nov 20, 2025
cd92605
Re-order InterpAsyncSuspendData for easier future diagnostics
davidwrighton Nov 20, 2025
01adff9
Fix setting methodStartIp
davidwrighton Nov 20, 2025
8b61a6f
Update tests to work for both JIT and interpreter scenarios
davidwrighton Nov 20, 2025
9913e56
Merge branch 'main' of https://github.com/dotnet/runtime into Runtime…
davidwrighton Dec 1, 2025
7cf7c52
Tweak the new CallJittedMethodRetBuffRSI function.
davidwrighton Dec 1, 2025
250ee75
Actually handle the merge details
davidwrighton Dec 1, 2025
9d8bf9d
Fix build on Linux x64
davidwrighton Dec 1, 2025
a5feaac
Remove ProtectValueClassFrame changes as they are not necessary
davidwrighton Dec 1, 2025
3806ea2
Add some documentation on how runtime generated async code generation…
davidwrighton Dec 1, 2025
d99c3f7
Fix WASM build
davidwrighton Dec 2, 2025
ded4f49
Fix Unix X64 stubs
davidwrighton Dec 2, 2025
811e321
Fixup formatting and some missing bits of info.
davidwrighton Dec 2, 2025
4765f06
Fit and finish details
davidwrighton Dec 2, 2025
ffb1656
Integrate with Jakob's changes around calli signature handling, and a…
davidwrighton Dec 2, 2025
1ea976f
Move the JIT copy of dataAsyncResumeInfo to corinfo.h and rename it C…
davidwrighton Dec 2, 2025
c5e0fbb
Fix more code review issues
davidwrighton Dec 3, 2025
c0124af
Apply suggestions from code review
davidwrighton Dec 3, 2025
96a9114
Merge branch 'RuntimeAsync_Interpreter' of https://github.com/davidwr…
davidwrighton Dec 3, 2025
2d74b6b
Fix GCC build break
davidwrighton Dec 3, 2025
354e365
More code review + disable RuntimeAsync by default so we can actually…
davidwrighton Dec 3, 2025
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
1 change: 1 addition & 0 deletions docs/design/coreclr/botr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Below is a table of contents.
- [Mixed Mode Assemblies](mixed-mode.md)
- [Guide For Porting](guide-for-porting.md)
- [Vectors and Intrinsics](vectors-and-intrinsics.md)
- [Runtime Async Codegen](runtime-async-codegen.md)


It may be possible that this table is not complete. You can get a complete list
Expand Down
16 changes: 16 additions & 0 deletions docs/design/coreclr/botr/clr-abi.md
Original file line number Diff line number Diff line change
Expand Up @@ -678,3 +678,19 @@ MyStruct Test2()
return default;
}
```

# Interpreter ABI details

The interpreter data stack is separately allocated from the normal "thread" stack, and it grows UP. The interpreter execution control stack is allocated on the "thread" stack, as a series of `InterpMethodContextFrame` values that are linked in a singly linked list onto an `InterpreterFrame` which is placed onto the Frame chain of the thread. `InterpMethodContextFrame` structures are always allocated in descending order so that a callee method's associated `InterpMethodContextFrame` is always located lower in memory compared to its caller or the containing `InterpreterFrame`.

The base stack pointer within a method never changes, but when a function is called in the interpreter it will have a stack pointer which is associated with the set of arguments passed. In effect argument passing is done by giving a portion of the temporary args space of the caller function to the callee.

All instructions and GC that address the stack pointer are relative to the current stack pointer, which does not move. This requires that implementations of the localloc instruction actually allocate the memory on the heap, and localloc'd memory is not actually tied to the data stack in any way.

The stack pointer in all interpreter functions is always aligned on a `INTERP_STACK_ALIGNMENT` boundary. Currently this is a 16 byte alignment requirement.

The stack elements are always aligned to at least `INTERP_STACK_SLOT_SIZE` and never more than `INTERP_STACK_ALIGNMENT` Given that today's implementation sets `INTERP_STACK_SLOT_SIZE` to 8 and `INTERP_STACK_ALIGNMENT` to 16, this implies all data on the stack is either aligned at an 8 or 16 byte alignment.

Primitive types smaller than 4 bytes are always zero or sign extended to 4 bytes when on the stack.

When a function is async it will have a continuation return. This return is not done using the data stack, but instead is done by setting the Continuation field in the `InterpreterFrame`. Thunks are responsible for setting/resetting this value as we enter/leave code compiled by the JIT.
129 changes: 129 additions & 0 deletions docs/design/coreclr/botr/runtime-async-codegen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Responsibilities of a code generator for implementing the Runtime Async feature

This document describes the behaviors that a code generator must conform to to correctly make the runtime async feature work correctly.

This document is NOT intended to describe the runtime-async feature. That is better described in the runtime-async specification. See (https://github.com/dotnet/runtime/blob/main/docs/design/specs/runtime-async.md).



The general responsibilities of the runtime-async code generator

1. Wrap the body of Task and ValueTask returning functions in try/finally blocks which set/reset the `ExecutionContext` and `SynchronizationContext`.

2. Allow the async thunk logic to work.

3. Generate Async Debug info (Not yet described in this document)f



# Identifying calls to Runtime-Async methods that can be handled by runtime-async

When compiling a call to a method that might be called in the optimized fashion, recognize the following sequence.

```
call[virt] <Method>
[ OPTIONAL ]
{
[ OPTIONAL - Used for ValueTask based ConfigureAwait ]
{
stloc X;
ldloca X
}
ldc.i4.0 / ldc.i4.1
call[virt] <ConfigureAwait> (The virt instruction is used for ConfigureAwait on a Task based runtime async function) NI_System_Threading_Tasks_Task_ConfigureAwait
}
call <Await> One of the functions which matches NI_System_Runtime_CompilerServices_AsyncHelpers_Await
```

A search for this sequence is done if Method is known to be async.

The dispatch to async functions save the `ExecutionContext` on suspension and restore it on resumption via `AsyncHelpers.CaptureExecutionContext` and `AsyncHelpers.RestoreExecutionContext` respectively

If PREFIX_TASK_AWAIT_CONTINUE_ON_CAPTURED_CONTEXT, then continuation mode shall be ContinuationContextHandling::ContinueOnCapturedContext otherwise ContinuationContextHandling::ContinueOnThreadPool.



# Non-optimized pattern

It is also legal for code to have a simple direct usage of `AsyncHelpers.Await`, `AsyncHelpers.AwaitAwaiter`, `AsyncHelpers.UnsafeAwaitAwaiter` or `AsyncHelpers.TransparentAwait`. To support this these functions are marked as async even though they do not return a Task/ValueTask.

Like other async calls the dispatch to these functions will save and restore the execution context on suspension/resumption.

The dispatch to these functions will set continuation mode to ContinuationContextHandling::None

# Calli of an async function

The dispatch to these functions will save and restore the execution context only on async dispatch.

# The System.Runtime.CompilerServices.AsyncHelpers::AsyncSuspend intrinsic

When encountered, triggers the function to suspend immediately, and return the passed in Continuation.

# Saving and restoring of contexts

Capture the execution context before the suspension, and when the function resumes, call `AsyncHelpers.RestoreExecutionContext`. The context should be stored into the Continuation. The context may be captured by calling `AsyncHelpers.CaptureExecutionContext`.

# ABI for async function handling

There is an additional argument which is the Continuation. When calling a function normally, this is always set to 0. When resuming, this is set to the Continuation object. There is also an extra return argument. It is either 0 or a Continuation. If it is a continuation, then the calling function needs to suspend (if it is an async function), or generate a Task/ValueTask (if it is a async function wrapper).

## Suspension path

This is what is used in calls to async functions made from async functions.

```
bool didSuspend = false; // Needed for the context restore

(result, continuation) = call func(NULL /\* Continuation argument \*/, args)
if (continuation != NULL)
{
// Allocate new continuation
// Capture Locals
// Copy resumption details into continuation (Do things like call AsyncHelpers.CaptureContinuationContext or AsyncHelpers.CaptureExecutionContext as needed)
// Chain to continuation returned from called function
// IF in a function which saves the exec and sync contexts, and we haven't yet suspended, restore the old values.
// return.

// Resumption point

// Copy values out of continuation (including captured sync context and execution context locals)
// If the continuation may have an exception, check to see if its there, and if it is, throw it. Do this if CORINFO\_CONTINUATION\_HAS\_EXCEPTION is set.
// If the continuation has a return value, copy it out of the continuation. (CORINFO\_CONTINUATION\_HAS\_RESULT is set)
}
```

## Thunks path

This is what is used in non-async functions when calling an async function. Generally used in the AsyncResumptionStub and in the Task returning thunk.
```
(result, continuation) = call func(NULL /\* Continuation argument \*/, args)
place result onto IL evaluation stack
Place continuation into a local for access using the StubHelpers.AsyncCallContinuation() helper function.
```

Implement an intrinsic for StubHelpers.AsyncCallContinuation() which will load the most recent value stored into the continuation local.

# Behavior of ContinuationContextHandling

This only applies to calls which where ContinuationContextHandling is not ContinuationContextHandling::None.

If set to ContinuationContextHandling::ContinueOnCapturedContext

- The Continuation shall have an allocated data member for the captured context, and the CORINFO_CONTINUATION_HAS_CONTINUATION_CONTEXT flag shall be set on the continuation.

- The Continuation will store the captured synchronization context. This is done by calling `AsyncHelpers.CaptureContinuationContext(ref newContinuation.ContinuationContext, ref newContinuation.Flags)` while filling in the `Continuation`.

If set to ContinuationContextHandling::ContinueOnThreadPool
- The Continuation shall have the CORINFO_CONTINUATION_CONTINUE_ON_THREAD_POOL flag set

# Exception handling behavior

If an async function is called within a try block (In the jit hasTryIndex return true), set the CORINFO\_CONTINUATION\_HAS\_EXCEPTION bit on the Continuation and make it large enough.

# Locals handling

ByRef locals must not be captured. In fact, we should NULL out any locals which are ByRefs or ByRef-like. Currently we do not do this on synchronous execution, but logically possibly we should.

# Saving and restoring the synchronization and execution contexts

The code generator must save/restore the sync and execution contexts around the body of all Task/ValueTask methods when directly called with a null continuation context. The EE communicates when this is necessary with the `CORINFO_ASYNC_SAVE_CONTEXTS` flag returned through `getMethodInfo`.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Runtime.Versioning;
Expand Down Expand Up @@ -83,7 +84,7 @@ internal enum ContinuationFlags
ContinueOnCapturedTaskScheduler = 64,
}

// Keep in sync with dataAsyncResumeInfo in the JIT
// Keep in sync with CORINFO_AsyncResumeInfo in corinfo.h
internal unsafe struct ResumeInfo
{
public delegate*<Continuation, ref byte, Continuation?> Resume;
Expand Down Expand Up @@ -144,6 +145,18 @@ public ref byte GetResultStorageOrNull()

public static partial class AsyncHelpers
{
#if FEATURE_INTERPRETER
[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "AsyncHelpers_ResumeInterpreterContinuation")]
private static partial void AsyncHelpers_ResumeInterpreterContinuation(ObjectHandleOnStack cont, ref byte resultStorage);

internal static Continuation? ResumeInterpreterContinuation(Continuation cont, ref byte resultStorage)
{
ObjectHandleOnStack contHandle = ObjectHandleOnStack.Create(ref cont);
AsyncHelpers_ResumeInterpreterContinuation(contHandle, ref resultStorage);
return cont;
}
#endif

// This is the "magic" method on which other "Await" methods are built.
// Calling this from an Async method returns the continuation to the caller thus
// explicitly initiates suspension.
Expand Down
26 changes: 19 additions & 7 deletions src/coreclr/inc/corinfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -1854,7 +1854,13 @@ enum { LCL_FINALLY_MARK = 0xFC }; // FC = "Finally Call"
* when it generates code
**********************************************************************************/

typedef void* CORINFO_MethodPtr; // a generic method pointer
#ifdef TARGET_64BIT
typedef uint64_t TARGET_SIZE_T;
#else
typedef uint32_t TARGET_SIZE_T;
#endif

typedef TARGET_SIZE_T CORINFO_MethodPtr; // a generic method pointer

struct CORINFO_Object
{
Expand Down Expand Up @@ -1927,19 +1933,25 @@ struct CORINFO_RefArray : public CORINFO_Object
CORINFO_Object* refElems[1]; // actually of variable size;
};

struct CORINFO_RefAny
{
void * dataPtr;
CORINFO_CLASS_HANDLE type;
};

// The jit assumes the CORINFO_VARARGS_HANDLE is a pointer to a subclass of this
struct CORINFO_VarArgInfo
{
unsigned argBytes; // number of bytes the arguments take up.
// (The CORINFO_VARARGS_HANDLE counts as an arg)
};

// Note: Keep synchronized with AsyncHelpers.ResumeInfo
// Any changes to this are an R2R breaking change. Update the R2R verion as needed
struct CORINFO_AsyncResumeInfo
{
// delegate*<Continuation, ref byte, Continuation>
TARGET_SIZE_T Resume;
// Pointer in main code for diagnostics. See comments on
// ICorDebugInfo::AsyncSuspensionPoint::DiagnosticNativeOffset and
// ResumeInfo.DiagnosticIP in SPC.
TARGET_SIZE_T DiagnosticIP;
};

struct CORINFO_TYPE_LAYOUT_NODE
{
// Type handle if this is a SIMD type, i.e. for intrinsic types in
Expand Down
Loading
Loading