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 @@ -41,7 +41,7 @@ public enum ReadyToRunImportSectionFlags : ushort
/// Constants for method and field encoding
/// </summary>
[Flags]
public enum ReadyToRunMethodSigFlags : byte
public enum ReadyToRunMethodSigFlags : ushort
{
READYTORUN_METHOD_SIG_None = 0x00,
READYTORUN_METHOD_SIG_UnboxingStub = 0x01,
Expand All @@ -52,6 +52,7 @@ public enum ReadyToRunMethodSigFlags : byte
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)?

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 meant this to indicate the signature is for the Thunk variant, either the AsyncThunkVariant or RuntimeAsync kinds. It feels more natural to put the Impl in the MethodDefEntryPoints table, and the thunk into the InstanceMethodEntryPoints table, but maybe this should be AsyncVariant. It sounds like the flag being AsyncVariant would make it resilient to implementations in other assemblies changing?

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

I see it being read in a few places, but I don't see crossgen2 writing that flag out anywhere. We could potentially reuse that, but I don't know enough to be confident that it wouldn't cause issues.

Copy link
Member

Choose a reason for hiding this comment

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

}

[Flags]
Expand Down
109 changes: 0 additions & 109 deletions src/coreclr/tools/Common/JitInterface/AsyncMethodDesc.cs

This file was deleted.

24 changes: 0 additions & 24 deletions src/coreclr/tools/Common/JitInterface/AsyncMethodDescFactory.cs

This file was deleted.

11 changes: 8 additions & 3 deletions src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,11 @@ private MethodDesc MethodBeingCompiled
{
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.

#else
return _methodCodeNode.Method;
#endif
}
}

Expand Down Expand Up @@ -877,6 +881,7 @@ private void Get_CORINFO_SIG_INFO(MethodSignature signature, CORINFO_SIG_INFO* s

if (!signature.IsStatic) sig->callConv |= CorInfoCallConv.CORINFO_CALLCONV_HASTHIS;
if (signature.IsExplicitThis) sig->callConv |= CorInfoCallConv.CORINFO_CALLCONV_EXPLICITTHIS;
if (signature.IsAsyncCallConv) sig->callConv |= CorInfoCallConv.CORINFO_CALLCONV_ASYNCCALL;

TypeDesc returnType = signature.ReturnType;

Expand Down Expand Up @@ -1395,7 +1400,7 @@ private bool resolveVirtualMethod(CORINFO_DEVIRTUALIZATION_INFO* info)

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.

}
else
{
Expand All @@ -1410,7 +1415,7 @@ private bool resolveVirtualMethod(CORINFO_DEVIRTUALIZATION_INFO* info)
info->detail = CORINFO_DEVIRTUALIZATION_DETAIL.CORINFO_DEVIRTUALIZATION_FAILED_DECL_NOT_REPRESENTABLE;
return false;
}
methodWithTokenDecl = new MethodWithToken(decl, declToken, null, false, null, devirtualizedMethodOwner: decl.OwningType);
methodWithTokenDecl = new MethodWithToken(decl, declToken, null, false, asyncVariant: false, null, devirtualizedMethodOwner: decl.OwningType);
}
MethodWithToken methodWithTokenImpl;
#endif
Expand All @@ -1436,7 +1441,7 @@ private bool resolveVirtualMethod(CORINFO_DEVIRTUALIZATION_INFO* info)
else
{
#if READYTORUN
methodWithTokenImpl = new MethodWithToken(nonUnboxingImpl, resolver.GetModuleTokenForMethod(nonUnboxingImpl.GetTypicalMethodDefinition(), allowDynamicallyCreatedReference: false, throwIfNotFound: true), null, unboxingStub, null, devirtualizedMethodOwner: impl.OwningType);
methodWithTokenImpl = new MethodWithToken(nonUnboxingImpl, resolver.GetModuleTokenForMethod(nonUnboxingImpl.GetTypicalMethodDefinition(), allowDynamicallyCreatedReference: false, throwIfNotFound: true), null, unboxingStub, asyncVariant: false, null, devirtualizedMethodOwner: impl.OwningType);
#endif

info->resolvedTokenDevirtualizedMethod = CreateResolvedTokenFromMethod(this, impl
Expand Down
2 changes: 2 additions & 0 deletions src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ public unsafe struct CORINFO_SIG_INFO
private uint totalILArgs() { return (uint)(numArgs + (hasImplicitThis() ? 1 : 0)); }
private bool isVarArg() { return ((getCallConv() == CorInfoCallConv.CORINFO_CALLCONV_VARARG) || (getCallConv() == CorInfoCallConv.CORINFO_CALLCONV_NATIVEVARARG)); }
internal bool hasTypeArg() { return ((callConv & CorInfoCallConv.CORINFO_CALLCONV_PARAMTYPE) != 0); }
private bool isAsyncCallConv() { return ((callConv & CorInfoCallConv.CORINFO_CALLCONV_ASYNCCALL) != 0); }
};

//----------------------------------------------------------------------------
Expand Down Expand Up @@ -377,6 +378,7 @@ public enum CorInfoCallConv
CORINFO_CALLCONV_HASTHIS = 0x20,
CORINFO_CALLCONV_EXPLICITTHIS = 0x40,
CORINFO_CALLCONV_PARAMTYPE = 0x80, // Passed last. Same as CORINFO_GENERICS_CTXT_FROM_PARAMTYPEARG
CORINFO_CALLCONV_ASYNCCALL = 0x100, // Is this a call to an async function?
}

// Represents the calling conventions supported with the extensible calling convention syntax
Expand Down
8 changes: 8 additions & 0 deletions src/coreclr/tools/Common/JitInterface/UnboxingMethodDesc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ public override MethodDesc InstantiateSignature(Instantiation typeInstantiation,
return this;
}

public override AsyncMethodKind AsyncMethodKind
{
get
{
return _wrappedMethod.AsyncMethodKind;
}
}

public override string ToString()
{
return "Unboxing MethodDesc: " + _wrappedMethod.ToString();
Expand Down
138 changes: 138 additions & 0 deletions src/coreclr/tools/Common/TypeSystem/Common/AsyncMethodVariant.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using Internal.JitInterface;


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.

/// </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.

Copy link
Member

Choose a reason for hiding this comment

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

Thinking about this, it might be better to do this in crossgen2 too. So take the CompilerTypeSystemContext.Async.cs from my PR and AsyncVariantImplMethod.cs too, but rename the method itself to AsyncMethodVariant like it's here (based on Jan's comments on my PR).

In the end this is not about how long this lives, but how many components get exposed to this. Longer living means more components get exposed, but not necessarily - we can still hide the existence of these thunks from most of the system.

Copy link
Member

Choose a reason for hiding this comment

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

we can still hide the existence of these thunks from most of the system.

You probably meant "existence of async variants".

{
private readonly AsyncMethodVariantFactory _factory;
private readonly AsyncMethodKind _asyncMethodKind;
private readonly int _jitVisibleHashCode;
private MethodSignature _asyncSignature;

public AsyncMethodVariant(MethodDesc wrappedMethod, AsyncMethodVariantFactory factory, AsyncMethodKind kind)
: base(wrappedMethod)
{
Debug.Assert(wrappedMethod.IsTaskReturning);
Debug.Assert(kind switch
{
AsyncMethodKind.AsyncVariantThunk => !wrappedMethod.IsAsync,
AsyncMethodKind.AsyncVariantImpl => wrappedMethod.IsAsync,
_ => false,
});
Comment on lines +26 to +31
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 the kind parameter? We can deduce the kind from the wrappedMethod. It would simplify the Factory too.

Copy link
Member

Choose a reason for hiding this comment

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

Also, we should not need to cache the kind. We do cache IsAsync and kind is a trivial transform of that.

We probably do not need AsyncMethodKind enum at all.

Copy link
Member

Choose a reason for hiding this comment

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

Also, we should not need to cache the kind. We do cache IsAsync and kind is a trivial transform of that.

We probably do not need AsyncMethodKind enum at all.

Yeah, I was thinking about that too. We just need to be careful about:

class Foo<T>
{
    static T Return();
}

// This is not an async method in any way despite returning Task<int>
Foo<Task<int>>.Return();

But from method.IsAsync, method.Signature.IsAsyncCallConv and method.GetTypicalMethodDefinition().Signature.IsTaskReturning() we should be able to deduce everything (GetTypicalMethodDefinition() to deal with the T example above).

If we really need an enum, it could be an extension method and an enum stashed on the side, but honestly the names of these thunks still confuse me after looking at this for 2 days and when it's explicitly spelled out, it's more clear what we're checking.

_factory = factory;
_asyncMethodKind = kind;
_jitVisibleHashCode = HashCode.Combine(wrappedMethod.GetHashCode(), 0x310bb74f);
}

public MethodDesc Target => _wrappedMethod;

public override AsyncMethodKind AsyncMethodKind => _asyncMethodKind;

public override MethodSignature Signature
{
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.

}
}

public override MethodDesc GetCanonMethodTarget(CanonicalFormKind kind)
{
return _factory.GetOrCreateAsyncMethodImplVariant(_wrappedMethod.GetCanonMethodTarget(kind), _asyncMethodKind);
}

public override MethodDesc GetMethodDefinition()
{
var real = _wrappedMethod.GetMethodDefinition();
if (real == _wrappedMethod)
return this;

return _factory.GetOrCreateAsyncMethodImplVariant(real, _asyncMethodKind);
}

public override MethodDesc GetTypicalMethodDefinition()
{
var real = _wrappedMethod.GetTypicalMethodDefinition();
if (real == _wrappedMethod)
return this;
return _factory.GetOrCreateAsyncMethodImplVariant(real, _asyncMethodKind);
}

public override MethodDesc InstantiateSignature(Instantiation typeInstantiation, Instantiation methodInstantiation)
{
var real = _wrappedMethod.InstantiateSignature(typeInstantiation, methodInstantiation);
if (real == _wrappedMethod)
return this;
return _factory.GetOrCreateAsyncMethodImplVariant(real, _asyncMethodKind);
}

public override string ToString() => $"Async variant ({_asyncMethodKind}): " + _wrappedMethod.ToString();

protected override int ClassCode => throw new NotImplementedException();

protected override int CompareToImpl(MethodDesc other, TypeSystemComparer comparer)
{
throw new NotImplementedException();
}

protected override int ComputeHashCode()
{
throw new NotSupportedException("This method may not be stored as it is expected to only be used transiently in the JIT");
}

int IJitHashableOnly.GetJitVisibleHashCode() => _jitVisibleHashCode;
}

public sealed class AsyncMethodVariantFactory : Dictionary<(MethodDesc, AsyncMethodKind), AsyncMethodVariant>
{
public AsyncMethodVariant GetOrCreateAsyncMethodImplVariant(MethodDesc wrappedMethod, AsyncMethodKind kind)
{
Debug.Assert(wrappedMethod.IsAsync);
if (!TryGetValue((wrappedMethod, kind), out AsyncMethodVariant variant))
{
variant = new AsyncMethodVariant(wrappedMethod, this, kind);
this[(wrappedMethod, kind)] = variant;
}
return variant;
}

public AsyncMethodVariant GetOrCreateAsyncThunk(MethodDesc wrappedMethod)
{
return GetOrCreateAsyncMethodImplVariant(wrappedMethod, AsyncMethodKind.AsyncVariantThunk);
}

public AsyncMethodVariant GetOrCreateAsyncImpl(MethodDesc wrappedMethod)
{
return GetOrCreateAsyncMethodImplVariant(wrappedMethod, AsyncMethodKind.AsyncVariantImpl);
}
}

public static class AsyncMethodVariantExtensions
{
/// <summary>
/// Returns true if this MethodDesc is an AsyncMethodVariant, which should not escape the jit interface.
/// </summary>
public static bool IsAsyncVariant(this MethodDesc method)
{
return method is AsyncMethodVariant;
}

/// <summary>
/// Gets the wrapped method of the AsyncMethodVariant. This method is Task-returning.
/// </summary>
public static MethodDesc GetAsyncVariantDefinition(this MethodDesc method)
{
return ((AsyncMethodVariant)method).Target;
}
Comment on lines +130 to +136
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?

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Threading;

namespace Internal.TypeSystem
{
public sealed partial class InstantiatedMethod
{
public override AsyncMethodKind AsyncMethodKind
{
get
{
return _methodDef.AsyncMethodKind;
}
}
}
}
Loading
Loading