Skip to content

Commit 67df3da

Browse files
Async continuations for native AOT
The same program as in dotnet#121295 still works, but we can newly report async helpers as Intrinsic/Async. This implements things around suspension/resumption. Most of this change is around handling continuation types. Continuation types are synthetic types (created in the compiler) that derive from `Continuation` in the CoreLib. The JIT requests these based on the shape of locals it needs to preserve. What we get from the JIT in CorInfoImpl is size of the type and a pointer map (1011001 - non-zero means "GC pointer is here"). What we need to generate is a `MethodTable` for a type that derives from `Continuation` and has the specified GC layout after fields inherited from `Continuation`. We already have a similar thing in the compiler ("`MethodTable`" we use for GC statics), however because we need to derive from `Continuation` and presumably need to have a working vtable (with Equals/GetHashCode/ToString), we can't use this. So we emit a normal `MethodTable` for a synthetic type. Maybe we could optimize this to the one without vtable in the future. The synthetic type is a `MetadataType` that reports `Continuation` as the base type. It has instance fields of type `object` or `nint`, based on what we need. Because the standard type layout algorithm (the thing that assigns offsets to fields) would attempt to group GC fields together (it's a layout that works better for the GC), we also have a custom layouting algorithm that lays out these fields sequentially. The resumption stub is just a stubbed out TODO for someone else to look into for now.
1 parent 96d2c26 commit 67df3da

12 files changed

Lines changed: 366 additions & 30 deletions

File tree

src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
using System.Threading;
1313
using System.Threading.Tasks;
1414

15+
#if NATIVEAOT
16+
using Internal.Runtime;
17+
#endif
18+
1519
namespace System.Runtime.CompilerServices
1620
{
1721
internal struct ExecutionAndSyncBlockStore
@@ -144,11 +148,16 @@ private struct RuntimeAsyncAwaitState
144148

145149
private static unsafe Continuation AllocContinuation(Continuation prevContinuation, MethodTable* contMT)
146150
{
147-
Continuation newContinuation = (Continuation)RuntimeTypeHandle.InternalAllocNoChecks(contMT);
151+
#if NATIVEAOT
152+
Continuation newContinuation = (Continuation)RuntimeImports.RhNewObject(contMT);
153+
#else
154+
Continuation newContinuation = (Continuation)RuntimeTypeHandle.InternalAllocNoChecks(contMT);
155+
#endif
148156
prevContinuation.Next = newContinuation;
149157
return newContinuation;
150158
}
151159

160+
#if !NATIVEAOT
152161
private static unsafe Continuation AllocContinuationMethod(Continuation prevContinuation, MethodTable* contMT, int keepAliveOffset, MethodDesc* method)
153162
{
154163
LoaderAllocator loaderAllocator = RuntimeMethodHandle.GetLoaderAllocator(new RuntimeMethodHandleInternal((IntPtr)method));
@@ -170,6 +179,7 @@ private static unsafe Continuation AllocContinuationClass(Continuation prevConti
170179
}
171180
return newContinuation;
172181
}
182+
#endif
173183

174184
[BypassReadyToRun]
175185
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.Async)]

src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@
5555
<Link>ExceptionStringID.cs</Link>
5656
</Compile>
5757
</ItemGroup>
58+
<ItemGroup>
59+
<!-- TODO: (async) once we know which helpers can actually be shared, move those to libraries partition -->
60+
<Compile Include="..\..\..\System.Private.CoreLib\src\System\Runtime\CompilerServices\AsyncHelpers.CoreCLR.cs" />
61+
</ItemGroup>
5862
<ItemGroup>
5963
<Compile Include="Internal\Runtime\CompilerHelpers\DelegateHelpers.cs" />
6064
<Compile Include="Internal\Runtime\CompilerHelpers\LibraryInitializer.cs" />

src/coreclr/tools/Common/Compiler/CompilerTypeSystemContext.Async.cs

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
5+
using System.Collections.Generic;
6+
using System.Text;
7+
using System.Threading;
8+
49
using Internal.IL;
510
using Internal.TypeSystem;
611
using Internal.TypeSystem.Ecma;
@@ -40,5 +45,226 @@ protected override bool CompareValueToValue(AsyncMethodVariant value1, AsyncMeth
4045
protected override AsyncMethodVariant CreateValueFromKey(EcmaMethod key) => new AsyncMethodVariant(key);
4146
}
4247
private AsyncVariantImplHashtable _asyncVariantImplHashtable = new AsyncVariantImplHashtable();
48+
49+
public MetadataType GetContinuationType(GCPointerMap pointerMap)
50+
{
51+
return _continuationTypeHashtable.GetOrCreateValue(pointerMap);
52+
}
53+
54+
private sealed class ContinuationTypeHashtable : LockFreeReaderHashtable<GCPointerMap, ContinuationType>
55+
{
56+
private readonly CompilerTypeSystemContext _parent;
57+
private MetadataType _continuationType;
58+
59+
public ContinuationTypeHashtable(CompilerTypeSystemContext parent)
60+
=> _parent = parent;
61+
62+
protected override int GetKeyHashCode(GCPointerMap key) => key.GetHashCode();
63+
protected override int GetValueHashCode(ContinuationType value) => value.PointerMap.GetHashCode();
64+
protected override bool CompareKeyToValue(GCPointerMap key, ContinuationType value) => key.Equals(value.PointerMap);
65+
protected override bool CompareValueToValue(ContinuationType value1, ContinuationType value2)
66+
=> value1.PointerMap.Equals(value2.PointerMap);
67+
protected override ContinuationType CreateValueFromKey(GCPointerMap key)
68+
{
69+
if (_continuationType == null)
70+
_continuationType = _parent.SystemModule.GetKnownType("System.Runtime.CompilerServices"u8, "Continuation"u8);
71+
return new ContinuationType(_continuationType, key);
72+
}
73+
}
74+
private ContinuationTypeHashtable _continuationTypeHashtable;
75+
76+
/// <summary>
77+
/// An async continuation type. The code generator will request this to store local state
78+
/// through an async suspension/resumption. We only identify these using a <see cref="GCPointerMap"/>
79+
/// since that's all the code generator cares about - size of the type, and where the GC pointers are.
80+
/// </summary>
81+
private sealed class ContinuationType : MetadataType
82+
{
83+
private readonly MetadataType _continuationType;
84+
private FieldDesc[] _fields;
85+
public GCPointerMap PointerMap { get; }
86+
87+
public override DefType[] ExplicitlyImplementedInterfaces => [];
88+
public override ReadOnlySpan<byte> Name => Encoding.UTF8.GetBytes(DiagnosticName);
89+
public override ReadOnlySpan<byte> Namespace => [];
90+
91+
// The layout of the type is "sequential-in-spirit", but since there are GC pointers,
92+
// the standard layout algorithm wouldn't respect that. We have a custom layout algorithm.
93+
// The following layout-related properties are meaningless.
94+
public override bool IsExplicitLayout => false;
95+
public override bool IsSequentialLayout => false;
96+
public override bool IsExtendedLayout => false;
97+
public override bool IsAutoLayout => false;
98+
public override ClassLayoutMetadata GetClassLayout() => default;
99+
100+
public override bool IsBeforeFieldInit => false;
101+
public override ModuleDesc Module => _continuationType.Module;
102+
public override MetadataType BaseType => _continuationType;
103+
public override bool IsSealed => true;
104+
public override bool IsAbstract => false;
105+
public override MetadataType ContainingType => null;
106+
public override PInvokeStringFormat PInvokeStringFormat => default;
107+
public override string DiagnosticName => $"ContinuationType_{PointerMap}";
108+
public override string DiagnosticNamespace => "";
109+
protected override int ClassCode => 0x528741a;
110+
public override TypeSystemContext Context => _continuationType.Context;
111+
112+
public ContinuationType(MetadataType continuationType, GCPointerMap pointerMap)
113+
=> (_continuationType, PointerMap) = (continuationType, pointerMap);
114+
115+
public override bool HasCustomAttribute(string attributeNamespace, string attributeName) => false;
116+
public override IEnumerable<MetadataType> GetNestedTypes() => [];
117+
public override MetadataType GetNestedType(string name) => null;
118+
protected override MethodImplRecord[] ComputeVirtualMethodImplsForType() => [];
119+
public override MethodImplRecord[] FindMethodsImplWithMatchingDeclName(ReadOnlySpan<byte> name) => [];
120+
121+
protected override int CompareToImpl(TypeDesc other, TypeSystemComparer comparer)
122+
{
123+
GCPointerMap otherPointerMap = ((ContinuationType)other).PointerMap;
124+
return PointerMap.CompareTo(otherPointerMap);
125+
}
126+
127+
public override int GetHashCode() => PointerMap.GetHashCode();
128+
129+
protected override TypeFlags ComputeTypeFlags(TypeFlags mask)
130+
{
131+
TypeFlags flags = 0;
132+
133+
if ((mask & TypeFlags.HasGenericVarianceComputed) != 0)
134+
{
135+
flags |= TypeFlags.HasGenericVarianceComputed;
136+
}
137+
138+
if ((mask & TypeFlags.CategoryMask) != 0)
139+
{
140+
flags |= TypeFlags.Class;
141+
}
142+
143+
flags |= TypeFlags.HasFinalizerComputed;
144+
flags |= TypeFlags.AttributeCacheComputed;
145+
146+
return flags;
147+
}
148+
149+
private void InitializeFields()
150+
{
151+
FieldDesc[] fields = new FieldDesc[PointerMap.Size];
152+
153+
for (int i = 0; i < PointerMap.Size; i++)
154+
fields[i] = new ContinuationField(this, i);
155+
156+
Interlocked.CompareExchange(ref _fields, fields, null);
157+
}
158+
public override IEnumerable<FieldDesc> GetFields()
159+
{
160+
if (_fields == null)
161+
{
162+
InitializeFields();
163+
}
164+
return _fields;
165+
}
166+
167+
/// <summary>
168+
/// A field on a continuation type. The type of the field is determined by consulting the
169+
/// associated GC pointer map and it's either `object` or `nint`.
170+
/// </summary>
171+
private sealed class ContinuationField : FieldDesc
172+
{
173+
private readonly ContinuationType _owningType;
174+
private readonly int _index;
175+
176+
public ContinuationField(ContinuationType owningType, int index)
177+
=> (_owningType, _index) = (owningType, index);
178+
179+
public override MetadataType OwningType => _owningType;
180+
public override TypeDesc FieldType => Context.GetWellKnownType(_owningType.PointerMap[_index] ? WellKnownType.Object : WellKnownType.IntPtr);
181+
public override bool HasEmbeddedSignatureData => false;
182+
public override bool IsStatic => false;
183+
public override bool IsInitOnly => false;
184+
public override bool IsThreadStatic => false;
185+
public override bool HasRva => false;
186+
public override bool IsLiteral => false;
187+
public override TypeSystemContext Context => _owningType.Context;
188+
public override EmbeddedSignatureData[] GetEmbeddedSignatureData() => null;
189+
public override bool HasCustomAttribute(string attributeNamespace, string attributeName) => false;
190+
191+
protected override int ClassCode => 0xc761a66;
192+
protected override int CompareToImpl(FieldDesc other, TypeSystemComparer comparer)
193+
{
194+
var otherField = (ContinuationField)other;
195+
int result = _index.CompareTo(otherField._index);
196+
if (result != 0)
197+
return result;
198+
199+
return comparer.Compare(_owningType, otherField._owningType);
200+
}
201+
}
202+
}
203+
204+
/// <summary>
205+
/// Layout algorithm that lays out continuation types. It ensures the type has the layout
206+
/// that the code generator requested (the GC pointers are where we need them).
207+
/// </summary>
208+
private sealed class ContinuationTypeFieldLayoutAlgorithm : FieldLayoutAlgorithm
209+
{
210+
public override bool ComputeContainsGCPointers(DefType type)
211+
{
212+
// ContainsGCPointers because the base already has some.
213+
Debug.Assert(type.BaseType.ContainsGCPointers);
214+
return true;
215+
}
216+
217+
public override ComputedInstanceFieldLayout ComputeInstanceLayout(DefType type, InstanceLayoutKind layoutKind)
218+
{
219+
var continuationType = (ContinuationType)type;
220+
var continuationBaseType = (EcmaType)continuationType.BaseType;
221+
Debug.Assert(continuationBaseType.Name.SequenceEqual("Continuation"u8));
222+
223+
LayoutInt pointerSize = continuationType.Context.Target.LayoutPointerSize;
224+
225+
LayoutInt dataOffset = continuationBaseType.InstanceByteCountUnaligned;
226+
227+
#if DEBUG
228+
// Validate we match the expected DataOffset value.
229+
// DataOffset is a const int32 field in the Continuation class.
230+
EcmaField dataOffsetField = continuationBaseType.GetField("DataOffset"u8);
231+
Debug.Assert(dataOffsetField.IsLiteral);
232+
233+
var reader = dataOffsetField.MetadataReader;
234+
var constant = reader.GetConstant(reader.GetFieldDefinition(dataOffsetField.Handle).GetDefaultValue());
235+
Debug.Assert(constant.TypeCode == System.Reflection.Metadata.ConstantTypeCode.Int32);
236+
int expectedDataOffset = reader.GetBlobReader(constant.Value).ReadInt32()
237+
+ pointerSize.AsInt /* + MethodTable field */;
238+
Debug.Assert(dataOffset.AsInt == expectedDataOffset);
239+
#endif
240+
FieldAndOffset[] offsets = new FieldAndOffset[continuationType.PointerMap.Size];
241+
int i = 0;
242+
243+
foreach (FieldDesc field in type.GetFields())
244+
{
245+
Debug.Assert(field.FieldType.GetElementSize() == pointerSize);
246+
offsets[i++] = new FieldAndOffset(field, dataOffset);
247+
dataOffset += pointerSize;
248+
}
249+
250+
return new ComputedInstanceFieldLayout
251+
{
252+
FieldSize = pointerSize,
253+
FieldAlignment = pointerSize,
254+
ByteCountAlignment = pointerSize,
255+
ByteCountUnaligned = dataOffset,
256+
Offsets = offsets,
257+
IsAutoLayoutOrHasAutoLayoutFields = false,
258+
IsInt128OrHasInt128Fields = false,
259+
IsVectorTOrHasVectorTFields = false,
260+
LayoutAbiStable = false,
261+
};
262+
}
263+
264+
public override bool ComputeContainsByRefs(DefType type) => throw new NotImplementedException();
265+
public override bool ComputeIsUnsafeValueType(DefType type) => throw new NotImplementedException();
266+
public override ComputedStaticFieldLayout ComputeStaticFieldLayout(DefType type, StaticLayoutKind layoutKind) => default;
267+
public override ValueTypeShapeCharacteristics ComputeValueTypeShapeCharacteristics(DefType type) => throw new NotImplementedException();
268+
}
43269
}
44270
}

src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,7 @@ private void CompileMethodCleanup()
685685

686686
#if !READYTORUN
687687
_debugInfo = null;
688+
_asyncResumptionStub = null;
688689
#endif
689690

690691
_debugLocInfos = null;
@@ -3393,7 +3394,19 @@ private void getAsyncInfo(ref CORINFO_ASYNC_INFO pAsyncInfoOut)
33933394
private CORINFO_CLASS_STRUCT_* getContinuationType(nuint dataSize, ref bool objRefs, nuint objRefsSize)
33943395
{
33953396
Debug.Assert(objRefsSize == (dataSize + (nuint)(PointerSize - 1)) / (nuint)PointerSize);
3397+
#if READYTORUN
33963398
throw new NotImplementedException("getContinuationType");
3399+
#else
3400+
GCPointerMapBuilder gcMapBuilder = new GCPointerMapBuilder((int)dataSize, PointerSize);
3401+
ReadOnlySpan<bool> bools = MemoryMarshal.CreateReadOnlySpan(ref objRefs, (int)objRefsSize);
3402+
for (int i = 0; i < bools.Length; i++)
3403+
{
3404+
if (bools[i])
3405+
gcMapBuilder.MarkGCPointer(i * PointerSize);
3406+
}
3407+
3408+
return ObjectToHandle(_compilation.TypeSystemContext.GetContinuationType(gcMapBuilder.ToGCMap()));
3409+
#endif
33973410
}
33983411

33993412
private mdToken getMethodDefFromMethod(CORINFO_METHOD_STRUCT_* hMethod)
@@ -3630,9 +3643,6 @@ public static ReadyToRunHelperId GetReadyToRunHelperFromStaticBaseHelper(CorInfo
36303643
return res;
36313644
}
36323645

3633-
private void getFunctionFixedEntryPoint(CORINFO_METHOD_STRUCT_* ftn, bool isUnsafeFunctionPointer, ref CORINFO_CONST_LOOKUP pResult)
3634-
{ throw new NotImplementedException("getFunctionFixedEntryPoint"); }
3635-
36363646
#pragma warning disable CA1822 // Mark members as static
36373647
private CorInfoHelpFunc getLazyStringLiteralHelper(CORINFO_MODULE_STRUCT_* handle)
36383648
#pragma warning restore CA1822 // Mark members as static
@@ -3748,7 +3758,11 @@ private bool getTailCallHelpers(ref CORINFO_RESOLVED_TOKEN callToken, CORINFO_SI
37483758
private CORINFO_METHOD_STRUCT_* getAsyncResumptionStub()
37493759
#pragma warning restore CA1822 // Mark members as static
37503760
{
3761+
#if READYTORUN
37513762
throw new NotImplementedException("Crossgen2 does not support runtime-async yet");
3763+
#else
3764+
return ObjectToHandle(_asyncResumptionStub ??= new AsyncResumptionStub(MethodBeingCompiled));
3765+
#endif
37523766
}
37533767

37543768
private byte[] _code;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Internal.IL.Stubs;
5+
using Internal.TypeSystem;
6+
7+
namespace ILCompiler
8+
{
9+
public partial class AsyncResumptionStub : ILStubMethod, IPrefixMangledMethod
10+
{
11+
MethodDesc IPrefixMangledMethod.BaseMethod => _owningMethod;
12+
13+
string IPrefixMangledMethod.Prefix => "Resume";
14+
}
15+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Internal.IL.Stubs;
5+
using Internal.TypeSystem;
6+
7+
namespace ILCompiler
8+
{
9+
public partial class AsyncResumptionStub : ILStubMethod
10+
{
11+
protected override int ClassCode => 0x773ab1;
12+
13+
protected override int CompareToImpl(MethodDesc other, TypeSystemComparer comparer)
14+
{
15+
return comparer.Compare(_owningMethod, ((AsyncResumptionStub)other)._owningMethod);
16+
}
17+
}
18+
}

0 commit comments

Comments
 (0)