diff --git a/docs/design/datacontracts/RuntimeTypeSystem.md b/docs/design/datacontracts/RuntimeTypeSystem.md index 83182979b22088..f074d92b353723 100644 --- a/docs/design/datacontracts/RuntimeTypeSystem.md +++ b/docs/design/datacontracts/RuntimeTypeSystem.md @@ -58,6 +58,11 @@ partial interface IRuntimeTypeSystem : IContract public virtual bool RequiresAlign8(TypeHandle typeHandle); // True if the MethodTable represents a continuation type used by the async continuation feature public virtual bool IsContinuation(TypeHandle typeHandle); + // Returns the GC pointer runs for the method table as (offset, size) pairs. Each + // run starts Offset bytes from the object pointer (`this`), where offset 0 + // is the method table pointer, and includes Size bytes of contiguous pointers + // For handles representing value types the object is assumed to be stored in the boxed layout. + public virtual IEnumerable<(uint Offset, uint Size)> GetGCDescSeries(TypeHandle typeHandle, uint numComponents = 0); public virtual bool IsDynamicStatics(TypeHandle typeHandle); public virtual ushort GetNumInterfaces(TypeHandle typeHandle); @@ -554,6 +559,63 @@ Contracts used: public bool RequiresAlign8(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.RequiresAlign8; + public bool IsContinuation(TypeHandle typeHandle) => typeHandle.IsMethodTable() + && ContinuationMethodTablePointer != TargetPointer.Null + && _methodTables[typeHandle.Address].ParentMethodTable == ContinuationMethodTablePointer; + + IEnumerable<(uint Offset, uint Size)> GetGCDescSeries(TypeHandle typeHandle, uint numComponents = 0) + { + // Returns empty if not a method table or has no GC pointers. + // Compute objectSize: baseSize + numComponents * componentSize. + // For non-array types, numComponents == 0 so objectSize == baseSize. + + // Read NumSeries from (mtAddress - pointerSize), sign-extended to native width. + // NumSeries == 0 → empty. + + // NumSeries > 0: Regular series. + // Used for normal objects and reference-type arrays (e.g. object[]). + // Memory layout (each slot is pointer-sized, growing away from MT): + // + // MT - (2*N+1)*ptrSize : series[N-1].seriessize + // MT - (2*N) *ptrSize : series[N-1].startoffset + // ... + // MT - 3*ptrSize : series[0].seriessize + // MT - 2*ptrSize : series[0].startoffset + // MT - 1*ptrSize : NumSeries (positive) + // MT : MethodTable + // + // The raw seriessize is stored with baseSize subtracted. + // Add objectSize back to get the true run length: + // trueRunLength = rawSeriesSize + objectSize + // For non-arrays objectSize == baseSize, recovering the actual run. + // For arrays objectSize > baseSize, extending the single series across all elements. + + // NumSeries < 0: Value-class (repeating) series. + // Used for arrays of value types containing GC references. + // |NumSeries| val_serie_items describe pointer runs within one array element. + // Memory layout: + // + // MT - (N+2)*ptrSize : val_serie[N-1] + // ... + // MT - 3*ptrSize : val_serie[0] + // MT - 2*ptrSize : startoffset + // MT - 1*ptrSize : NumSeries (-N) + // MT : MethodTable + // + // Each val_serie_item is { HALF_SIZE_T nptrs; HALF_SIZE_T skip; } packed into one pointer-width. + // HALF_SIZE_T is uint16 on 32-bit, uint32 on 64-bit. + // Read nptrs and skip as separate typed reads for endianness safety. + // runBytes = nptrs * pointerSize; advance currentOffset by runBytes + skip after each item. + // + // The val_serie_items describe the GC pointer layout of a single array element. + // An outer loop repeats the pattern across all elements in the array: + // currentOffset = startoffset + // while currentOffset <= objectSize - pointerSize: + // for each val_serie_item: + // yield (currentOffset, nptrs * pointerSize) + // currentOffset += nptrs * pointerSize + skip + } + public bool IsDynamicStatics(TypeHandle TypeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[TypeHandle.Address].Flags.IsDynamicStatics; public ushort GetNumInterfaces(TypeHandle TypeHandle) => !typeHandle.IsMethodTable() ? 0 : _methodTables[TypeHandle.Address].NumInterfaces; diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 5fd9ac2e2a117a..fb98c9670a21fa 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -173,6 +173,10 @@ CDAC_TYPE_FIELD(String, T_POINTER, m_FirstChar, cdac_data::m_First CDAC_TYPE_FIELD(String, T_UINT32, m_StringLength, cdac_data::m_StringLength) CDAC_TYPE_END(String) +CDAC_TYPE_BEGIN(ContinuationObject) +CDAC_TYPE_SIZE(sizeof(ContinuationObject)) +CDAC_TYPE_END(ContinuationObject) + CDAC_TYPE_BEGIN(Array) CDAC_TYPE_SIZE(sizeof(ArrayBase)) CDAC_TYPE_FIELD(Array, T_UINT32, m_NumComponents, cdac_data::m_NumComponents) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs index 8cc74fc5d1ee72..7b81723becd276 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs @@ -121,6 +121,12 @@ public interface IRuntimeTypeSystem : IContract bool RequiresAlign8(TypeHandle typeHandle) => throw new NotImplementedException(); // True if the MethodTable represents a continuation type used by the async continuation feature bool IsContinuation(TypeHandle typeHandle) => throw new NotImplementedException(); + /// + /// Enumerates GC pointer runs from the CGCDesc stored before the method table. + /// Returns (offset, size) pairs normalized to actual byte lengths. + /// See RuntimeTypeSystem.md for the full GCDesc format documentation. + /// + IEnumerable<(uint Offset, uint Size)> GetGCDescSeries(TypeHandle typeHandle, uint numComponents = 0) => throw new NotImplementedException(); bool IsDynamicStatics(TypeHandle typeHandle) => throw new NotImplementedException(); ushort GetNumInterfaces(TypeHandle typeHandle) => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs index 989db1625b33f9..cc65732c7954f3 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs @@ -77,6 +77,7 @@ public enum DataType StressMsg, StressMsgHeader, Object, + ContinuationObject, NativeObjectWrapperObject, ManagedObjectWrapperHolderObject, ManagedObjectWrapperLayout, diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs index 27e971fc0cd615..a0fe5b5eeb3a54 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs @@ -564,6 +564,81 @@ private Data.EEClass GetClassData(TypeHandle typeHandle) public bool IsContinuation(TypeHandle typeHandle) => typeHandle.IsMethodTable() && _continuationMethodTablePointer != TargetPointer.Null && _methodTables[typeHandle.Address].ParentMethodTable == _continuationMethodTablePointer; + + IEnumerable<(uint Offset, uint Size)> IRuntimeTypeSystem.GetGCDescSeries(TypeHandle typeHandle, uint numComponents) + { + if (!typeHandle.IsMethodTable()) + yield break; + + if (!ContainsGCPointers(typeHandle)) + yield break; + + uint baseSize = GetBaseSize(typeHandle); + uint componentSize = GetComponentSize(typeHandle); + uint objectSize = baseSize + numComponents * componentSize; + + ulong mtAddress = typeHandle.Address; + ulong pointerSize = (ulong)_target.PointerSize; + + // Sign-extend NumSeries from native pointer width. + long numSeries = _target.PointerSize == sizeof(uint) + ? (long)(int)_target.ReadPointer(mtAddress - pointerSize).Value + : (long)_target.ReadPointer(mtAddress - pointerSize).Value; + if (numSeries == 0) + yield break; + + if (numSeries > 0) + { + // Regular series: iterate from highest (closest to MT) to lowest. + for (ulong i = 0; i < (ulong)numSeries; i++) + { + ulong seriesBase = mtAddress - (3 + 2 * i) * pointerSize; + ulong rawSeriesSize = _target.ReadPointer(seriesBase).Value; + ulong seriesOffset = _target.ReadPointer(seriesBase + pointerSize).Value; + yield return ((uint)seriesOffset, (uint)(rawSeriesSize + objectSize)); + } + } + else + { + long absNumSeries = -numSeries; + ulong startOffset = _target.ReadPointer(mtAddress - 2 * pointerSize).Value; + + var seriesItems = new (uint Nptrs, uint Skip)[absNumSeries]; + for (long i = 0; i < absNumSeries; i++) + { + ulong itemAddress = mtAddress - (3 + (ulong)i) * pointerSize; + + // Read val_serie_item fields individually for endianness safety. + uint nptrs, skip; + if (_target.PointerSize == sizeof(uint)) + { + nptrs = _target.Read(itemAddress); + skip = _target.Read(itemAddress + sizeof(ushort)); + } + else + { + nptrs = _target.Read(itemAddress); + skip = _target.Read(itemAddress + sizeof(uint)); + } + + seriesItems[i] = (nptrs, skip); + } + + ulong currentOffset = startOffset; + for (int i = 0; i < numComponents; i++) + { + for (long j = 0; j < absNumSeries; j++) + { + if (currentOffset > objectSize - pointerSize) + yield break; + uint runBytes = seriesItems[j].Nptrs * (uint)pointerSize; + yield return ((uint)currentOffset, runBytes); + currentOffset += runBytes + seriesItems[j].Skip; + } + } + } + } + public bool IsDynamicStatics(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.IsDynamicStatics; public ushort GetNumInterfaces(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? (ushort)0 : _methodTables[typeHandle.Address].NumInterfaces; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/TypeNameBuilder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/TypeNameBuilder.cs index 201fdef999bab0..0bd9de10466510 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/TypeNameBuilder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/TypeNameBuilder.cs @@ -6,6 +6,7 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Text; +using Microsoft.Diagnostics.DataContractReader; using Microsoft.Diagnostics.DataContractReader.Contracts; namespace Microsoft.Diagnostics.DataContractReader.Legacy; @@ -283,7 +284,14 @@ private static void AppendTypeCore(ref TypeNameBuilder tnb, Contracts.TypeHandle Contracts.ModuleHandle moduleHandle = tnb.Target.Contracts.Loader.GetModuleHandleFromModulePtr(typeSystemContract.GetModule(typeHandle)); if (MetadataTokens.EntityHandle((int)typeDefToken).IsNil) { - tnb.AddName("(dynamicClass)"); + if (typeSystemContract.IsContinuation(typeHandle)) + { + AppendContinuationName(ref tnb, typeSystemContract, typeHandle); + } + else + { + tnb.AddName("(dynamicClass)"); + } } else { @@ -480,6 +488,52 @@ private void AddAssemblySpec(string? assemblySpec) } } + /// + /// Builds the synthetic name for a dynamically-created continuation method table, mirroring the + /// native AsyncContinuationsManager::PrintContinuationName in asynccontinuations.h. + /// + /// + /// The name has the form: + /// Continuation_<dataSize>[_<gcOffset>_<gcCount>]* + /// where: + /// + /// + /// dataSize is the number of bytes of data payload (base size minus the fixed + /// object-header and continuation-header overhead). + /// + /// + /// Each _gcOffset_gcCount pair describes one GC-pointer run: gcOffset is the + /// offset in bytes from the start of the data payload to the run, and gcCount is the + /// number of pointer-sized GC references in that run. + /// + /// + /// Only GC descriptor series whose startoffset is at or above the continuation data + /// payload (i.e., after the fixed CORINFO_Continuation header fields) are included. + /// + private static void AppendContinuationName(ref TypeNameBuilder tnb, IRuntimeTypeSystem typeSystemContract, TypeHandle typeHandle) + { + uint baseSize = typeSystemContract.GetBaseSize(typeHandle); + uint continuationDataOffset = tnb.Target.GetTypeInfo(DataType.ContinuationObject).Size!.Value; + uint objHeaderSize = tnb.Target.GetTypeInfo(DataType.ObjectHeader).Size!.Value; + uint dataSize = baseSize - (objHeaderSize + continuationDataOffset); + + var name = new StringBuilder("Continuation_"); + name.Append(dataSize); + + foreach ((uint seriesOffset, uint seriesSize) in typeSystemContract.GetGCDescSeries(typeHandle)) + { + if (seriesOffset < continuationDataOffset) + continue; + + name.Append('_'); + name.Append(seriesOffset - continuationDataOffset); + name.Append('_'); + name.Append(seriesSize / (uint)tnb.Target.PointerSize); + } + + tnb.AddNameNoEscaping(name); + } + private static void AppendNestedTypeDef(ref TypeNameBuilder tnb, MetadataReader reader, TypeDefinitionHandle typeDefToken, TypeNameFormat format) { TypeDefinition typeDef = reader.GetTypeDefinition(typeDefToken); diff --git a/src/native/managed/cdac/tests/MethodTableTests.cs b/src/native/managed/cdac/tests/MethodTableTests.cs index 89a87fd259cc4a..e8c15ad11f6734 100644 --- a/src/native/managed/cdac/tests/MethodTableTests.cs +++ b/src/native/managed/cdac/tests/MethodTableTests.cs @@ -27,6 +27,7 @@ public class MethodTableTests [DataType.ParamTypeDesc] = TargetTestHelpers.CreateTypeInfo(rtsBuilder.ParamTypeDescLayout), [DataType.TypeVarTypeDesc] = TargetTestHelpers.CreateTypeInfo(rtsBuilder.TypeVarTypeDescLayout), [DataType.GCCoverageInfo] = TargetTestHelpers.CreateTypeInfo(rtsBuilder.GCCoverageInfoLayout), + [DataType.ContinuationObject] = new Target.TypeInfo { Size = rtsBuilder.ContinuationObjectSize }, }; internal static (string Name, ulong Value)[] CreateContractGlobals(MockRTS rtsBuilder) @@ -649,4 +650,401 @@ public void RequiresAlign8(MockTarget.Architecture arch, bool flagSet) Contracts.TypeHandle typeHandle = contract.GetTypeHandle(methodTablePtr); Assert.Equal(flagSet, contract.RequiresAlign8(typeHandle)); } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesReturnsEmptyForNonMethodTable(MockTarget.Architecture arch) + { + // TypeDesc handles should yield no series + TargetPointer typeDescAddress = default; + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + MockParamTypeDesc typeDesc = rtsBuilder.AddParamTypeDesc(); + typeDescAddress = typeDesc.Address | (ulong)RuntimeTypeSystem_1.TypeHandleBits.TypeDesc; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeDescHandle = contract.GetTypeHandle(typeDescAddress); + Assert.Empty(contract.GetGCDescSeries(typeDescHandle)); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesReturnsEmptyWhenNoGCPointers(MockTarget.Architecture arch) + { + TargetPointer mtPtr = default; + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + MockEEClass eeClass = rtsBuilder.AddEEClass("NoGCPointers"); + MockMethodTable mt = rtsBuilder.AddMethodTable("NoGCPointers"); + uint baseSize = rtsBuilder.Builder.TargetTestHelpers.ObjectBaseSize; + mt.BaseSize = baseSize; + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + // MTFlags does NOT have ContainsGCPointers (0x01000000) set + mtPtr = mt.Address; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + Assert.False(contract.ContainsGCPointers(typeHandle)); + Assert.Empty(contract.GetGCDescSeries(typeHandle)); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesReturnsSingleSeries(MockTarget.Architecture arch) + { + TargetPointer mtPtr = default; + uint expectedSeriesOffset = 0; + uint expectedSeriesSize = 0; + + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + TargetTestHelpers helpers = rtsBuilder.Builder.TargetTestHelpers; + uint pointerSize = (uint)helpers.PointerSize; + + // Object layout: [ObjHeader][MT*][ref1] + // BaseSize = ObjHeader + MT* + ref1 = 3 * pointerSize + uint baseSize = helpers.ObjHeaderSize + 2u * pointerSize; + + // One series covering the single reference field. + // seriessize is stored as (actualSize - baseSize); for one pointer: pointerSize - baseSize + ulong rawSeriesSize = pointerSize - baseSize; // stored as size_t (wraps to large value) + ulong rawSeriesOffset = helpers.ObjHeaderSize + pointerSize; // after ObjHeader+MT* + + MockEEClass eeClass = rtsBuilder.AddEEClass("SingleRef"); + MockMethodTable mt = rtsBuilder.AddMethodTableWithGCDesc( + "SingleRef", + baseSize, + [(rawSeriesSize, rawSeriesOffset)]); + mt.MTFlags |= 0x01000000u; // ContainsGCPointers + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + + mtPtr = mt.Address; + expectedSeriesOffset = (uint)rawSeriesOffset; + // After normalization: rawSeriesSize + baseSize = pointerSize (one pointer-sized run) + expectedSeriesSize = pointerSize; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + Assert.True(contract.ContainsGCPointers(typeHandle)); + + (uint Offset, uint Size)[] series = contract.GetGCDescSeries(typeHandle).ToArray(); + Assert.Single(series); + Assert.Equal(expectedSeriesOffset, series[0].Offset); + Assert.Equal(expectedSeriesSize, series[0].Size); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesReturnsMultipleSeriesInOrder(MockTarget.Architecture arch) + { + TargetPointer mtPtr = default; + (uint Offset, uint Size)[] expectedSeries = []; + + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + TargetTestHelpers helpers = rtsBuilder.Builder.TargetTestHelpers; + uint pointerSize = (uint)helpers.PointerSize; + + // Two separate GC reference runs in the object. + // Object layout: [ObjHeader][MT*][ref1][nonref][ref2] + uint baseSize = helpers.ObjHeaderSize + 4u * pointerSize; + + ulong series0Offset = helpers.ObjHeaderSize + pointerSize; // ref1 field + ulong series0Size = pointerSize - baseSize; // raw stored size + + ulong series1Offset = helpers.ObjHeaderSize + 3u * pointerSize; // ref2 field + ulong series1Size = pointerSize - baseSize; + + MockEEClass eeClass = rtsBuilder.AddEEClass("TwoRefs"); + MockMethodTable mt = rtsBuilder.AddMethodTableWithGCDesc( + "TwoRefs", + baseSize, + // Ordered highest (lowest index) to lowest as required by AddMethodTableWithGCDesc + [(series0Size, series0Offset), (series1Size, series1Offset)]); + mt.MTFlags |= 0x01000000u; // ContainsGCPointers + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + + mtPtr = mt.Address; + // After normalization (rawSize + baseSize), each series covers one pointer + expectedSeries = + [ + ((uint)series0Offset, pointerSize), + ((uint)series1Offset, pointerSize), + ]; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + + (uint Offset, uint Size)[] series = contract.GetGCDescSeries(typeHandle).ToArray(); + Assert.Equal(expectedSeries.Length, series.Length); + for (int i = 0; i < expectedSeries.Length; i++) + { + Assert.Equal(expectedSeries[i].Offset, series[i].Offset); + Assert.Equal(expectedSeries[i].Size, series[i].Size); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesReturnsSingleValueClassSeries(MockTarget.Architecture arch) + { + // A negative NumSeries indicates a value-class (repeating) series layout. + // This models a 1-element array of struct { ref field; }: one val_serie_item with nptrs=1, skip=0. + TargetPointer mtPtr = default; + uint expectedOffset = 0; + uint expectedSize = 0; + + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + TargetTestHelpers helpers = rtsBuilder.Builder.TargetTestHelpers; + uint pointerSize = (uint)helpers.PointerSize; + + // Array of structs each containing one GC ref. + // startoffset is relative to the object pointer (MT* slot), past MT* + length. + uint startOffset = 2u * pointerSize; // past MT* + length + uint componentSize = pointerSize; // element is struct { ref field; } + uint baseSize = helpers.ObjHeaderSize + 2u * pointerSize; // ObjHeader + MT* + length + + MockEEClass eeClass = rtsBuilder.AddEEClass("ValueClassArray_1ref"); + MockMethodTable mt = rtsBuilder.AddMethodTableWithValueClassGCDesc( + "ValueClassArray_1ref", + baseSize, + startOffset, + [(1, 0)]); // nptrs=1, skip=0 + // Set array flags with componentSize so GetComponentSize returns the element size. + mt.MTFlags = (uint)(MethodTableFlags_1.WFLAGS_HIGH.HasComponentSize + | MethodTableFlags_1.WFLAGS_HIGH.Category_Array) + | componentSize + | 0x01000000u; // ContainsGCPointers + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + mtPtr = mt.Address; + + expectedOffset = startOffset; + expectedSize = 1u * pointerSize; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + Assert.True(contract.ContainsGCPointers(typeHandle)); + + // Pass numComponents=1 because value-class GCDesc iterates one element per component. + (uint Offset, uint Size)[] series = contract.GetGCDescSeries(typeHandle, 1).ToArray(); + Assert.Single(series); + Assert.Equal(expectedOffset, series[0].Offset); + Assert.Equal(expectedSize, series[0].Size); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesReturnsMultipleValueClassSeries(MockTarget.Architecture arch) + { + // Two val_serie_items: [2 ptrs, skip 2*ptrSize] [1 ptr, skip 0 bytes] + // This models a 1-element array of struct { ref a; ref b; int pad1; int pad2; ref c; } + TargetPointer mtPtr = default; + (uint Offset, uint Size)[] expectedSeries = []; + + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + TargetTestHelpers helpers = rtsBuilder.Builder.TargetTestHelpers; + uint pointerSize = (uint)helpers.PointerSize; + + // startoffset is relative to the object pointer (MT* slot), past MT* + length. + uint startOffset = 2u * pointerSize; // past MT* + length + uint baseSize = helpers.ObjHeaderSize + 2u * pointerSize; // ObjHeader + MT* + length + + uint skip = 2u * pointerSize; // two pointer-sized non-ref fields between runs + // Element layout: [ref a (ptr)][ref b (ptr)][pad1 (ptr)][pad2 (ptr)][ref c (ptr)] = 5 * pointerSize + uint componentSize = (2u + 2u + 1u) * pointerSize; + + MockEEClass eeClass = rtsBuilder.AddEEClass("ValueClassArray_2runs"); + MockMethodTable mt = rtsBuilder.AddMethodTableWithValueClassGCDesc( + "ValueClassArray_2runs", + baseSize, + startOffset, + [(2, skip), (1, 0)]); // first: 2 ptrs then skip, second: 1 ptr no skip + // Set array flags with componentSize so GetComponentSize returns the element size. + mt.MTFlags = (uint)(MethodTableFlags_1.WFLAGS_HIGH.HasComponentSize + | MethodTableFlags_1.WFLAGS_HIGH.Category_Array) + | componentSize + | 0x01000000u; // ContainsGCPointers + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + mtPtr = mt.Address; + + expectedSeries = + [ + (startOffset, 2u * pointerSize), // first run: 2 ptrs + (startOffset + 2u * pointerSize + skip, 1u * pointerSize), // second run: 1 ptr after skip + ]; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + + // Pass numComponents=1 because value-class GCDesc iterates one element per component. + (uint Offset, uint Size)[] series = contract.GetGCDescSeries(typeHandle, 1).ToArray(); + Assert.Equal(expectedSeries.Length, series.Length); + for (int i = 0; i < expectedSeries.Length; i++) + { + Assert.Equal(expectedSeries[i].Offset, series[i].Offset); + Assert.Equal(expectedSeries[i].Size, series[i].Size); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesRegularSeriesWithArrayNumComponents(MockTarget.Architecture arch) + { + // object[] has a single regular series. When numComponents > 0, the series size + // should extend across all elements: rawSeriesSize + baseSize + numComponents * componentSize. + TargetPointer mtPtr = default; + uint expectedSeriesOffset = 0; + + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + TargetTestHelpers helpers = rtsBuilder.Builder.TargetTestHelpers; + uint pointerSize = (uint)helpers.PointerSize; + + // object[] layout: [ObjHeader][MT*][Length][elem0][elem1][elem2] + // baseSize covers the header: ObjHeader + MT* + Length = 3 * pointerSize + // componentSize = pointerSize (each element is a reference) + uint baseSize = helpers.ObjHeaderSize + 2u * pointerSize; // ObjHeader + MT* + Length field + uint componentSize = pointerSize; + + // One series starting after ObjHeader+MT*+Length, covering element slots. + // rawSeriesSize is stored as (actualRunForOneElement - baseSize). + // For object[], the series covers from first element to end: actualRun = pointerSize (per-element). + // But the raw value encodes (pointerSize - baseSize) so that rawSeriesSize + objectSize gives total span. + ulong rawSeriesSize = pointerSize - baseSize; // wraps unsigned + ulong seriesOffset = helpers.ObjHeaderSize + 2u * pointerSize; // after header + length + + MockEEClass eeClass = rtsBuilder.AddEEClass("ObjectArray"); + MockMethodTable mt = rtsBuilder.AddMethodTableWithGCDesc( + "ObjectArray", + baseSize, + [(rawSeriesSize, seriesOffset)]); + // Set array flags: HasComponentSize | Category_Array | componentSize in low bits + mt.MTFlags = (uint)(MethodTableFlags_1.WFLAGS_HIGH.HasComponentSize + | MethodTableFlags_1.WFLAGS_HIGH.Category_Array) + | componentSize + | 0x01000000u; // ContainsGCPointers + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + + mtPtr = mt.Address; + expectedSeriesOffset = (uint)seriesOffset; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + Assert.True(contract.ContainsGCPointers(typeHandle)); + uint pointerSz = (uint)target.PointerSize; + + // With 0 components, series size = rawSeriesSize + baseSize = pointerSize (one element worth) + (uint Offset, uint Size)[] series0 = contract.GetGCDescSeries(typeHandle, 0).ToArray(); + Assert.Single(series0); + Assert.Equal(expectedSeriesOffset, series0[0].Offset); + Assert.Equal(pointerSz, series0[0].Size); + + // With 3 components, objectSize = baseSize + 3*pointerSize, so series size = pointerSize - baseSize + objectSize = 4*pointerSize + uint numComponents = 3; + (uint Offset, uint Size)[] series3 = contract.GetGCDescSeries(typeHandle, numComponents).ToArray(); + Assert.Single(series3); + Assert.Equal(expectedSeriesOffset, series3[0].Offset); + Assert.Equal((numComponents + 1) * pointerSz, series3[0].Size); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesValueClassRepeatingWithArrayNumComponents(MockTarget.Architecture arch) + { + // Array of structs where each element has one GC ref (nptrs=1, skip=pointerSize for a non-ref field). + // With numComponents > 0, the repeating pattern should iterate across multiple elements. + TargetPointer mtPtr = default; + + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + TargetTestHelpers helpers = rtsBuilder.Builder.TargetTestHelpers; + uint pointerSize = (uint)helpers.PointerSize; + + // Array layout: [ObjHeader][MT*][Length][elem0.ref][elem0.int][elem1.ref][elem1.int]... + // Each element is { ref field, int field } = 2 * pointerSize. + uint baseSize = helpers.ObjHeaderSize + 2u * pointerSize; // header + length + uint componentSize = 2u * pointerSize; + uint startOffset = helpers.ObjHeaderSize + 2u * pointerSize; // first element starts after header + + MockEEClass eeClass = rtsBuilder.AddEEClass("StructArray"); + MockMethodTable mt = rtsBuilder.AddMethodTableWithValueClassGCDesc( + "StructArray", + baseSize, + startOffset, + [(1, pointerSize)]); // nptrs=1 ref, skip=pointerSize (non-ref field) + mt.MTFlags = (uint)(MethodTableFlags_1.WFLAGS_HIGH.HasComponentSize + | MethodTableFlags_1.WFLAGS_HIGH.Category_Array) + | componentSize + | 0x01000000u; // ContainsGCPointers + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + + mtPtr = mt.Address; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + Assert.True(contract.ContainsGCPointers(typeHandle)); + uint elemSize = 2 * (uint)target.PointerSize; + uint startOff = 3u * (uint)target.PointerSize; + + // With 0 components, the for loop runs 0 times so the result is always empty. + (uint Offset, uint Size)[] series0 = contract.GetGCDescSeries(typeHandle, 0).ToArray(); + Assert.Empty(series0); + + // With 2 components, objectSize = baseSize + 2 * elemSize = baseSize + 4*ptr. + // The loop should produce 2 runs (one per element), each at the ref field of that element. + uint numComponents = 2; + (uint Offset, uint Size)[] series2 = contract.GetGCDescSeries(typeHandle, numComponents).ToArray(); + Assert.Equal(2, series2.Length); + Assert.Equal(startOff, series2[0].Offset); + Assert.Equal((uint)target.PointerSize, series2[0].Size); + Assert.Equal(startOff + elemSize, series2[1].Offset); + Assert.Equal((uint)target.PointerSize, series2[1].Size); + } } diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs index 8510203e0e5ae0..ccf99761dd4dd4 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs @@ -316,6 +316,8 @@ public class RuntimeTypeSystem internal ulong ContinuationMethodTableGlobalAddress => TestContinuationMethodTableGlobalAddress; internal ulong MethodDescAlignment => GetMethodDescAlignment(Builder.TargetTestHelpers); internal ulong ArrayBaseSize => Builder.TargetTestHelpers.ArrayBaseBaseSize; + // sizeof(ContinuationObject) = sizeof(MT*) + sizeof(Next*) + sizeof(Resume*) + sizeof(Flags) + sizeof(State) + internal uint ContinuationObjectSize => 3u * (uint)Builder.TargetTestHelpers.PointerSize + 8; public RuntimeTypeSystem(MockMemorySpace.Builder builder) : this(builder, (DefaultAllocationRangeStart, DefaultAllocationRangeEnd)) @@ -447,5 +449,133 @@ private TView Add(Layout layout, ulong size, string name) MockMemorySpace.HeapFragment fragment = TypeSystemAllocator.Allocate(size, name); return layout.Create(fragment.Data.AsMemory(), fragment.Address); } + + /// + /// Allocates a method table together with a CGCDesc immediately before it in memory. + /// + /// Descriptive name for the allocation. + /// Value to store in the method table's BaseSize field. + /// + /// GC descriptor series ordered from highest to lowest + /// (matching CGCDesc::GetHighestSeries down to GetLowestSeries). + /// Each entry is (Size, Offset) – the raw field values stored in the + /// CGCDescSeries struct (i.e. seriessize already has BaseSize subtracted). + /// + /// + /// A whose address is after the GCDesc bytes. + /// The caller is responsible for setting additional method table flags (e.g. + /// ContainsGCPointers = 0x01000000) and linking to an EEClass. + /// + internal MockMethodTable AddMethodTableWithGCDesc(string name, uint baseSize, (ulong Size, ulong Offset)[] series) + { + int pointerSize = Builder.TargetTestHelpers.PointerSize; + + // GCDesc layout (each slot is pointer-sized): + // [ series[N-1].seriessize ] [ series[N-1].startoffset ] <- lowest series (fragment start) + // ... + // [ series[0].seriessize ] [ series[0].startoffset ] <- highest series (MT - 3*ptrSize) + // [ NumSeries ] <- MT - 1*ptrSize + // [ MethodTable data starts here ] + int gcDescSize = (1 + 2 * series.Length) * pointerSize; + int totalSize = gcDescSize + MethodTableLayout.Size; + + MockMemorySpace.HeapFragment fragment = TypeSystemAllocator.Allocate((ulong)totalSize, $"GCDesc+MethodTable '{name}'"); + + // Write series entries. The highest series (index 0) lives closest to the MT (highest address), + // and the lowest series (index N-1) lives farthest from the MT (lowest address). + // So series[i] goes at fragment offset (N-1-i)*2*pointerSize. + for (int i = 0; i < series.Length; i++) + { + int seriesBase = (series.Length - 1 - i) * 2 * pointerSize; + Builder.TargetTestHelpers.WritePointer(fragment.Data.AsSpan(seriesBase, pointerSize), series[i].Size); + Builder.TargetTestHelpers.WritePointer(fragment.Data.AsSpan(seriesBase + pointerSize, pointerSize), series[i].Offset); + } + + // Write NumSeries immediately before the MT + Builder.TargetTestHelpers.WritePointer( + fragment.Data.AsSpan(series.Length * 2 * pointerSize, pointerSize), + (ulong)series.Length); + + // The MockMethodTable lives at offset gcDescSize within the combined fragment + ulong mtAddress = fragment.Address + (ulong)gcDescSize; + MockMethodTable mt = MethodTableLayout.Create(fragment.Data.AsMemory(gcDescSize, MethodTableLayout.Size), mtAddress); + mt.BaseSize = baseSize; + return mt; + } + + /// + /// Allocates a method table together with a value-class (repeating) CGCDesc immediately before it. + /// + /// Descriptive name for the allocation. + /// Value to store in the method table's BaseSize field. + /// The startoffset field of the CGCDescSeries (offset from object start). + /// + /// The val_serie_item entries, each as (nptrs, skip). + /// Index 0 corresponds to val_serie[0] (overlapping seriessize in the union). + /// + /// + /// A whose address is after the GCDesc bytes. + /// The caller is responsible for setting additional method table flags (e.g. + /// ContainsGCPointers = 0x01000000) and linking to an EEClass. + /// + internal MockMethodTable AddMethodTableWithValueClassGCDesc(string name, uint baseSize, ulong startOffset, (uint Nptrs, uint Skip)[] valSeries) + { + TargetTestHelpers helpers = Builder.TargetTestHelpers; + int pointerSize = helpers.PointerSize; + int halfSize = pointerSize / 2; + + // Value-class GCDesc layout (each slot is pointer-sized unless noted): + // [ val_serie[N-1] ] <- lowest address (fragment start) + // ... + // [ val_serie[0] ] <- overlaps seriessize in CGCDescSeries union + // [ startoffset ] <- one ptrSize slot + // [ NumSeries (-N) ] <- one ptrSize slot (negative) + // [ MethodTable data starts here ] + // + // ComputeSizeRepeating = sizeof(size_t) + sizeof(CGCDescSeries) + (N-1)*sizeof(val_serie_item) + // = ptrSize + 2*ptrSize + (N-1)*ptrSize = (N+2)*ptrSize + int gcDescSize = (valSeries.Length + 2) * pointerSize; + int totalSize = gcDescSize + MethodTableLayout.Size; + + MockMemorySpace.HeapFragment fragment = TypeSystemAllocator.Allocate((ulong)totalSize, $"ValueClassGCDesc+MethodTable '{name}'"); + + // Write val_serie items. val_serie[0] is closest to MT (highest address in the GCDesc region), + // val_serie[N-1] is farthest (lowest address). + // Each val_serie_item is { HALF_SIZE_T nptrs; HALF_SIZE_T skip; } + for (int i = 0; i < valSeries.Length; i++) + { + int itemOffset = (valSeries.Length - 1 - i) * pointerSize; + if (pointerSize == sizeof(uint)) + { + helpers.Write(fragment.Data.AsSpan(itemOffset, halfSize), (ushort)valSeries[i].Nptrs); + helpers.Write(fragment.Data.AsSpan(itemOffset + halfSize, halfSize), (ushort)valSeries[i].Skip); + } + else + { + helpers.Write(fragment.Data.AsSpan(itemOffset, halfSize), valSeries[i].Nptrs); + helpers.Write(fragment.Data.AsSpan(itemOffset + halfSize, halfSize), valSeries[i].Skip); + } + } + + // Write startoffset + helpers.WritePointer(fragment.Data.AsSpan(valSeries.Length * pointerSize, pointerSize), startOffset); + + // Write NumSeries as a negative value (-N) + long negativeCount = -valSeries.Length; + if (pointerSize == sizeof(uint)) + { + helpers.Write(fragment.Data.AsSpan((valSeries.Length + 1) * pointerSize, pointerSize), (int)negativeCount); + } + else + { + helpers.WritePointer(fragment.Data.AsSpan((valSeries.Length + 1) * pointerSize, pointerSize), unchecked((ulong)negativeCount)); + } + + // The MockMethodTable lives at offset gcDescSize within the combined fragment + ulong mtAddress = fragment.Address + (ulong)gcDescSize; + MockMethodTable mt = MethodTableLayout.Create(fragment.Data.AsMemory(gcDescSize, MethodTableLayout.Size), mtAddress); + mt.BaseSize = baseSize; + return mt; + } } }