Skip to content
Open
73 changes: 73 additions & 0 deletions docs/design/datacontracts/Signature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Contract Signature

This contract describes the format of method, field, and local-variable signatures stored in target memory. Signatures use the ECMA-335 §II.23.2 format with two CoreCLR-internal element types added by the runtime.
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.

Suggested change
This contract describes the format of method, field, and local-variable signatures stored in target memory. Signatures use the ECMA-335 §II.23.2 format with two CoreCLR-internal element types added by the runtime.
This contract describes the format of method, field, and local-variable signatures stored in target memory. Signatures use the ECMA-335 §II.23.2 format with CoreCLR-internal element types added by the runtime.

Chances are we are going to add a third one. Avoid hardcoding "two" in the text to make future proof.


## Internal element types

The runtime extends the standard ECMA-335 element type encoding with two values that may appear in signatures stored in target memory:

| Encoding | Value | Layout following the tag |
| --- | --- | --- |
| `ELEMENT_TYPE_INTERNAL` | `0x21` | a target-sized pointer to a runtime `TypeHandle` |
| `ELEMENT_TYPE_CMOD_INTERNAL` | `0x22` | one byte (`1` = required, `0` = optional), then a target-sized pointer to a runtime `TypeHandle` |

These tags are used in signatures generated internally by the runtime that are not persisted to a managed image. They are defined alongside the standard ECMA-335 element types in `src/coreclr/inc/corhdr.h`. Their literal values are part of this contract -- changing them is a breaking change.

Tag `3` in the `TypeDefOrRefOrSpec` encoding (ECMA-335 §II.23.2.8) is reserved and decoders throw `BadImageFormatException` when they encounter it.

Comment on lines +16 to +17
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.

Suggested change
Tag `3` in the `TypeDefOrRefOrSpec` encoding (ECMA-335 §II.23.2.8) is reserved and decoders throw `BadImageFormatException` when they encounter it.

This is duplicating ECMA-335 §II.23.2

## APIs of contract

```csharp
TypeHandle DecodeFieldSignature(BlobHandle blobHandle, ModuleHandle moduleHandle, TypeHandle ctx);
```

## Version 1

Data descriptors used:
| Data Descriptor Name | Field | Meaning |
| --- | --- | --- |
| _none_ | | |

Global variables used:
| Global Name | Type | Purpose |
| --- | --- | --- |
| _none_ | | |

Contracts used:
| Contract Name |
| --- |
| RuntimeTypeSystem |
| Loader |
| EcmaMetadata |

Constants:
| Constant Name | Meaning | Value |
| --- | --- | --- |
| `ELEMENT_TYPE_INTERNAL` | runtime-internal element type tag for an internal `TypeHandle` | `0x21` |
| `ELEMENT_TYPE_CMOD_INTERNAL` | runtime-internal element type tag for an internal modified type | `0x22` |

Decoding a signature follows the ECMA-335 §II.23.2 grammar. For all standard element types, decoding behaves identically to `System.Reflection.Metadata.SignatureDecoder<TType, TGenericContext>`. When the decoder encounters one of the two runtime-internal tags above, it reads the target-sized pointer (and optional `required` byte for `ELEMENT_TYPE_CMOD_INTERNAL`) from the signature blob and resolves it to a runtime `TypeHandle`.

The decoder is implemented as `RuntimeSignatureDecoder<TType, TGenericContext>` -- a clone of SRM's `SignatureDecoder<TType, TGenericContext>` with added support for the two runtime-internal element types. The clone takes an additional `Target` so internal-type pointers can be sized for the target architecture. Provider implementations implement `IRuntimeSignatureTypeProvider<TType, TGenericContext>` -- a superset of `System.Reflection.Metadata.ISignatureTypeProvider<TType, TGenericContext>` -- adding two methods for the runtime-internal element types:

```csharp
TType GetInternalType(TargetPointer typeHandlePointer);
TType GetInternalModifiedType(TargetPointer typeHandlePointer, TType unmodifiedType, bool isRequired);
```

The contract's provider resolves these pointers through `RuntimeTypeSystem.GetTypeHandle`. Standard ECMA-335 element types resolve through `RuntimeTypeSystem.GetPrimitiveType` and `RuntimeTypeSystem.GetConstructedType`. Generic type parameters (`VAR`) and generic method parameters (`MVAR`) resolve via `RuntimeTypeSystem.GetInstantiation` and `RuntimeTypeSystem.GetGenericMethodInstantiation` respectively, using a `TypeHandle` (for generic types) or `MethodDescHandle` (for generic methods) generic context. `GetTypeFromDefinition` and `GetTypeFromReference` resolve tokens via the module's `TypeDefToMethodTableMap` / `TypeRefToMethodTableMap`; cross-module references and `GetTypeFromSpecification` are not currently implemented.

```csharp
TypeHandle ISignature.DecodeFieldSignature(BlobHandle blobHandle, ModuleHandle moduleHandle, TypeHandle ctx)
{
SignatureTypeProvider<TypeHandle> provider = new(_target, moduleHandle);
MetadataReader mdReader = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle)!;
BlobReader blobReader = mdReader.GetBlobReader(blobHandle);
RuntimeSignatureDecoder<TypeHandle, TypeHandle> decoder = new(provider, _target, mdReader, ctx);
return decoder.DecodeFieldSignature(ref blobReader);
}
```

### Other consumers

`RuntimeSignatureDecoder` is shared infrastructure within the cDAC. Other contracts construct their own decoder and provider directly when they need to decode method or local signatures rather than going through this contract. For example, the [StackWalk](./StackWalk.md) contract uses `RuntimeSignatureDecoder<GcTypeKind, GcSignatureContext>` with a GC-specific provider to classify method parameters during signature-based GC reference scanning.
70 changes: 0 additions & 70 deletions docs/design/datacontracts/SignatureDecoder.md

This file was deleted.

64 changes: 63 additions & 1 deletion docs/design/datacontracts/StackWalk.md
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,69 @@ At each frame yielded by `Filter`, the walk determines whether to scan for GC re
- **PrestubMethodFrame / CallCountingHelperFrame**: Use signature-based scanning.
- Other frame types: No GC roots to report.

See [GCRefMap Format and Resolution](#gcrefmap-format-and-resolution) for the GCRefMap scanning path details.
See [GCRefMap Format and Resolution](#gcrefmap-format-and-resolution) for the GCRefMap scanning path and [Signature-Based Scanning](#signature-based-scanning) for the signature decoding path.

### Signature-Based Scanning

When a transition frame's calling convention is not described by a precomputed GCRefMap (`PrestubMethodFrame`, `CallCountingHelperFrame`, and the fallback path for `StubDispatchFrame`/`ExternalMethodFrame`), the GC reference walk classifies caller-stack arguments by decoding the callee's method signature. This corresponds to native `TransitionFrame::PromoteCallerStack` (`src/coreclr/vm/frames.cpp`).

#### GcSignatureTypeProvider

`GcSignatureTypeProvider` is an `IRuntimeSignatureTypeProvider<GcTypeKind, GcSignatureContext>` that classifies each parameter type into one of:

```csharp
internal enum GcTypeKind
{
None, // Non-GC primitive that fits in a single slot
Ref, // Object reference (TYPE_GC_REF)
Interior, // Managed pointer / byref (TYPE_GC_BYREF)
Other, // Value type that may contain GC refs, or any type larger than a slot
}
```

The provider is scoped to the method's containing module (captured at construction) so that `TypeDef` and `TypeRef` tokens can be resolved to a loaded `MethodTable` via the module's `TypeDefToMethodTable` / `TypeRefToMethodTable` lookup tables. The decoder's generic context is a `GcSignatureContext(TypeHandle classContext, MethodDescHandle methodContext)` carrying the method's class and method instantiations.

The provider classifies primitives directly (`String`/`Object` -> `Ref`, `TypedReference` -> `Other`, others -> `None`). For `TypeDef`/`TypeRef` it resolves the loaded `TypeHandle` and classifies via `RuntimeTypeSystem.GetSignatureCorElementType`, treating enums (`IsEnum`) as their underlying primitive (`None`). When the type cannot be resolved (e.g., not yet loaded), classification falls back to the signature's `rawTypeKind` (`ValueType` -> `Other`, otherwise `Ref`). Arrays are `Ref`, byrefs are `Interior`, raw pointers are `None`. Generic parameters (`!T`, `!!T`) are resolved against the `GcSignatureContext` (via `GetInstantiation` / `GetGenericMethodInstantiation`) and classified by their actual instantiation -- matching native `SigTypeContext`-driven `PeekElemTypeNormalized` behavior. `ELEMENT_TYPE_INTERNAL` resolves the `TypeHandle` via `RuntimeTypeSystem.GetSignatureCorElementType` and maps the `CorElementType` to a `GcTypeKind`.

#### PromoteCallerStack Algorithm

1. Read the `MethodDesc` pointer from the `FramedMethodFrame` and obtain a `MethodDescHandle` from `RuntimeTypeSystem`.
2. Resolve the method's `MetadataReader` via `Loader.GetModuleHandleFromModulePtr` and `EcmaMetadata.GetMetadata`. If metadata is unavailable, no caller-stack refs are reported (matches native fallback behavior).
3. Obtain the method's signature blob, matching native `MethodDesc::GetSig`:
- If `RuntimeTypeSystem.IsStoredSigMethodDesc` is true (dynamic, EEImpl, and array method descs), pin the stored signature span and pass a `BlobReader` over it to `RuntimeSignatureDecoder.DecodeMethodSignature`.
- Otherwise, look up the signature via the metadata token (`mdMethodDef`), skipping methods with a nil token (`0x06000000`).
4. Decode the signature with `RuntimeSignatureDecoder<GcTypeKind, GcSignatureContext>` and a `GcSignatureTypeProvider` constructed for the method's module. The `GcSignatureContext` passes the method's class and method instantiations so that `VAR`/`MVAR` placeholders resolve to their actual types. See [Signature contract](./Signature.md) for the decoder.
5. Skip varargs methods (the caller-stack layout is not described by the callee signature alone).
6. Compute the number of reserved register slots in the `TransitionBlock`:

| Reserved Slot | Condition |
|---|---|
| `this` pointer | `MethodSignature.Header.IsInstance` |
| Return buffer | Return type is `GcTypeKind.Other` |
| Generic instantiation arg | `RuntimeTypeSystem.RequiresInstArg(methodDesc)` |
| Async continuation | `RuntimeTypeSystem.IsAsyncMethod(methodDesc)` |
| ARM64 indirect-result register (`x8`) | Target architecture is ARM64 |

7. If `IsInstance`, report the `this` slot at position `0` (or `1` on ARM64 to skip `x8`). The slot is reported as `GC_CALL_INTERIOR` for value-type `this`, otherwise as a normal reference.
8. Walk `MethodSignature.ParameterTypes` starting at slot index = reserved slot count, advancing one slot per parameter:
- `GcTypeKind.Ref` -> report as a reference.
- `GcTypeKind.Interior` -> report with `GC_CALL_INTERIOR`.
- `GcTypeKind.Other` / `GcTypeKind.None` -> not reported (large value types are reported via the GCRefMap path when one is available; otherwise their interior refs are not visible to this scan).

The slot address is computed using the same formula as the GCRefMap path:

```csharp
slotAddress = transitionBlockPtr + FirstGCRefMapSlot + (position * pointerSize);
```

#### Limitations vs. Native

This signature-based scan is conservative compared to native:

* It does not enumerate embedded GC refs inside large value types passed by value (a `GcTypeKind.Other` parameter is silently skipped).
* It does not yet apply native's `ArgIterator`-driven multi-slot / HFA layout, nor does it model `String` constructors or `SuppressParamTypeArg`.

These limitations are visible to the cDAC GC stress verification harness, which compares cDAC and native walks; they may be tightened in future versions of this contract.
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.

Should we track these TODOs as issues?


### GCRefMap Format and Resolution

Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/inc/corhdr.h
Original file line number Diff line number Diff line change
Expand Up @@ -913,7 +913,7 @@ typedef enum CorElementType
ELEMENT_TYPE_CMOD_OPT = 0x20, // optional C modifier : E_T_CMOD_OPT <mdTypeRef/mdTypeDef>

// This is for signatures generated internally (which will not be persisted in any way).
// [cDAC] [RuntimeTypeSystem]: Contract depends on the values of ELEMENT_TYPE_INTERNAL and ELEMENT_TYPE_CMOD_INTERNAL.
// [cDAC] [Signature][RuntimeTypeSystem]: Contract depends on the values of ELEMENT_TYPE_INTERNAL and ELEMENT_TYPE_CMOD_INTERNAL.
ELEMENT_TYPE_INTERNAL = 0x21, // INTERNAL <typehandle>
ELEMENT_TYPE_CMOD_INTERNAL = 0x22, // CMOD_INTERNAL <required (1 byte: non-zero if required, 0 if optional)> <typehandle>

Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/vm/datadescriptor/datadescriptor.inc
Original file line number Diff line number Diff line change
Expand Up @@ -1574,7 +1574,7 @@ CDAC_GLOBAL_CONTRACT(ReJIT, c1)
CDAC_GLOBAL_CONTRACT(RuntimeInfo, c1)
CDAC_GLOBAL_CONTRACT(RuntimeTypeSystem, c1)
CDAC_GLOBAL_CONTRACT(SHash, c1)
CDAC_GLOBAL_CONTRACT(SignatureDecoder, c1)
CDAC_GLOBAL_CONTRACT(Signature, c1)
CDAC_GLOBAL_CONTRACT(StackWalk, c1)
CDAC_GLOBAL_CONTRACT(StressLog, c2)
CDAC_GLOBAL_CONTRACT(SyncBlock, c1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ public abstract class ContractRegistry
/// </summary>
public virtual ICodeNotifications CodeNotifications => GetContract<ICodeNotifications>();
/// <summary>
/// Gets an instance of the SignatureDecoder contract for the target.
/// Gets an instance of the Signature contract for the target.
/// </summary>
public virtual ISignatureDecoder SignatureDecoder => GetContract<ISignatureDecoder>();
public virtual ISignature Signature => GetContract<ISignature>();
/// <summary>
/// Gets an instance of the SyncBlock contract for the target.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@

namespace Microsoft.Diagnostics.DataContractReader.Contracts;

public interface ISignatureDecoder : IContract
public interface ISignature : IContract
{
static string IContract.Name { get; } = nameof(SignatureDecoder);
static string IContract.Name { get; } = nameof(Signature);
TypeHandle DecodeFieldSignature(BlobHandle blobHandle, ModuleHandle moduleHandle, TypeHandle ctx) => throw new NotImplementedException();
}

public readonly struct SignatureDecoder : ISignatureDecoder
public readonly struct Signature : ISignature
{
// Everything throws NotImplementedException
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Reflection.Metadata;

namespace Microsoft.Diagnostics.DataContractReader.SignatureHelpers;

/// <summary>
/// Superset of SRM's <see cref="ISignatureTypeProvider{TType, TGenericContext}"/>
/// that adds support for runtime-internal type codes
/// (<c>ELEMENT_TYPE_INTERNAL</c> 0x21 and <c>ELEMENT_TYPE_CMOD_INTERNAL</c> 0x22).
/// </summary>
/// <remarks>
/// Providers implementing this interface automatically satisfy SRM's
/// <see cref="ISignatureTypeProvider{TType, TGenericContext}"/> and can be used
/// with both SRM's <c>SignatureDecoder</c> and our
/// <see cref="RuntimeSignatureDecoder{TType, TGenericContext}"/>.
/// </remarks>
public interface IRuntimeSignatureTypeProvider<TType, TGenericContext>
: ISignatureTypeProvider<TType, TGenericContext>
{
/// <summary>
/// Classify an <c>ELEMENT_TYPE_INTERNAL</c> (0x21) type by resolving the
/// embedded TypeHandle pointer via the target's runtime type system.
/// </summary>
TType GetInternalType(TargetPointer typeHandlePointer);

/// <summary>
/// Classify an <c>ELEMENT_TYPE_CMOD_INTERNAL</c> (0x22) custom modifier by
/// resolving the embedded TypeHandle pointer via the target's runtime type system.
/// </summary>
TType GetInternalModifiedType(TargetPointer typeHandlePointer, TType unmodifiedType, bool isRequired);
}
Loading
Loading