Skip to content

Reduce raw global / typeinfo usage in cDAC Legacy interfaces#129260

Open
max-charlamb wants to merge 2 commits into
mainfrom
removeGlobalUsageInLegacyInterfaces
Open

Reduce raw global / typeinfo usage in cDAC Legacy interfaces#129260
max-charlamb wants to merge 2 commits into
mainfrom
removeGlobalUsageInLegacyInterfaces

Conversation

@max-charlamb

@max-charlamb max-charlamb commented Jun 10, 2026

Copy link
Copy Markdown
Member

Note

This PR description was AI-generated by GitHub Copilot.

Refactors the Microsoft.Diagnostics.DataContractReader.Legacy project to stop reading runtime globals and typeinfo by string key, routing them through typed contract accessors instead. Behavior-preserving: each refactor moves an existing read down one layer (from Legacy into the corresponding Contracts/*_1 impl) without changing what bytes get read.

Changes

1. ILoader.GetLookupTables owns the ModuleLookupMap.TableData offset

Adds a TableDataOffset field to the existing ModuleLookupTables record struct, populated by Loader_1.GetLookupTables. SOSDacImpl.GetModuleData no longer reads raw typeinfo via _target.GetTypeInfo(DataType.ModuleLookupMap) and instead consumes the offset from the struct, going through a small ReadMapBase helper that null-guards the table pointer.

This removes the only direct GetTypeInfo call in the Legacy project.

2. ILoader.GetAppDomain for the global AppDomain pointer

Adds a simple GetAppDomain() accessor on ILoader that reads and dereferences the AppDomain global pointer. All Legacy call sites that previously did ReadGlobalPointer + ReadPointer inline now call this single method.

RuntimeDomainInfo struct and GetDomainInfo() were removed per review feedback. DefaultADID was removed from the data contract -- Legacy call sites use a local const uint DefaultAppDomainId = 1 instead.

3. IRuntimeTypeSystem.GetWellKnownMethodTable for runtime singleton MTs

Introduces a WellKnownMethodTable enum (Object, String, Array, Exception, Free, Canon) and a GetWellKnownMethodTable(kind) accessor on IRuntimeTypeSystem. RuntimeTypeSystem_1 implements it with a safe TryReadGlobalPointer + TryReadPointer pair so callers (e.g. GetUsefulGlobals during early load notifications) get TargetPointer.Null when the runtime has not yet initialized a given global, matching the prior lazy-read behavior.

Refactors the Legacy project to use the new accessor:

  • SOSDacImpl: removes the _stringMethodTable / _objectMethodTable lazy fields; GetObjectData routes through the contract; GetUsefulGlobals reads all 5 well-known MTs via the contract.
  • DacDbiImpl: IsExceptionObject (Exception MT) and EnumerateTypeParameters (Canon MT).
  • Dbi/Helpers/HeapWalk: ctor (Free MT).

4. DAC: Remove SystemDomain special-casing

GetAppDomainStoreData now reports systemDomain = 0. The SystemDomain guard in GetAppDomainData, GetAssemblyList, and GetAppDomainName has been removed -- these methods now always treat the address as an AppDomain.

Important

Breaking behavior change: DacpAppDomainStoreData::systemDomain will now be NULL. SOS consumers that assume systemDomain is always valid need to be updated. The companion diagnostics PR handles this: dotnet/diagnostics#5878

Testing

All cDAC unit tests pass (2493 passed, 16 skipped). Test updates:

  • ClrDataTaskTests.GetCurrentAppDomain registers the ILoader contract.
  • DacDbiImplTests.IsExceptionObject now mocks IRuntimeTypeSystem.GetWellKnownMethodTable instead of setting up the raw ExceptionMethodTable global.

Out of scope

Several additional Legacy raw-global sites (e.g. DacNotificationFlags, StressLog, FeatureCOMInterop, SOSBreakingChangeVersion, TlsIndexBase/OffsetOfCurrentThreadInfo) remain and can be addressed in follow-up PRs where the contract-side surface is justified by callers beyond a single SOS API.

@github-actions

This comment has been minimized.

Copilot AI left a comment

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.

Pull request overview

Refactors the cDAC Legacy implementation to reduce direct raw reads of runtime globals / typeinfo by routing those reads through typed contract APIs in the Abstractions/Contracts layer.

Changes:

  • Adds new contract accessors/types to centralize reading of domain globals (ILoader.GetDomainInfo) and well-known singleton MethodTables (IRuntimeTypeSystem.GetWellKnownMethodTable).
  • Moves ModuleLookupMap.TableData offset calculation into ILoader.GetLookupTables via a new TableDataOffset field on ModuleLookupTables.
  • Updates Legacy (SOS/IXCLRData*/DBI/heap walk) call sites and unit tests to consume the new contract-based APIs.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/native/managed/cdac/tests/UnitTests/DacDbiImplTests.cs Updates tests to mock GetWellKnownMethodTable instead of setting up raw globals.
src/native/managed/cdac/tests/UnitTests/ClrDataTaskTests.cs Updates test target setup to provide globals required by ILoader.GetDomainInfo and registers the Loader contract.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs Replaces raw AppDomain global reads with Loader.GetDomainInfo().DefaultAppDomain.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs Removes remaining Legacy GetTypeInfo(ModuleLookupMap) usage and routes domain/MT reads via contracts.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/HeapWalk.cs Uses GetWellKnownMethodTable(Free) instead of reading FreeObjectMethodTable global directly.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs Routes domain id / appdomain / exception+canon MT reads through the new contract accessors.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataTask.cs Uses Loader.GetDomainInfo().DefaultAppDomain for GetCurrentAppDomain.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataFrame.cs Uses Loader.GetDomainInfo().DefaultAppDomain for frame AppDomain/method-instance construction.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataAppDomain.cs Uses Loader.GetDomainInfo().DefaultAppDomainId for AppDomain unique id.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs Implements GetWellKnownMethodTable with safe TryRead* global dereferencing.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs Adds GetDomainInfo and populates ModuleLookupTables.TableDataOffset.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs Introduces WellKnownMethodTable enum + GetWellKnownMethodTable API on the abstraction.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ILoader.cs Extends ModuleLookupTables with TableDataOffset and adds RuntimeDomainInfo + GetDomainInfo.

Comment thread src/native/managed/cdac/tests/UnitTests/DacDbiImplTests.cs
@github-actions

This comment has been minimized.

Copilot AI review requested due to automatic review settings June 10, 2026 23:26
@github-actions

This comment has been minimized.

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.

Comment on lines 3339 to +3343
Target.TypeInfo arrayTypeInfo = _target.GetTypeInfo(DataType.Array);
ulong numComponentsOffset = (ulong)_target.GetTypeInfo(DataType.Array).Fields[Constants.FieldNames.Array.NumComponents].Offset;
data->Size += _target.Read<uint>(objAddr + numComponentsOffset) * data->dwComponentSize;
}
else if (mt == _stringMethodTable.Value)
else if (mt == runtimeTypeSystemContract.GetWellKnownMethodTable(Contracts.WellKnownMethodTable.String))
Comment on lines 100 to 147
@@ -131,6 +141,9 @@ public interface IRuntimeTypeSystem : IContract
// True if the MethodTable is the System.Object MethodTable (g_pObjectClass)
bool IsObject(TypeHandle typeHandle) => throw new NotImplementedException();
bool IsString(TypeHandle typeHandle) => throw new NotImplementedException();
// Returns the address of one of the runtime's well-known singleton MethodTables,
// or TargetPointer.Null if the runtime has not yet initialized that global.
TargetPointer GetWellKnownMethodTable(WellKnownMethodTable kind) => throw new NotImplementedException();
bool IsObjRef(TypeHandle typeHandle) => throw new NotImplementedException();
Comment on lines 75 to 109
public record struct ModuleLookupTables(
TargetPointer FieldDefToDesc,
TargetPointer ManifestModuleReferences,
TargetPointer MemberRefToDesc,
TargetPointer MethodDefToDesc,
TargetPointer TypeDefToMethodTable,
TargetPointer TypeRefToMethodTable,
TargetPointer MethodDefToILCodeVersioningState);
TargetPointer MethodDefToILCodeVersioningState,
uint TableDataOffset);

public readonly struct RuntimeDomainInfo
{
public TargetPointer SystemDomain { get; init; }
public TargetPointer DefaultAppDomain { get; init; }
public uint DefaultAppDomainId { get; init; }
}

public readonly struct LoaderHeapBlockData
{
public TargetPointer Address { get; init; }
public TargetNUInt Size { get; init; }
public TargetPointer NextBlock { get; init; }
}

public interface ILoader : IContract
{
static string IContract.Name => nameof(Loader);

ModuleHandle GetModuleHandleFromModulePtr(TargetPointer modulePointer) => throw new NotImplementedException();
ModuleHandle GetModuleHandleFromAssemblyPtr(TargetPointer assemblyPointer) => throw new NotImplementedException();
IEnumerable<ModuleHandle> GetModuleHandles(TargetPointer appDomain, AssemblyIterationFlags iterationFlags) => throw new NotImplementedException();
TargetPointer GetRootAssembly() => throw new NotImplementedException();
string GetAppDomainFriendlyName() => throw new NotImplementedException();
RuntimeDomainInfo GetDomainInfo() => throw new NotImplementedException();
TargetPointer GetModule(ModuleHandle handle) => throw new NotImplementedException();
Comment thread docs/design/datacontracts/Loader.md Outdated
| --- | --- | --- |
| `AppDomain` | TargetPointer | Pointer to the global AppDomain |
| `SystemDomain` | TargetPointer | Pointer to the global SystemDomain |
| `DefaultADID` | uint | Id of the default AppDomain |

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
| `DefaultADID` | uint | Id of the default AppDomain |
| `DefaultADID` | uint | Id of the global AppDomain |

This is the same thing as what AppDomain points to, so we should call it the same.

(I am not sure whether we need ADID in the data contract. It exists just to pass a dummy value around a few APIs. Having a local constant for that as needed would be fine.)

Comment thread docs/design/datacontracts/Loader.md Outdated
// Aggregate view of the runtime's domain singletons.
readonly struct RuntimeDomainInfo
{
TargetPointer SystemDomain { get; init; }

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.

SystemDomain is a grab bag of some statics. It does not make sense as a concept. It may be better to convert the few fields that we need to read from it to regular statics and read them directly.

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.

If you incorporate this and my other feedback, RuntimeDomainInfo will be unnecessary since there is going to be just the DefaultAppDomain left.

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.

Heads up - As-is reporting SystemDomain = NULL to SOS is going to give errors in some cases but I think it will be easy to make an SOS change to fix them.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Made changes in diagnostics to handle a NULL systemdomain gracefully. dotnet/diagnostics#5878

Max Charlamb and others added 2 commits June 12, 2026 13:27
- Add ILoader.GetDomainInfo and route domain global reads through it
- Move ModuleLookupMap TableData offset into ILoader.GetLookupTables
- Add IRuntimeTypeSystem.GetWellKnownMethodTable for runtime singleton MTs
- Document RuntimeDomainInfo, WellKnownMethodTable, and TableDataOffset
- Address PR feedback on GetDomainInfo and naming
Address Jan's review feedback:
- Remove RuntimeDomainInfo struct and GetDomainInfo(), replace with
  simple ILoader.GetAppDomain() returning TargetPointer
- Remove DefaultADID from data contract globals, use local constant
  (value 1) in Legacy call sites
- Remove SystemDomain special-casing from DAC request.cpp
  (GetAppDomainStoreData, GetAppDomainData, GetAssemblyList,
  GetAppDomainName)
- Report systemDomain as NULL from GetAppDomainStoreData
- Update Loader.md documentation to match

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 12, 2026 17:39
@max-charlamb max-charlamb force-pushed the removeGlobalUsageInLegacyInterfaces branch from 4b91585 to f8464a3 Compare June 12, 2026 17:39
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #129260

Note

This review was generated by GitHub Copilot.

Holistic Assessment

Motivation: Well-justified layering improvement. The Legacy project had 22+ raw ReadGlobalPointer/ReadGlobal/GetTypeInfo call sites for domain singletons and well-known MethodTables. Centralizing these in Loader_1 and RuntimeTypeSystem_1 is the right architectural direction—reducing duplication, clarifying the contract boundary, and consolidating error handling.

Approach: Clean and mechanical. Each refactored read moves exactly one layer down into the contract implementation without changing semantics. The latest revision correctly incorporates maintainer feedback (jkotas) by simplifying GetDomainInfo down to GetAppDomain() and hardcoding DefaultAppDomainId = 1 (matching the native const DWORD DefaultADID = 1 in appdomain.hpp).

Summary: ⚠️ Needs Human Review. The managed cDAC refactoring is clean and behavior-preserving. However, the native DAC changes in request.cpp are behavioral (not just refactoring)—returning NULL for systemDomain and removing SystemDomain special-casing. This has a companion fix in dotnet/diagnostics#5878 that must land in sync. A human reviewer should confirm the diagnostics-side coordination is tracked and that no other consumers of GetAppDomainStoreData.systemDomain could be affected.


Detailed Findings

✅ Correctness — Managed refactoring is behavior-preserving

All managed call sites maintain identical semantics:

  • GetAppDomain() returns the same pointer value previously obtained by raw ReadGlobalPointer + ReadPointer pairs.
  • GetWellKnownMethodTable() uses TryReadGlobalPointer/TryReadPointer returning TargetPointer.Null on failure, correctly matching the prior lazy-read semantics for early-init scenarios.
  • ReadMapBase helper (SOSDacImpl.cs:3145) adds a null-guard that the old code lacked—a correctness improvement preventing reads from TargetPointer.Null + offset.
  • GetObjectData switches from Lazy<TargetPointer> cached reads to per-call GetWellKnownMethodTable. Since valid objects always have non-null MTs, the mt == TargetPointer.Null comparison is always false when globals are uninitialized, so no false-positive type matching can occur.

⚠️ Native DAC behavioral changes — Requires coordination

The request.cpp changes are not purely mechanical:

  1. GetAppDomainStoreData now returns systemDomain = NULL (was SystemDomain::System())
  2. GetAppDomainData removes the if (addr != SystemDomain::System()) guard
  3. GetAssemblyList removes the SystemDomain rejection (E_INVALIDARG)
  4. GetAppDomainName removes the SystemDomain special-case

Per the review thread, this is addressed by dotnet/diagnostics#5878. A human reviewer should confirm the coordination—if SOS or other consumers of systemDomain are used before the diagnostics fix is released, they could encounter errors.

✅ API Design — Well-scoped contract additions (no API review needed)

Per the cDAC instructions for .NET 11 dev branches (SDK 11.0.100-preview.5), new APIs on IContract implementations do NOT require API review. The additions are appropriate:

  • WellKnownMethodTable enum covers exactly the 6 singleton MTs referenced across the codebase.
  • ILoader.GetAppDomain() provides a single centralized read for the 12+ call sites that need the default AppDomain pointer.
  • ModuleLookupTables.TableDataOffset keeps the typeinfo read co-located with module data in Loader_1.GetLookupTables, removing the last direct GetTypeInfo call from Legacy.

✅ Test Updates — Complete and well-adapted

  • ClrDataTaskTests.GetCurrentAppDomain now registers the AppDomain global and the ILoader contract.
  • DacDbiImplTests.IsExceptionObject cleanly mocks GetWellKnownMethodTable instead of constructing raw memory fragments—simpler and more intention-revealing.
  • The exceptionMT parameter was correctly removed from CreateDacDbiWithExceptionMT.

💡 Pre-existing: unused local in GetObjectData (SOSDacImpl.cs:3329)

Target.TypeInfo arrayTypeInfo = _target.GetTypeInfo(DataType.Array);
ulong numComponentsOffset = (ulong)_target.GetTypeInfo(DataType.Array).Fields[...].Offset;

arrayTypeInfo is assigned but unused—the next line redundantly calls GetTypeInfo again. This is pre-existing (not introduced by this PR) and was already flagged in prior review comments. Non-blocking, but a trivial fix.

💡 Minor — Missing blank line (RuntimeTypeSystem_1.cs:606-607)

The closing } of GetWellKnownMethodTable is immediately followed by public bool IsObjRef(...) with no separating blank line, unlike surrounding methods. Cosmetic only.

Generated by Code Review for issue #129260 · ● 7.4M ·

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.

Comment on lines 79 to +83
TargetPointer MethodDefToDesc,
TargetPointer TypeDefToMethodTable,
TargetPointer TypeRefToMethodTable,
TargetPointer MethodDefToILCodeVersioningState);
TargetPointer MethodDefToILCodeVersioningState,
uint TableDataOffset);
Comment on lines 2554 to 2558
ClrDataAccess::GetAssemblyList(CLRDATA_ADDRESS addr, int count, CLRDATA_ADDRESS values[], int *pNeeded)
{
if (addr == (CLRDATA_ADDRESS)NULL)
return E_INVALIDARG;

Comment on lines 3329 to 3330
Target.TypeInfo arrayTypeInfo = _target.GetTypeInfo(DataType.Array);
ulong numComponentsOffset = (ulong)_target.GetTypeInfo(DataType.Array).Fields[Constants.FieldNames.Array.NumComponents].Offset;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants