[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 wants to merge 32 commits intomainfrom
Conversation
|
Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag |
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.
…ack mode Implement a managed [GeneratedComClass] wrapper that adapts System.Reflection.Metadata.MetadataReader to the IMetaDataImport/IMetaDataImport2 COM interfaces. This enables SOS and ClrMD to query metadata in cDAC no-fallback mode where the legacy DAC is unavailable. Key changes: - IMetaDataImport.cs: Managed COM interface definitions with exact vtable ordering from cor.h (62 IMetaDataImport + 8 IMetaDataImport2 methods) - MetadataImportWrapper.cs: Implementation wrapping MetadataReader with ~21 real implementations (enum, property, blob/token methods) and ~40 E_NOTIMPL stubs - ClrDataModule.cs: Wire up wrapper in GetInterface() as no-fallback path with thread-safe lazy init and proper COM ref counting - MetadataImportWrapperTests.cs: 21 unit tests using synthetic metadata Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove 7 unused method implementations (EnumTypeDefs, EnumTypeRefs, EnumMembers, EnumMethods, EnumCustomAttributes, CountEnum, ResetEnum) that are not called by SOS or ClrMD, replacing them with E_NOTIMPL stubs. Add 8 new method implementations needed by SOS/ClrMD consumers: FindTypeDefByName, GetMemberRefProps, GetParamProps, GetModuleRefProps, GetTypeSpecFromToken, GetUserString, GetParamForMethodIndex, GetClassLayout. Extract CLDB_E_RECORD_NOTFOUND to a class-level constant. Update tests: remove tests for removed methods, add tests for all new methods, update NotImplementedMethods test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
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>
| Assert.NotNull(reader); | ||
|
|
||
| return (reader, new MetaDataImportImpl(reader)); |
There was a problem hiding this comment.
GetRootModuleImport uses MetadataReader? and then returns/passes it as non-nullable MetadataReader. Since MetadataReader is a struct and IEcmaMetadata.GetMetadata returns MetadataReader?, this won't compile; use reader.Value (after checking reader.HasValue) or restructure to avoid nullable.
| Assert.NotNull(reader); | |
| return (reader, new MetaDataImportImpl(reader)); | |
| Assert.True(reader.HasValue); | |
| MetadataReader metadataReader = reader.Value; | |
| return (metadataReader, new MetaDataImportImpl(metadataReader)); |
| mb.AddTypeSpecification(mb.GetOrAddBlob(typeSpecSig)); | ||
|
|
||
| // UserString: "Hello, World!" | ||
| UserStringHandle userStringHandle = mb.GetOrAddUserString("Hello, World!"); |
There was a problem hiding this comment.
CreateTestMetadata declares userStringHandle but never uses it. This triggers CS0219 (warning-as-error) and will fail the build; either remove the local or explicitly discard the result (e.g., assign to _).
| UserStringHandle userStringHandle = mb.GetOrAddUserString("Hello, World!"); | |
| _ = mb.GetOrAddUserString("Hello, World!"); |
| // FieldDef: StringConst (string) — field row 2, with string constant | ||
| FieldDefinitionHandle stringConstField = mb.AddFieldDefinition( | ||
| FieldAttributes.Public | FieldAttributes.Static | FieldAttributes.Literal | FieldAttributes.HasDefault, | ||
| mb.GetOrAddString("StringConst"), intFieldSig); |
There was a problem hiding this comment.
StringConst is described as a string field but is created with the intFieldSig signature. That makes the synthetic metadata inconsistent and can hide issues; use a string field signature blob for StringConst.
| mb.GetOrAddString("StringConst"), intFieldSig); | |
| mb.GetOrAddString("StringConst"), stringFieldSig); |
| // Provider is stored alongside wrapper to prevent GC from collecting pinned metadata memory. | ||
| private static MetadataReaderProvider? _testProvider; | ||
|
|
||
| private static MetaDataImportImpl CreateWrapper() | ||
| { | ||
| (MetadataReader reader, MetadataReaderProvider provider) = CreateTestMetadata(); | ||
| _testProvider = provider; | ||
| return new MetaDataImportImpl(reader); | ||
| } |
There was a problem hiding this comment.
CreateWrapper stores the MetadataReaderProvider in a static field and never disposes it. Since MetadataReaderProvider is IDisposable, this retains metadata buffers for the entire test run; consider returning the provider from CreateWrapper and disposing it per-test (or using a fixture implementing IDisposable).
| int copyChars = Math.Min(charCount, (int)cchString); | ||
| new ReadOnlySpan<char>(dataPtr, copyChars).CopyTo(new Span<char>(szString, copyChars)); | ||
|
|
||
| if ((uint)charCount > cchString) | ||
| { | ||
| szString[cchString - 1] = '\0'; |
There was a problem hiding this comment.
GetUserString copies charCount characters into the buffer but only writes a null terminator on truncation and treats cchString == charCount as non-truncated. This can return a non-null-terminated buffer and incorrect truncation behavior; treat cchString as including space for the terminator (copy at most cchString-1, always write \0 when cchString>0, and return CLDB_S_TRUNCATION when charCount + 1 > cchString).
| int copyChars = Math.Min(charCount, (int)cchString); | |
| new ReadOnlySpan<char>(dataPtr, copyChars).CopyTo(new Span<char>(szString, copyChars)); | |
| if ((uint)charCount > cchString) | |
| { | |
| szString[cchString - 1] = '\0'; | |
| int destinationCapacity = (int)cchString - 1; | |
| int copyChars = Math.Min(charCount, destinationCapacity); | |
| new ReadOnlySpan<char>(dataPtr, copyChars).CopyTo(new Span<char>(szString, copyChars)); | |
| szString[copyChars] = '\0'; | |
| if ((uint)charCount + 1 > cchString) | |
| { |
…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>
| public static unsafe bool CopyStringToBuffer(char* stringBuf, uint bufferSize, uint* neededBufferSize, string str) | ||
| { |
There was a problem hiding this comment.
Changing CopyStringToBuffer from void to bool is a breaking change for any external callers of this public helper. If this API is consumed outside this assembly/package, consider preserving the original void signature (and adding a new overload/optional out parameter for truncation) to avoid breaking binary/source compatibility.
…criptor" This reverts commit 6117d00.
This comment has been minimized.
This comment has been minimized.
- 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>
| if (_cdacEnumHandles.ContainsKey(hEnum)) | ||
| { | ||
| MetadataEnum? e = GetEnum(hEnum); | ||
| if (e is null) | ||
| return HResults.E_FAIL; | ||
| if (pulCount is not null) | ||
| *pulCount = (uint)e.Tokens.Count; | ||
| return HResults.S_OK; |
There was a problem hiding this comment.
CountEnum uses _cdacEnumHandles.ContainsKey(hEnum) and then dereferences the GCHandle via GetEnum(hEnum). If another thread calls CloseEnum between those operations, the handle can be freed and GCHandle.Target access may throw (unhandled here). Consider using a single atomic lookup and wrapping GetEnum in a try/catch (or otherwise ensuring the handle can’t be freed concurrently).
| if (_cdacEnumHandles.ContainsKey(hEnum)) | ||
| { | ||
| MetadataEnum? e = GetEnum(hEnum); | ||
| if (e is null) | ||
| return HResults.E_FAIL; | ||
| e.Position = (int)Math.Min(ulPos, (uint)e.Tokens.Count); | ||
| return HResults.S_OK; |
There was a problem hiding this comment.
ResetEnum has the same race as CountEnum: ContainsKey followed by GetEnum(hEnum) can observe a handle that gets freed concurrently by CloseEnum, potentially throwing before you can return an HRESULT. Consider an atomic lookup pattern and exception-to-HRESULT handling here as well.
| int copyChars = Math.Min(charCount, (int)cchString); | ||
| new ReadOnlySpan<char>(dataPtr, copyChars).CopyTo(new Span<char>(szString, copyChars)); | ||
|
|
||
| if ((uint)charCount > cchString) | ||
| { | ||
| szString[cchString - 1] = '\0'; |
There was a problem hiding this comment.
GetUserString copies up to cchString characters and only writes a null terminator when charCount > cchString. If the buffer size is intended to include the terminator (as with other *Props APIs), the copy should reserve 1 char for \0 and treat charCount >= cchString as truncation; otherwise callers can get a non-null-terminated buffer when charCount == cchString.
| int copyChars = Math.Min(charCount, (int)cchString); | |
| new ReadOnlySpan<char>(dataPtr, copyChars).CopyTo(new Span<char>(szString, copyChars)); | |
| if ((uint)charCount > cchString) | |
| { | |
| szString[cchString - 1] = '\0'; | |
| int copyChars = Math.Min(charCount, (int)cchString - 1); | |
| new ReadOnlySpan<char>(dataPtr, copyChars).CopyTo(new Span<char>(szString, copyChars)); | |
| szString[copyChars] = '\0'; | |
| if ((uint)charCount >= cchString) | |
| { |
| using System.Reflection; | ||
| using System.Runtime.CompilerServices; | ||
| using System.Runtime.Loader; |
There was a problem hiding this comment.
using System.Runtime.CompilerServices; is currently unused in this file; please remove it to avoid build warnings/noise.
There was a problem hiding this comment.
This seems correct.
| if (reader is null) | ||
| return CustomQueryInterfaceResult.NotHandled; | ||
|
|
||
| wrapper = new MetaDataImportImpl(reader, legacyImport); |
There was a problem hiding this comment.
reader is a MetadataReader? (nullable struct). Passing it directly into new MetaDataImportImpl(reader, legacyImport) will not compile; unwrap it after the null-check (e.g., reader.Value / GetValueOrDefault()).
| wrapper = new MetaDataImportImpl(reader, legacyImport); | |
| wrapper = new MetaDataImportImpl(reader.Value, legacyImport); |
- 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>
| using System.Reflection; | ||
| using System.Runtime.CompilerServices; | ||
| using System.Runtime.Loader; |
There was a problem hiding this comment.
This seems correct.
- 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>
| return CustomQueryInterfaceResult.NotHandled; | ||
|
|
There was a problem hiding this comment.
If EcmaMetadata.GetMetadata fails (reader is null) but the legacy module pointer can still provide IMetaDataImport, this currently returns NotHandled and the QI fails even in fallback mode. Consider falling back to returning the legacy IMetaDataImport COM pointer when reader is unavailable, preserving the previous behavior for legacy-capable scenarios.
| return CustomQueryInterfaceResult.NotHandled; | |
| { | |
| if (legacyImport is null) | |
| return CustomQueryInterfaceResult.NotHandled; | |
| ppv = (nint)ComInterfaceMarshaller<IMetaDataImport>.ConvertToUnmanaged(legacyImport); | |
| return CustomQueryInterfaceResult.Handled; | |
| } |
| { | ||
| int hr = HResults.S_OK; | ||
| try | ||
| { |
There was a problem hiding this comment.
GetTypeDefProps currently masks off the token type (td & 0x00FFFFFF) and always treats the input as a TypeDef RID. Native RegMeta returns S_FALSE for non-TypeDef tokens and has special handling for mdTypeDefNil (0). To match expected COM semantics and avoid returning incorrect data for wrong token kinds, validate TypeFromToken(td) and handle td==0 explicitly before reading metadata.
| { | |
| { | |
| if (td == 0) | |
| return HResults.S_FALSE; | |
| if ((td & 0xFF000000) != (uint)CorTokenType.mdtTypeDef) | |
| return HResults.S_FALSE; |
| TypeReferenceHandle refHandle = MetadataTokens.TypeReferenceHandle((int)(tr & 0x00FFFFFF)); | ||
| TypeReference typeRef = _reader.GetTypeReference(refHandle); | ||
|
|
||
| string fullName = GetTypeRefFullName(typeRef); | ||
| bool truncated = OutputBufferHelpers.CopyStringToBuffer(szName, cchName, pchName, fullName); | ||
|
|
||
| if (ptkResolutionScope is not null) | ||
| { | ||
| EntityHandle scope = typeRef.ResolutionScope; | ||
| *ptkResolutionScope = scope.IsNil ? 0 : (uint)MetadataTokens.GetToken(scope); | ||
| } | ||
|
|
||
| hr = truncated ? CorDbgHResults.CLDB_S_TRUNCATION : HResults.S_OK; |
There was a problem hiding this comment.
GetTypeRefProps also ignores the token type by masking to a RID and unconditionally reading the TypeRef table. Native RegMeta returns S_FALSE for non-TypeRef tokens and treats mdTypeRefNil (0) as a special case (empty name, length=1, scope=nil). Add token-kind validation and nil-token handling to avoid returning the wrong name/scope for unexpected tokens.
| TypeReferenceHandle refHandle = MetadataTokens.TypeReferenceHandle((int)(tr & 0x00FFFFFF)); | |
| TypeReference typeRef = _reader.GetTypeReference(refHandle); | |
| string fullName = GetTypeRefFullName(typeRef); | |
| bool truncated = OutputBufferHelpers.CopyStringToBuffer(szName, cchName, pchName, fullName); | |
| if (ptkResolutionScope is not null) | |
| { | |
| EntityHandle scope = typeRef.ResolutionScope; | |
| *ptkResolutionScope = scope.IsNil ? 0 : (uint)MetadataTokens.GetToken(scope); | |
| } | |
| hr = truncated ? CorDbgHResults.CLDB_S_TRUNCATION : HResults.S_OK; | |
| const uint MdTypeRefNil = 0; | |
| const uint TypeRefTokenType = 0x01000000; | |
| if (tr == MdTypeRefNil) | |
| { | |
| bool truncated = OutputBufferHelpers.CopyStringToBuffer(szName, cchName, pchName, string.Empty); | |
| if (ptkResolutionScope is not null) | |
| { | |
| *ptkResolutionScope = 0; | |
| } | |
| hr = truncated ? CorDbgHResults.CLDB_S_TRUNCATION : HResults.S_FALSE; | |
| } | |
| else if ((tr & 0xFF000000) != TypeRefTokenType) | |
| { | |
| hr = HResults.S_FALSE; | |
| } | |
| else | |
| { | |
| TypeReferenceHandle refHandle = MetadataTokens.TypeReferenceHandle((int)(tr & 0x00FFFFFF)); | |
| TypeReference typeRef = _reader.GetTypeReference(refHandle); | |
| string fullName = GetTypeRefFullName(typeRef); | |
| bool truncated = OutputBufferHelpers.CopyStringToBuffer(szName, cchName, pchName, fullName); | |
| if (ptkResolutionScope is not null) | |
| { | |
| EntityHandle scope = typeRef.ResolutionScope; | |
| *ptkResolutionScope = scope.IsNil ? 0 : (uint)MetadataTokens.GetToken(scope); | |
| } | |
| hr = truncated ? CorDbgHResults.CLDB_S_TRUNCATION : HResults.S_OK; | |
| } |
| int hr = HResults.S_OK; | ||
| try | ||
| { | ||
| MethodDefinitionHandle methodHandle = MetadataTokens.MethodDefinitionHandle((int)(mb & 0x00FFFFFF)); | ||
| MethodDefinition methodDef = _reader.GetMethodDefinition(methodHandle); | ||
|
|
||
| string name = _reader.GetString(methodDef.Name); | ||
| bool truncated = OutputBufferHelpers.CopyStringToBuffer(szMethod, cchMethod, pchMethod, name); | ||
|
|
||
| if (pClass is not null) | ||
| *pClass = MapGlobalParentToken((uint)MetadataTokens.GetToken(methodDef.GetDeclaringType())); | ||
|
|
||
| if (pdwAttr is not null) | ||
| *pdwAttr = (uint)methodDef.Attributes; | ||
|
|
||
| if (ppvSigBlob is not null || pcbSigBlob is not null) | ||
| { | ||
| BlobHandle sigHandle = methodDef.Signature; | ||
| BlobReader blobReader = _reader.GetBlobReader(sigHandle); | ||
| if (ppvSigBlob is not null) | ||
| *ppvSigBlob = blobReader.StartPointer; | ||
| if (pcbSigBlob is not null) | ||
| *pcbSigBlob = (uint)blobReader.Length; | ||
| } | ||
|
|
||
| if (pulCodeRVA is not null) | ||
| *pulCodeRVA = (uint)methodDef.RelativeVirtualAddress; | ||
|
|
||
| if (pdwImplFlags is not null) | ||
| *pdwImplFlags = (uint)methodDef.ImplAttributes; | ||
|
|
||
| hr = truncated ? CorDbgHResults.CLDB_S_TRUNCATION : HResults.S_OK; |
There was a problem hiding this comment.
GetMethodProps constructs a MethodDefinitionHandle from (mb & 0x00FFFFFF) without validating that mb is a MethodDef token or handling mdMethodDefNil. In native RegMeta these APIs return S_FALSE for wrong token types. Without validation, passing a non-MethodDef token can return unrelated method data instead of failing. Consider validating the token kind up front (and similarly for GetFieldProps/GetMemberRefProps/etc.).
| int hr = HResults.S_OK; | |
| try | |
| { | |
| MethodDefinitionHandle methodHandle = MetadataTokens.MethodDefinitionHandle((int)(mb & 0x00FFFFFF)); | |
| MethodDefinition methodDef = _reader.GetMethodDefinition(methodHandle); | |
| string name = _reader.GetString(methodDef.Name); | |
| bool truncated = OutputBufferHelpers.CopyStringToBuffer(szMethod, cchMethod, pchMethod, name); | |
| if (pClass is not null) | |
| *pClass = MapGlobalParentToken((uint)MetadataTokens.GetToken(methodDef.GetDeclaringType())); | |
| if (pdwAttr is not null) | |
| *pdwAttr = (uint)methodDef.Attributes; | |
| if (ppvSigBlob is not null || pcbSigBlob is not null) | |
| { | |
| BlobHandle sigHandle = methodDef.Signature; | |
| BlobReader blobReader = _reader.GetBlobReader(sigHandle); | |
| if (ppvSigBlob is not null) | |
| *ppvSigBlob = blobReader.StartPointer; | |
| if (pcbSigBlob is not null) | |
| *pcbSigBlob = (uint)blobReader.Length; | |
| } | |
| if (pulCodeRVA is not null) | |
| *pulCodeRVA = (uint)methodDef.RelativeVirtualAddress; | |
| if (pdwImplFlags is not null) | |
| *pdwImplFlags = (uint)methodDef.ImplAttributes; | |
| hr = truncated ? CorDbgHResults.CLDB_S_TRUNCATION : HResults.S_OK; | |
| const uint MethodDefTokenType = 0x06000000; | |
| int hr = HResults.S_OK; | |
| try | |
| { | |
| if ((mb & 0xFF000000) != MethodDefTokenType || (mb & 0x00FFFFFF) == 0) | |
| { | |
| hr = HResults.S_FALSE; | |
| } | |
| else | |
| { | |
| MethodDefinitionHandle methodHandle = MetadataTokens.MethodDefinitionHandle((int)(mb & 0x00FFFFFF)); | |
| MethodDefinition methodDef = _reader.GetMethodDefinition(methodHandle); | |
| string name = _reader.GetString(methodDef.Name); | |
| bool truncated = OutputBufferHelpers.CopyStringToBuffer(szMethod, cchMethod, pchMethod, name); | |
| if (pClass is not null) | |
| *pClass = MapGlobalParentToken((uint)MetadataTokens.GetToken(methodDef.GetDeclaringType())); | |
| if (pdwAttr is not null) | |
| *pdwAttr = (uint)methodDef.Attributes; | |
| if (ppvSigBlob is not null || pcbSigBlob is not null) | |
| { | |
| BlobHandle sigHandle = methodDef.Signature; | |
| BlobReader blobReader = _reader.GetBlobReader(sigHandle); | |
| if (ppvSigBlob is not null) | |
| *ppvSigBlob = blobReader.StartPointer; | |
| if (pcbSigBlob is not null) | |
| *pcbSigBlob = (uint)blobReader.Length; | |
| } | |
| if (pulCodeRVA is not null) | |
| *pulCodeRVA = (uint)methodDef.RelativeVirtualAddress; | |
| if (pdwImplFlags is not null) | |
| *pdwImplFlags = (uint)methodDef.ImplAttributes; | |
| hr = truncated ? CorDbgHResults.CLDB_S_TRUNCATION : HResults.S_OK; | |
| } |
| { | ||
| if (fieldOffsets is not null && count < cMax) | ||
| { | ||
| // Each entry is {ridOfField (uint), ulOffset (uint)} |
There was a problem hiding this comment.
The comment says each layout entry stores a "ridOfField", but the code writes the full FieldDef token (MetadataTokens.GetToken). This is correct behavior for COR_FIELD_OFFSET, but the comment is misleading—update it to refer to the FieldDef token to avoid confusion.
| // Each entry is {ridOfField (uint), ulOffset (uint)} | |
| // Each entry is {FieldDef token (uint), ulOffset (uint)} |
| // Provider is stored alongside wrapper to prevent GC from collecting pinned metadata memory. | ||
| private static MetadataReaderProvider? _testProvider; | ||
|
|
||
| private static IMetaDataImport2 CreateWrapper() | ||
| { | ||
| (MetadataReader reader, MetadataReaderProvider provider) = CreateTestMetadata(); | ||
| _testProvider = provider; | ||
| return new MetaDataImportImpl(reader); |
There was a problem hiding this comment.
CreateWrapper stores MetadataReaderProvider in a static field and overwrites it per test without disposing the previous instance. Since MetadataReaderProvider holds unmanaged resources, this leaks one provider per test method in this class. Consider disposing the previous provider before overwriting, or using an xUnit fixture/IDisposable pattern so each test disposes its provider deterministically.
🤖 Copilot Code Review — PR #127028Note This review was generated by Copilot (Claude Opus 4.6), with additional perspectives from GPT-5.3-Codex and Claude Haiku 4.5. Holistic AssessmentMotivation: This PR implements a managed Approach: Sound. The implementation covers the most-used IMetaDataImport methods with managed MetadataReader while 54 less-commonly-used methods delegate to the legacy DAC. The COM interop changes to return IMetaDataImport2 vtables address a known ClrMD vtable-boundary issue. The Summary: Detailed Findings
|
Note
This PR description was AI/Copilot-generated.
Summary
Implements a managed
[GeneratedComClass]wrapper that adaptsSystem.Reflection.Metadata.MetadataReaderto theIMetaDataImport/IMetaDataImport2COM interfaces. This enables SOS and ClrMD to query metadata in cDAC no-fallback mode where the legacy DAC is unavailable.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.Changes
IMetaDataImport.cs[GeneratedComInterface]definitions for IMetaDataImport (62 methods) and IMetaDataImport2 (8 methods) with exact vtable ordering from cor.hMetadataImportWrapper.cs[GeneratedComClass]implementation wrapping MetadataReader — ~21 real implementations + ~40E_NOTIMPLstubsClrDataModule.csGetInterface()as no-fallback path with thread-safe lazy init and proper COM ref countingMetadataImportWrapperTests.csMetadataBuilderImplemented methods
Enum:
EnumTypeDefs,EnumInterfaceImpls,EnumTypeRefs,EnumMembers,EnumMethods,EnumFields,EnumCustomAttributes,EnumGenericParams,CloseEnum,CountEnum,ResetEnumProperties:
GetTypeDefProps,GetTypeRefProps,GetMethodProps,GetFieldProps,GetMemberProps,GetInterfaceImplProps,GetNestedClassProps,GetGenericParamPropsBlob/token:
GetRVA,GetSigFromToken,GetCustomAttributeByName,IsValidTokenDesign decisions
GCHandle.Allocto box aMetadataEnumobject, matching the native pattern of opaque enumeration handlesStrategyBasedComWrappersInterlocked.CompareExchangefor lazy wrapper initializationCatchHR()helper normalizes all exceptions to HRESULT valuesTesting
All 1651 cDAC tests pass (1630 existing + 21 new).