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
59 changes: 59 additions & 0 deletions docs/design/datacontracts/RuntimeTypeSystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ partial interface IRuntimeTypeSystem : IContract
public virtual bool RequiresAlign8(TypeHandle typeHandle);
Comment thread
rcj1 marked this conversation as resolved.
// 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 normalized to actual byte lengths.
public virtual IEnumerable<(uint SeriesOffset, uint SeriesSize)> GetGCDescSeries(TypeHandle typeHandle, uint numComponents = 0);
public virtual bool IsDynamicStatics(TypeHandle typeHandle);
public virtual ushort GetNumInterfaces(TypeHandle typeHandle);

Expand Down Expand Up @@ -554,6 +556,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 SeriesOffset, uint SeriesSize)> 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 last 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;
Expand Down
4 changes: 4 additions & 0 deletions src/coreclr/vm/datadescriptor/datadescriptor.inc
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ CDAC_TYPE_FIELD(String, T_POINTER, m_FirstChar, cdac_data<StringObject>::m_First
CDAC_TYPE_FIELD(String, T_UINT32, m_StringLength, cdac_data<StringObject>::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<ArrayBase>::m_NumComponents)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
/// <summary>
/// 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.
/// </summary>
IEnumerable<(uint SeriesOffset, uint SeriesSize)> GetGCDescSeries(TypeHandle typeHandle, uint numComponents = 0) => throw new NotImplementedException();
bool IsDynamicStatics(TypeHandle typeHandle) => throw new NotImplementedException();
ushort GetNumInterfaces(TypeHandle typeHandle) => throw new NotImplementedException();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public enum DataType
StressMsg,
StressMsgHeader,
Object,
ContinuationObject,
NativeObjectWrapperObject,
ManagedObjectWrapperHolderObject,
ManagedObjectWrapperLayout,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,74 @@ private Data.EEClass GetClassData(TypeHandle typeHandle)
public bool IsContinuation(TypeHandle typeHandle) => typeHandle.IsMethodTable()
&& _continuationMethodTablePointer != TargetPointer.Null
&& _methodTables[typeHandle.Address].ParentMethodTable == _continuationMethodTablePointer;

IEnumerable<(uint SeriesOffset, uint SeriesSize)> 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
{
// Value-class (repeating) series.
long absNumSeries = -numSeries;
ulong startOffset = _target.ReadPointer(mtAddress - 2 * pointerSize).Value;

ulong currentOffset = startOffset;
Copy link
Copy Markdown
Member

@jkotas jkotas Apr 27, 2026

Choose a reason for hiding this comment

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

If this is trying to replicate https://github.com/microsoft/clrmd/blob/main/src/Microsoft.Diagnostics.Runtime/GCDesc.cs exactly, this would need to take the passed in object size into account too and keep repeating the sequence.

I am not sure whether the API shape that takes object size works best for all cases. It assumes that you have object size that is not always the case, like for the Continuation pretty printing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ok. I don't see an issue with taking the object size; Continuation method tables do have the total object size stored on the method table flags, accessible through the GetBaseSize API.

pMT->SetBaseSize(OBJECT_BASESIZE + startOfDataInInstance + dataSize);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could we take an optional numComponents instead of objectSize

For non-array types, baseSize==objectSize, for array types objectSize can be found from the numComponents and TypeHandle.

This removes redundant lookups at the call sites. The non-array path in AppendContinuationName does GetBaseSize only to pass it back in the
contract that already knows BaseSize.

Suggested shape:

 // Default 0 covers non-arrays cleanly: objectSize == BaseSize.
 IEnumerable<(uint SeriesOffset, uint SeriesSize)> GetGCDescSeries(
     TypeHandle typeHandle, uint numComponents = 0);

Inside the implementation:

 uint baseSize      = GetBaseSize(typeHandle);
 uint componentSize = GetComponentSize(typeHandle);
 uint objectSize    = baseSize + numComponents * componentSize;
 // ...existing logic...

This also matches native's CGCDesc::GetNumPointers(MT*, ObjectSize, NumComponents) more closely in spirit, and lets AppendContinuationName
simplify to GetGCDescSeries(typeHandle) (no baseSize local needed).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sounds good

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IEnumerable<(uint SeriesOffset, uint SeriesSize)> GetGCDescSeries(TypeHandle typeHandle, uint numComponents = 0);

This shape works well for enumerating all GC references in the given object instance. It does not work well for dumping GCInfo without an object instance, or for efficient or partial enumeration of GC references.

We can start with this shape; I am just pointing its short-comings.

while (currentOffset <= objectSize - pointerSize)
{
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<ushort>(itemAddress);
skip = _target.Read<ushort>(itemAddress + sizeof(ushort));
}
else
{
nptrs = _target.Read<uint>(itemAddress);
skip = _target.Read<uint>(itemAddress + sizeof(uint));
}

uint runBytes = nptrs * (uint)pointerSize;
yield return ((uint)currentOffset, runBytes);
currentOffset += runBytes + 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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -480,6 +488,52 @@ private void AddAssemblySpec(string? assemblySpec)
}
}

/// <summary>
/// Builds the synthetic name for a dynamically-created continuation method table, mirroring the
/// native <c>AsyncContinuationsManager::PrintContinuationName</c> in <c>asynccontinuations.h</c>.
/// </summary>
/// <remarks>
/// The name has the form:
/// <c>Continuation_&lt;dataSize&gt;[_&lt;gcOffset&gt;_&lt;gcCount&gt;]*</c>
/// where:
/// <list type="bullet">
/// <item><description>
/// <c>dataSize</c> is the number of bytes of data payload (base size minus the fixed
/// object-header and continuation-header overhead).
/// </description></item>
/// <item><description>
/// Each <c>_gcOffset_gcCount</c> pair describes one GC-pointer run: <c>gcOffset</c> is the
/// offset in bytes from the start of the data payload to the run, and <c>gcCount</c> is the
/// number of pointer-sized GC references in that run.
/// </description></item>
/// </list>
/// Only GC descriptor series whose <c>startoffset</c> is at or above the continuation data
/// payload (i.e., after the fixed <c>CORINFO_Continuation</c> header fields) are included.
/// </remarks>
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);
Expand Down
Loading
Loading