Skip to content

Commit 23d2e12

Browse files
max-charlambMax CharlambCopilot
authored
[cDAC] Add IMetaDataImport COM wrapper over MetadataReader for no-fallback mode (#127028)
> [!NOTE] > This PR description was AI/Copilot-generated. ## Summary Implements a managed `[GeneratedComClass]` wrapper (`MetaDataImportImpl`) that adapts `System.Reflection.Metadata.MetadataReader` to the `IMetaDataImport`, `IMetaDataImport2`, and `IMetaDataAssemblyImport` COM 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()` returned `NotHandled` for `IMetaDataImport` QIs when `_legacyModulePointer == 0`, meaning diagnostic tools couldn't access type/method/field metadata. The cDAC already has access to `MetadataReader` via the `EcmaMetadata` contract, so a thin COM wrapper bridges the gap. When a legacy DAC *is* available, the wrapper uses it for `#if DEBUG` validation (asserting that cDAC and DAC produce identical results) and as a fallback for the ~45 methods not yet implemented in managed code. ## Changes | File | Description | |------|-------------| | `IMetaDataImport.cs` | Managed `[GeneratedComInterface]` definitions for IMetaDataImport (51 methods), IMetaDataImport2 (8 methods), IMetaDataAssemblyImport (14 methods), ASSEMBLYMETADATA struct, and internal `CldbHResults` constants | | `MetaDataImportImpl.cs` | `[GeneratedComClass]` implementation — 28 full cDAC implementations via MetadataReader + ~45 legacy-delegated stubs. Uses explicit interface notation. | | `OutputBufferHelpers.cs` | `CopyStringToBuffer` split into two overloads: `void` (for callers that don't check truncation) and `out bool truncated` (for MetaDataImportImpl) | | `ClrDataModule.cs` | Wire up wrapper via `ICustomQueryInterface` — creates `MetaDataImportImpl` with both MetadataReader and optional legacy IMetaDataImport | | `MetaDataImportImplTests.cs` | 50 unit tests using synthetic metadata built with `MetadataBuilder` | | `MetaDataImportDumpTests.cs` | 3 dump-based integration tests verifying semantic parity against real metadata | | `MultiModule` debuggee | Test debuggee with non-const fields, user strings, and methods for dump tests | ### Implemented methods (28 cDAC, ~45 legacy fallback) **Enum (cDAC):** `EnumInterfaceImpls`, `EnumFields`, `EnumGenericParams`, `CloseEnum`, `CountEnum`, `ResetEnum` **Properties (cDAC):** `GetTypeDefProps`, `GetTypeRefProps`, `GetMethodProps`, `GetFieldProps`, `GetMemberProps`, `GetInterfaceImplProps`, `GetNestedClassProps`, `GetGenericParamProps`, `GetMemberRefProps`, `GetModuleRefProps`, `GetParamProps`, `GetClassLayout`, `GetUserString`, `GetParamForMethodIndex` **Blob/token (cDAC):** `GetRVA`, `GetSigFromToken`, `GetTypeSpecFromToken`, `GetCustomAttributeByName`, `IsValidToken`, `FindTypeDefByName` **Assembly (cDAC):** `GetAssemblyProps`, `GetAssemblyRefProps`, `GetAssemblyFromScope` **Legacy fallback:** All remaining methods delegate to `_legacyImport`/`_legacyImport2`/`_legacyAssemblyImport` or return `E_NOTIMPL` when no legacy is available. ### Native parity behaviors - **`<Module>` parent mapping**: `GetMethodProps`/`GetFieldProps`/`GetMemberRefProps` map TypeDef RID 1 to `mdTypeDefNil` (0x00000000) via `MapGlobalParentToken`, matching native RegMeta - **Constant defaults**: `GetFieldProps`/`GetParamProps` return `ELEMENT_TYPE_VOID` when no constant is present - **User string heap**: `GetUserString` uses raw `#US` heap byte parsing to exactly match native blob size validation (odd-length check, terminal byte stripping) - **Assembly flags**: `GetAssemblyProps` ORs `afPublicKey` into flags when public key blob is non-empty - **Truncation**: All string-returning methods return `CLDB_S_TRUNCATION` when buffer is too small - **Record not found**: `GetNestedClassProps`/`GetClassLayout`/`GetAssemblyProps` return `CLDB_E_RECORD_NOTFOUND` for missing records ### Design decisions - **Explicit interface implementation**: All ~73 COM methods use explicit interface notation (`int IMetaDataImport.Method(...)`) to keep the public surface clean - **ICustomQueryInterface**: QI for `IMetaDataImport` returns an `IMetaDataImport2` vtable so callers always get the full interface - **HCORENUM pattern**: Uses `GCHandle.Alloc` to box `MetadataEnum` objects; `ConcurrentDictionary<nint, byte>` tracks ownership for routing CloseEnum/CountEnum/ResetEnum between cDAC and legacy handles - **`#if DEBUG` validation**: Every cDAC-implemented method (with 2 justified exceptions) cross-checks its output against the legacy DAC in debug builds - **CopyStringToBuffer overloads**: `void` overload for callers that don't need truncation info; `out bool truncated` overload for MetaDataImportImpl callers that return `CLDB_S_TRUNCATION` ## Testing - **1845 unit tests** pass (all cDAC tests including 50 MetaDataImportImpl tests) - **214 dump-based tests** pass locally (3 are MetaDataImport-specific) - Tests cover: enum pagination, global/non-global parent mapping, constant handling, user string char counts, truncation, nested classes, generic params, assembly properties, invalid tokens, QI vtable correctness, and E_NOTIMPL fallback behavior --------- Co-authored-by: Max Charlamb <maxcharlamb@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent dc94b10 commit 23d2e12

10 files changed

Lines changed: 3411 additions & 7 deletions

File tree

src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataModule.cs

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public sealed unsafe partial class ClrDataModule : ICustomQueryInterface, IXCLRD
3434
// This is an IUnknown pointer for the legacy implementation
3535
private readonly nint _legacyModulePointer;
3636

37+
private MetaDataImportImpl? _metaDataImportImpl;
38+
3739
public ClrDataModule(TargetPointer address, Target target, IXCLRDataModule? legacyImpl)
3840
{
3941
_address = address;
@@ -49,19 +51,70 @@ public ClrDataModule(TargetPointer address, Target target, IXCLRDataModule? lega
4951

5052
private const uint CORDEBUG_JIT_DEFAULT = 0x1;
5153
private const uint CORDEBUG_JIT_DISABLE_OPTIMIZATION = 0x3;
52-
private static readonly Guid IID_IMetaDataImport = Guid.Parse("7DAC8207-D3AE-4c75-9B67-92801A497D44");
5354

5455
CustomQueryInterfaceResult ICustomQueryInterface.GetInterface(ref Guid iid, out nint ppv)
5556
{
5657
ppv = default;
57-
if (!LegacyFallbackHelper.CanFallback() || _legacyModulePointer == 0)
58-
return CustomQueryInterfaceResult.NotHandled;
5958

6059
// Legacy DAC implementation of IXCLRDataModule handles QIs for IMetaDataImport by creating and
6160
// passing out an implementation of IMetaDataImport. Note that it does not do COM aggregation.
6261
// It simply returns a completely separate object. See ClrDataModule::QueryInterface in task.cpp
63-
if (iid == IID_IMetaDataImport && Marshal.QueryInterface(_legacyModulePointer, iid, out ppv) >= 0)
62+
// The returned MetaDataImportImpl also implements IMetaDataImport2 and IMetaDataAssemblyImport,
63+
// so consumers can QI the returned object for those interfaces as well.
64+
//
65+
// IMPORTANT: Some consumers (e.g. ClrMD) QI for IMetaDataImport but then access IMetaDataImport2
66+
// vtable slots beyond the IMetaDataImport vtable boundary. This works with native C++ COM objects
67+
// (where the vtable for IMetaDataImport and IMetaDataImport2 is unified) but breaks with managed
68+
// [GeneratedComInterface] CCWs which create separate vtables per interface. To handle this, we
69+
// always return the IMetaDataImport2 vtable pointer when asked for IMetaDataImport. Since
70+
// IMetaDataImport2 inherits from IMetaDataImport, the first slots are identical.
71+
if (iid == typeof(IMetaDataImport).GUID)
72+
{
73+
MetaDataImportImpl? wrapper = _metaDataImportImpl;
74+
if (wrapper is null)
75+
{
76+
MetadataReader? reader = null;
77+
IMetaDataImport? legacyImport = null;
78+
79+
try
80+
{
81+
ILoader loader = _target.Contracts.Loader;
82+
Contracts.ModuleHandle moduleHandle = loader.GetModuleHandleFromModulePtr(_address);
83+
reader = _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle);
84+
}
85+
catch
86+
{
87+
}
88+
89+
try
90+
{
91+
Guid iidMetaDataImport = typeof(IMetaDataImport).GUID;
92+
if (_legacyModulePointer != 0 && Marshal.QueryInterface(_legacyModulePointer, iidMetaDataImport, out nint ppMdi) >= 0)
93+
{
94+
legacyImport = ComInterfaceMarshaller<IMetaDataImport>.ConvertToManaged((void*)ppMdi);
95+
Marshal.Release(ppMdi);
96+
}
97+
}
98+
catch
99+
{
100+
}
101+
102+
if (reader is null)
103+
return CustomQueryInterfaceResult.NotHandled;
104+
105+
wrapper = new MetaDataImportImpl(reader, legacyImport);
106+
_metaDataImportImpl ??= wrapper;
107+
wrapper = _metaDataImportImpl;
108+
}
109+
110+
nint pUnk = (nint)ComInterfaceMarshaller<IMetaDataImport2>.ConvertToUnmanaged(wrapper);
111+
112+
// ConvertToUnmanaged returns a COM pointer for IMetaDataImport2.
113+
// We return this directly as ppv so that consumers (e.g. ClrMD) that QI for
114+
// IMetaDataImport but access IMetaDataImport2 vtable slots get the full vtable.
115+
ppv = pUnk;
64116
return CustomQueryInterfaceResult.Handled;
117+
}
65118

66119
return CustomQueryInterfaceResult.NotHandled;
67120
}

0 commit comments

Comments
 (0)