[cDAC] Add IMetaDataImport COM wrapper over MetadataReader for no-fallback mode#127028
[cDAC] Add IMetaDataImport COM wrapper over MetadataReader for no-fallback mode#127028max-charlamb merged 36 commits intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a managed COM-compatible IMetaDataImport/IMetaDataImport2 implementation backed by System.Reflection.Metadata.MetadataReader, and wires it into cDAC’s ClrDataModule so diagnostic tools can query metadata when running in no-fallback mode (legacy DAC unavailable).
Changes:
- Introduces generated COM interface definitions for
IMetaDataImportandIMetaDataImport2. - Implements
MetadataImportWrapperto adaptMetadataReaderto the COM metadata import APIs (with a subset implemented and the rest stubbed). - Updates
ClrDataModuleto provideIMetaDataImportin no-fallback mode and adds unit tests validating implemented behaviors.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
src/native/managed/cdac/tests/MetadataImportWrapperTests.cs |
Adds unit tests that exercise the wrapper’s enumeration and property APIs over synthetic metadata. |
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/MetadataImportWrapper.cs |
Implements the COM wrapper over MetadataReader including enum-handle handling and a set of metadata accessors. |
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/IMetaDataImport.cs |
Adds managed [GeneratedComInterface] declarations for IMetaDataImport/IMetaDataImport2 with vtable ordering. |
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataModule.cs |
Hooks IMetaDataImport QI to return the managed wrapper when legacy DAC is unavailable. |
This comment has been minimized.
This comment has been minimized.
The ICustomQueryInterface.GetInterface method in ClrDataModule delegates IMetaDataImport QIs to the legacy module pointer. Gate this with CanFallback() so no-fallback mode blocks it, allowing PR #127028's managed MetadataReader wrapper to provide metadata instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add ICustomQueryInterface.GetInterface to the allowlist (needed until managed MetadataReader wrapper lands in PR #127028) - Replace file-based logging with Console.Error.WriteLine so blocked fallback calls appear directly in test output (captured by ProcessRunner) - Simplify tracking to ConcurrentDictionary<string, bool> with TryAdd - Remove Flush() CanFallback gate (cache management, not data retrieval) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
You'll likely need to add support for IMetaDataAssemblyImport as well for assembly-level info |
The ICustomQueryInterface.GetInterface method in ClrDataModule delegates IMetaDataImport QIs to the legacy module pointer. Gate this with CanFallback() so no-fallback mode blocks it, allowing PR #127028's managed MetadataReader wrapper to provide metadata instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add ICustomQueryInterface.GetInterface to the allowlist (needed until managed MetadataReader wrapper lands in PR #127028) - Replace file-based logging with Console.Error.WriteLine so blocked fallback calls appear directly in test output (captured by ProcessRunner) - Simplify tracking to ConcurrentDictionary<string, bool> with TryAdd - Remove Flush() CanFallback gate (cache management, not data retrieval) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
> [!NOTE] > This PR description was generated with the assistance of GitHub Copilot. ## Summary Add a granular, per-method allowlist (`LegacyFallbackHelper`) that controls which delegation-only APIs may fall back to the legacy DAC when `CDAC_NO_FALLBACK=1` is set. This enables selective no-fallback testing — blocking fallback for most APIs while allowing specific APIs that are known to not yet be implemented in the cDAC. Wire this into the runtime-diagnostics CI pipeline using the `-noFallback` flag from [dotnet/diagnostics#5806](dotnet/diagnostics#5806). All fallback attempts (both allowed and blocked) are logged to stderr with method name, file, and line number for capture by the diagnostics test infrastructure. ## Changes ### LegacyFallbackHelper.cs — Granular fallback control New static helper that every delegation-only call site invokes via `CanFallback()`. Uses `[CallerMemberName]`, `[CallerFilePath]`, and `[CallerLineNumber]` to identify the call site. - **Normal mode** (`CDAC_NO_FALLBACK` unset): Always returns `true` (single `bool` check, `[AggressiveInlining]`) - **No-fallback mode** (`CDAC_NO_FALLBACK=1`): Checks method name against a `HashSet<string>` allowlist and file name against a file-level allowlist **Per-method allowlist:** | Method | Reason | |--------|--------| | `EnumMemoryRegions` | Dump creation — cDAC has no memory enumeration implementation | | `GetInterface` | IMetaDataImport QI ([PR #127028](#127028)) | | `GetMethodDefinitionByToken` | IXCLRDataModule — not yet implemented in cDAC | | `IsTrackedType` | GC heap analysis ([PR #125895](#125895)) | | `TraverseLoaderHeap` | Loader heap traversal ([PR #125129](#125129)) | **File-level allowlist:** | File | Reason | |------|--------| | `DacDbiImpl.cs` | Entire DBI/ICorDebug interface (122 methods) — deferred | ### Entrypoints.cs — Simplified creation Both `CreateSosInterface` and `CreateDacDbiInterface` now follow the same pattern: the legacy implementation is always passed through, and `LegacyFallbackHelper.CanFallback()` at each call site decides whether to delegate. Removed `prevent_release`, `noFallback` env var check, and null-legacy-ref logic. ### 13 Legacy wrapper files — Instrumented delegation sites All 296 delegation-only methods across all legacy wrapper files now call `LegacyFallbackHelper.CanFallback()`: - `SOSDacImpl.cs` (12 methods) - `SOSDacImpl.IXCLRDataProcess.cs` (38 methods, `Flush()` intentionally excluded — cache management) - `ClrDataModule.cs` (29 methods + IMetaDataImport QI) - `DacDbiImpl.cs` (122 methods) - Other wrappers: `ClrDataTask.cs`, `ClrDataExceptionState.cs`, `ClrDataFrame.cs`, `ClrDataValue.cs`, `ClrDataTypeInstance.cs`, `ClrDataMethodInstance.cs`, `ClrDataStackWalk.cs`, `ClrDataProcess.cs` ### CI Pipeline — `-noFallback` flag Updated `runtime-diag-job.yml` to accept a `noFallback` parameter that passes `-noFallback` to the diagnostics build script. The `cDAC_no_fallback` leg in `runtime-diagnostics.yml` now uses `noFallback: true` instead of setting `CDAC_NO_FALLBACK` as a pipeline-level environment variable. The `-noFallback` flag (from [dotnet/diagnostics#5806](dotnet/diagnostics#5806)) properly: - Sets `DOTNET_ENABLE_CDAC=1` and `CDAC_NO_FALLBACK=1` on the debugger process - Defines `CDAC_NO_FALLBACK_TESTING` to skip `ClrStack -i` tests (ICorDebug not implemented in cDAC) ### Stderr logging Every fallback attempt is logged to stderr in the format: ``` [cDAC] Allowed fallback: CreateStackWalk at DacDbiImpl.cs:590 [cDAC] Blocked fallback: SomeMethod at SOSDacImpl.cs:123 ``` The diagnostics test infrastructure (`ProcessRunner`) captures stderr and routes it to xunit test output with `STDERROR:` prefix, making fallback usage visible in test results. ## Test Results With `CDAC_NO_FALLBACK=1` and the current allowlist, running the full SOS test suite against a private runtime build: - **24 passed**, **2 failed** (flaky/pre-existing), **2 skipped** (Linux-only) - **0 blocked fallbacks** ## Motivation The existing cDAC test leg always has the legacy DAC as a fallback, so unimplemented APIs are silently handled. The granular no-fallback mode makes gaps visible per-method, helping track progress toward full cDAC coverage while keeping tests green for known-deferred APIs. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
8f31425 to
a67d391
Compare
This comment has been minimized.
This comment has been minimized.
bbbd537 to
f5b1347
Compare
Native IMetaDataImport returns CLDB_S_TRUNCATION (0x00131106) when a string buffer is smaller than the required size. Match this behavior by: - Change CopyStringToBuffer return type from void to bool (true = truncated) - Add CLDB_S_TRUNCATION constant to MetaDataImportImpl - Update all 9 CopyStringToBuffer callers to set hr appropriately - Update GetAssemblyProps/GetAssemblyRefProps manual string copy with truncation detection for both name and locale strings - Update test to expect CLDB_S_TRUNCATION on small buffer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Return ELEMENT_TYPE_VOID (1) instead of 0 for pdwCPlusTypeFlag when no constant exists in GetFieldProps and GetParamProps - Divide pchValue by sizeof(WCHAR) for ELEMENT_TYPE_STRING constants in GetFieldProps and GetParamProps - Map global <Module> parent (TypeDef RID 1) to mdTypeDefNil (0) in GetMethodProps, GetFieldProps, and GetMemberRefProps - Fix GetUserString pchString to return character count without null terminator (matching native userString.GetSize() / sizeof(WCHAR)) - OR afPublicKey (0x0001) into assembly flags when public key blob is non-empty in GetAssemblyProps Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- GetFieldProps/GetParamProps: verify ELEMENT_TYPE_VOID (1) when no constant - GetFieldProps: verify string constant returns char count, not byte count - GetMethodProps: verify global method on <Module> returns mdTypeDefNil - GetFieldProps: verify global field on <Module> returns mdTypeDefNil - GetMethodProps: verify non-global method returns correct parent class - GetUserString: verify char count without null terminator - GetAssemblyProps: verify afPublicKey flag when public key blob is non-empty - GetParamProps: verify ELEMENT_TYPE_VOID when no constant - Enhanced test metadata with global method, string constant, and public key Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- GetFieldProps: verify ELEMENT_TYPE_VOID when field has no constant - GetMethodProps: verify non-global method returns correct parent TypeDef - GetMethodProps: verify global method on <Module> returns mdTypeDefNil - GetUserString: verify char count without null terminator - Add InternalsVisibleTo for DumpTests in Legacy csproj - Remove duplicate HResults.cs compile from DumpTests csproj Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Rewrite GetUserString to use raw #US heap bytes via BlobReader instead of MetadataReader.GetUserString() to match native semantics exactly (blob size validation, TruncateBySize(1), CLDB_E_FILE_CORRUPT check) - Fix CLDB_S_TRUNCATION: null-terminate only on truncation (last char), matching native copy semantics - Fix ClrMD AccessViolationException in EnumGenericParams: ClrMD QIs for IMetaDataImport but accesses IMetaDataImport2 vtable slots beyond the IMetaDataImport boundary. With native C++ COM objects the vtable is unified, but [GeneratedComInterface] CCWs have separate per-interface vtables. Return IMetaDataImport2 vtable when asked for IMetaDataImport. - Handle direct QI for IMetaDataImport2 and IMetaDataAssemblyImport in ClrDataModule.GetInterface Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ClrMD's CallableCOMWrapper performs a double QueryInterface: first QI on ClrDataModule for IMetaDataImport, then a second QI on the returned pointer for IMetaDataImport again. With [GeneratedComInterface] CCWs, each COM interface gets its own vtable. The second QI was returning the shorter IMetaDataImport vtable (65 slots), but ClrMD accesses slot 65 (EnumGenericParams from IMetaDataImport2), causing an AccessViolation. Fix: implement ICustomQueryInterface on MetaDataImportImpl to redirect IMetaDataImport QIs to IMetaDataImport2, providing the full 73-slot vtable. Store the StrategyBasedComWrappers instance on MetaDataImportImpl during CCW creation in ClrDataModule.GetInterface. Add regression test that reproduces the double-QI scenario and verifies EnumGenericParams is callable through the redirected vtable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add HasReader property with [MemberNotNullWhen(true, nameof(_reader))] so the compiler tracks _reader as non-null after the guard check. Replace all 'if (_reader is null)' guards with 'if (!HasReader)' and remove 57 uses of the null-forgiving operator (_reader!). Private helpers (GetTypeDefFullName, GetTypeRefFullName, BuildInterfaceImplLookup, GetCustomAttributeTypeName) use Debug.Assert(HasReader) to satisfy flow analysis since they are only called from contexts where _reader is known non-null. Also fix MetaDataImportDumpTests to assign Assert.NotNull result for proper nullable flow analysis. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Changed _reader from MetadataReader? to MetadataReader (non-nullable) - Removed HasReader property and MemberNotNullWhen attribute - Removed all 26 if (!HasReader) fallback blocks from implemented methods - ClrDataModule now returns NotHandled if reader is null (fail fast) - Removed NullReader_* tests since null reader is no longer valid Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The dump tests use xUnit v2 where Assert.NotNull returns void, unlike the unit tests which use xUnit v3 where it returns T. Split the assert and assignment into separate statements. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…am lookup - Fix CountEnum/ResetEnum to work with cDAC-created enum handles by tracking GCHandle values in a HashSet. Previously these delegated to legacy which couldn't interpret cDAC GCHandle-based enums. - Fix CloseEnum to distinguish cDAC vs legacy enum handles, preventing crashes when mixing handle types. - Implement GetClassLayout field offset population. Previously always returned pcFieldOffset=0; now fills COR_FIELD_OFFSET array with field tokens and their explicit layout offsets. - Optimize GetParamProps parent method lookup from O(N^2) triple-nested loop to O(1) cached dictionary lookup, matching the existing pattern used by BuildInterfaceImplLookup. - Add tests for CountEnum, ResetEnum, and null handle edge cases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mbly import methods - Replace HashSet<nint> with ConcurrentDictionary<nint, byte> for _cdacEnumHandles to support concurrent COM method calls from multiple threads. - Refactor GetAssemblyProps and GetAssemblyRefProps to use OutputBufferHelpers.CopyStringToBuffer instead of manual string copy, reducing code duplication and ensuring consistent buffer handling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add ContractsDictionaryConverter that handles both string and integer values in the contracts dictionary. Integer values from older runtimes are mapped to the 'c<N>' naming convention (e.g., 1 -> 'c1') to match the current string-based contract version registrations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…criptor" This reverts commit 6117d00.
- Only accept IID_IMetaDataImport in ClrDataModule QI (not IMetaDataImport2/AssemblyImport) - Replace StrategyBasedComWrappers with ComInterfaceMarshaller pattern - Fix unused userStringHandle variable in tests - Fix StringConst field to use string field signature instead of int Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- ConvertToUnmanaged already returns an AddRef'd interface pointer, so the follow-up Marshal.QueryInterface was unnecessary in both ClrDataModule and MetaDataImportImpl ICustomQueryInterface. - Remove null-forgiving operator on ConvertToManaged since legacyImport is already nullable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Move field declarations above constructor in ClrDataModule - Remove stale ComWrappers and ICustomQueryInterface comments - Move CLDB_* HResults to shared CorDbgHResults.cs - Use CorElementType enum instead of raw uint constants - Move validation helpers to bottom of MetaDataImportImpl - Make GetEnum validate ownership and return non-nullable - Convert all public methods to explicit interface notation - Simplify CountEnum/ResetEnum using validated GetEnum - Update tests for explicit interface notation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The explicit interface conversion missed MetaDataImportDumpTests.cs, which was calling methods directly on MetaDataImportImpl. Changed GetRootModuleImport() return type to IMetaDataImport. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove unused System.Runtime.CompilerServices using in MultiModule debuggee - Fix comment: 'ridOfField' -> 'FieldDef token' in GetClassLayout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Split CopyStringToBuffer into two overloads: - void CopyStringToBuffer(char*, uint, uint*, string) for callers that don't need truncation info (SOSDacImpl, ClrDataModule, etc.) - void CopyStringToBuffer(char*, uint, uint*, string, out bool truncated) for MetaDataImportImpl callers that check truncation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
46f0cdc to
817457d
Compare
🤖 Copilot Code Review — PR #127028Note This review was AI-generated by GitHub Copilot using multi-model analysis (Claude Opus 4.6 primary, with Claude Sonnet 4.5 and GPT-5.3-Codex sub-agents). Holistic AssessmentMotivation: Well-justified. A managed Approach: Sound. Implementing ~30 commonly-used methods with Summary: Detailed Findings
|
noahfalk
left a comment
There was a problem hiding this comment.
A few nits, but overall looks good to me!
|
/ba-g previous test run passed. only nit style changes from comments |
Note
This PR description was AI/Copilot-generated.
Summary
Implements a managed
[GeneratedComClass]wrapper (MetaDataImportImpl) that adaptsSystem.Reflection.Metadata.MetadataReaderto theIMetaDataImport,IMetaDataImport2, andIMetaDataAssemblyImportCOM interfaces. This enables SOS and ClrMD to query metadata in cDAC mode, with optional legacy DAC fallback for methods not yet implemented in the managed layer.Motivation
In cDAC no-fallback mode,
ClrDataModule.GetInterface()returnedNotHandledforIMetaDataImportQIs when_legacyModulePointer == 0, meaning diagnostic tools couldn't access type/method/field metadata. The cDAC already has access toMetadataReadervia theEcmaMetadatacontract, so a thin COM wrapper bridges the gap.When a legacy DAC is available, the wrapper uses it for
#if DEBUGvalidation (asserting that cDAC and DAC produce identical results) and as a fallback for the ~45 methods not yet implemented in managed code.Changes
IMetaDataImport.cs[GeneratedComInterface]definitions for IMetaDataImport (51 methods), IMetaDataImport2 (8 methods), IMetaDataAssemblyImport (14 methods), ASSEMBLYMETADATA struct, and internalCldbHResultsconstantsMetaDataImportImpl.cs[GeneratedComClass]implementation — 28 full cDAC implementations via MetadataReader + ~45 legacy-delegated stubs. Uses explicit interface notation.OutputBufferHelpers.csCopyStringToBuffersplit into two overloads:void(for callers that don't check truncation) andout bool truncated(for MetaDataImportImpl)ClrDataModule.csICustomQueryInterface— createsMetaDataImportImplwith both MetadataReader and optional legacy IMetaDataImportMetaDataImportImplTests.csMetadataBuilderMetaDataImportDumpTests.csMultiModuledebuggeeImplemented methods (28 cDAC, ~45 legacy fallback)
Enum (cDAC):
EnumInterfaceImpls,EnumFields,EnumGenericParams,CloseEnum,CountEnum,ResetEnumProperties (cDAC):
GetTypeDefProps,GetTypeRefProps,GetMethodProps,GetFieldProps,GetMemberProps,GetInterfaceImplProps,GetNestedClassProps,GetGenericParamProps,GetMemberRefProps,GetModuleRefProps,GetParamProps,GetClassLayout,GetUserString,GetParamForMethodIndexBlob/token (cDAC):
GetRVA,GetSigFromToken,GetTypeSpecFromToken,GetCustomAttributeByName,IsValidToken,FindTypeDefByNameAssembly (cDAC):
GetAssemblyProps,GetAssemblyRefProps,GetAssemblyFromScopeLegacy fallback: All remaining methods delegate to
_legacyImport/_legacyImport2/_legacyAssemblyImportor returnE_NOTIMPLwhen no legacy is available.Native parity behaviors
<Module>parent mapping:GetMethodProps/GetFieldProps/GetMemberRefPropsmap TypeDef RID 1 tomdTypeDefNil(0x00000000) viaMapGlobalParentToken, matching native RegMetaGetFieldProps/GetParamPropsreturnELEMENT_TYPE_VOIDwhen no constant is presentGetUserStringuses raw#USheap byte parsing to exactly match native blob size validation (odd-length check, terminal byte stripping)GetAssemblyPropsORsafPublicKeyinto flags when public key blob is non-emptyCLDB_S_TRUNCATIONwhen buffer is too smallGetNestedClassProps/GetClassLayout/GetAssemblyPropsreturnCLDB_E_RECORD_NOTFOUNDfor missing recordsDesign decisions
int IMetaDataImport.Method(...)) to keep the public surface cleanIMetaDataImportreturns anIMetaDataImport2vtable so callers always get the full interfaceGCHandle.Allocto boxMetadataEnumobjects;ConcurrentDictionary<nint, byte>tracks ownership for routing CloseEnum/CountEnum/ResetEnum between cDAC and legacy handles#if DEBUGvalidation: Every cDAC-implemented method (with 2 justified exceptions) cross-checks its output against the legacy DAC in debug buildsvoidoverload for callers that don't need truncation info;out bool truncatedoverload for MetaDataImportImpl callers that returnCLDB_S_TRUNCATIONTesting