Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c0f8333
Initial plan
Copilot Feb 27, 2026
f08a49a
Implement GetRCWInterfaces cDAC API in IBuiltInCOM (contract version 2)
Copilot Feb 27, 2026
07fa9dd
Fix critical bug: RCW.InterfaceEntries is inline array, not pointer; …
Copilot Feb 27, 2026
5d2fc47
Consolidate GetRCWInterfaces into BuiltInCOM version 1
Copilot Feb 27, 2026
95723c1
Fix build break: guard runtimecallablewrapper.h include with FEATURE_…
Copilot Feb 27, 2026
e68733c
Fix dump test: create strong GCHandle for rcwObject so it's visible i…
Copilot Feb 27, 2026
18f62a0
Fix InteropSyncBlockInfo: mask m_pRCW lock bit and handle m_pCCW sent…
Copilot Feb 28, 2026
b283ae4
Revert InteropSyncBlockInfo bit-masking change and extra ObjectTests …
Copilot Feb 28, 2026
838cf24
Implement GetRCWInterfaces in SOSDacImpl using IBuiltInCOM contract
Copilot Mar 5, 2026
e339a48
Change hr = E_INVALIDARG to throw new ArgumentException() in GetRCWIn…
Copilot Mar 5, 2026
a6d9291
merge
rcj1 Mar 6, 2026
a02b2b2
fix
rcj1 Mar 6, 2026
836bec7
fix
rcj1 Mar 6, 2026
a135eca
Fix GetBuiltInComData method call in DumpTests
rcj1 Mar 6, 2026
54c9b07
fix dump test
rcj1 Mar 6, 2026
22720fc
Remove duplicate include of cdacdata.h
rcj1 Mar 6, 2026
533d49f
fix test
rcj1 Mar 6, 2026
f7db1af
Merge remote-tracking branch 'origin/main' into copilot/implement-get…
Copilot Mar 6, 2026
c3765a0
Address reviewer feedback: array marshaller, lazy interface entries, …
Copilot Mar 6, 2026
824d7ae
Remove FEATURE_COMINTEROP comment; fix DEBUG validation to assert pNe…
Copilot Mar 6, 2026
1683000
Add short BuiltInCOM comment; remove per-entry DEBUG validation loop
Copilot Mar 6, 2026
6ecb2ea
Fix BuiltInCOM.md: move RCWInterfaceCacheSize from Contract Constants…
Copilot Mar 6, 2026
a6d72e0
Fix GetRCWInterfaces DEBUG validation: always pass &pNeededLocal and …
Copilot Mar 6, 2026
187bf27
validation
rcj1 Mar 6, 2026
93e0d71
fix
rcj1 Mar 6, 2026
2a91881
Add debug information for itemIndex validation
rcj1 Mar 6, 2026
4eca4b4
fix
rcj1 Mar 9, 2026
5154e1b
Fix merge conflicts with main: restore CCW/Loader implementations and…
Copilot Mar 9, 2026
e9bfe35
Revert "Fix merge conflicts with main: restore CCW/Loader implementat…
rcj1 Mar 9, 2026
556b9e3
merge
rcj1 Mar 10, 2026
cd11f3f
Update RCWInterfacesDumpTests.cs
rcj1 Mar 10, 2026
40198c2
Fix null check for interfacesLocal initialization
rcj1 Mar 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 45 additions & 11 deletions docs/design/datacontracts/BuiltInCOM.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ This contract is for getting information related to built-in COM.
## APIs of contract

``` csharp
public struct COMInterfacePointerData
{
// Address of the slot in ComCallWrapper that holds the COM interface pointer.
public TargetPointer InterfacePointerAddress;
// MethodTable for this interface, or TargetPointer.Null for slot 0 (IUnknown/IDispatch).
public TargetPointer MethodTable;
}

public record struct RCWCleanupInfo(
TargetPointer RCW,
TargetPointer Context,
TargetPointer STAThread,
bool IsFreeThreaded);

public ulong GetRefCount(TargetPointer ccw);
// Check whether the COM wrappers handle is weak.
public bool IsHandleWeak(TargetPointer ccw);
Expand All @@ -14,20 +28,13 @@ public TargetPointer GetCCWFromInterfacePointer(TargetPointer interfacePointer);
// Enumerate the COM interfaces exposed by the ComCallWrapper chain.
// ccw may be any ComCallWrapper in the chain; the implementation navigates to the start.
public IEnumerable<COMInterfacePointerData> GetCCWInterfaces(TargetPointer ccw);
```

where `COMInterfacePointerData` is:
``` csharp
public struct COMInterfacePointerData
{
// Address of the slot in ComCallWrapper that holds the COM interface pointer.
public TargetPointer InterfacePointerAddress;
// MethodTable for this interface, or TargetPointer.Null for slot 0 (IUnknown/IDispatch).
public TargetPointer MethodTable;
}
// Enumerate entries in the RCW cleanup list.
// If cleanupListPtr is Null, the global g_pRCWCleanupList is used.
public IEnumerable<RCWCleanupInfo> GetRCWCleanupList(TargetPointer cleanupListPtr);
// Enumerate the interface entries cached in an RCW.
public IEnumerable<(TargetPointer MethodTable, TargetPointer Unknown)> GetRCWInterfaces(TargetPointer rcw);
// Get the COM context cookie for an RCW.
public TargetPointer GetRCWContext(TargetPointer rcw);
```

## Version 1
Expand All @@ -51,6 +58,9 @@ Data descriptors used:
| `RCW` | `CtxCookie` | COM context cookie for the RCW |
| `RCW` | `CtxEntry` | Pointer to `CtxEntry` (bit 0 is a synchronization flag; must be masked off before use) |
| `CtxEntry` | `STAThread` | STA thread pointer for the context entry |
| `RCW` | `InterfaceEntries` | Offset of the inline interface entry cache array within the RCW struct |
| `InterfaceEntry` | `MethodTable` | MethodTable pointer for the cached COM interface |
| `InterfaceEntry` | `Unknown` | `IUnknown*` pointer for the cached COM interface |

Global variables used:
| Global Name | Type | Purpose |
Expand All @@ -62,6 +72,7 @@ Global variables used:
| `TearOffAddRefSimple` | pointer | Address of `Unknown_AddRefSpecial`; identifies `SimpleComCallWrapper` interface pointers |
| `TearOffAddRefSimpleInner` | pointer | Address of `Unknown_AddRefInner`; identifies inner `SimpleComCallWrapper` interface pointers |
| `RCWCleanupList` | `pointer` | Pointer to the global `g_pRCWCleanupList` instance |
| `RCWInterfaceCacheSize` | `uint32` | Number of entries in the inline interface entry cache (`INTERFACE_ENTRY_CACHE_SIZE`) |

### Contract Constants:
| Name | Type | Purpose | Value |
Expand Down Expand Up @@ -156,5 +167,28 @@ public IEnumerable<RCWCleanupInfo> GetRCWCleanupList(TargetPointer cleanupListPt
bucketPtr = _target.ReadPointer(bucketPtr + /* RCW::NextCleanupBucket offset */);
}
}

public IEnumerable<(TargetPointer MethodTable, TargetPointer Unknown)> GetRCWInterfaces(TargetPointer rcw)
{
// InterfaceEntries is an inline array — the offset gives the address of the first element.
TargetPointer interfaceEntriesAddr = rcw + /* RCW::InterfaceEntries offset */;
uint cacheSize = _target.ReadGlobal<uint>("RCWInterfaceCacheSize");
uint entrySize = /* size of InterfaceEntry */;

for (uint i = 0; i < cacheSize; i++)
{
TargetPointer entryAddress = interfaceEntriesAddr + i * entrySize;
TargetPointer methodTable = _target.ReadPointer(entryAddress + /* InterfaceEntry::MethodTable offset */);
TargetPointer unknown = _target.ReadPointer(entryAddress + /* InterfaceEntry::Unknown offset */);
// An entry is free if Unknown == null (matches InterfaceEntry::IsFree())
if (unknown != TargetPointer.Null)
yield return (methodTable, unknown);
}
}

public TargetPointer GetRCWContext(TargetPointer rcw)
{
return _target.ReadPointer(rcw + /* RCW::CtxCookie offset */);
}
```

9 changes: 9 additions & 0 deletions src/coreclr/vm/comcache.h
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,15 @@ struct InterfaceEntry
// will not try and optimize reads and writes to them.
Volatile<IE_METHODTABLE_PTR> m_pMT; // Interface asked for
Volatile<IUnknown*> m_pUnknown; // Result of query

friend struct ::cdac_data<InterfaceEntry>;
};

template<>
struct cdac_data<InterfaceEntry>
{
static constexpr size_t MethodTable = offsetof(InterfaceEntry, m_pMT);
static constexpr size_t Unknown = offsetof(InterfaceEntry, m_pUnknown);
};

class CtxEntryCacheTraits : public DefaultSHashTraits<CtxEntry *>
Expand Down
8 changes: 8 additions & 0 deletions src/coreclr/vm/datadescriptor/datadescriptor.inc
Original file line number Diff line number Diff line change
Expand Up @@ -1115,12 +1115,19 @@ CDAC_TYPE_FIELD(RCW, /*pointer*/, NextRCW, cdac_data<RCW>::NextRCW)
CDAC_TYPE_FIELD(RCW, /*uint32*/, Flags, cdac_data<RCW>::Flags)
CDAC_TYPE_FIELD(RCW, /*pointer*/, CtxCookie, cdac_data<RCW>::CtxCookie)
CDAC_TYPE_FIELD(RCW, /*pointer*/, CtxEntry, cdac_data<RCW>::CtxEntry)
CDAC_TYPE_FIELD(RCW, /*inline array*/, InterfaceEntries, cdac_data<RCW>::InterfaceEntries)
CDAC_TYPE_END(RCW)

CDAC_TYPE_BEGIN(CtxEntry)
CDAC_TYPE_INDETERMINATE(CtxEntry)
CDAC_TYPE_FIELD(CtxEntry, /*pointer*/, STAThread, cdac_data<CtxEntry>::STAThread)
CDAC_TYPE_END(CtxEntry)

CDAC_TYPE_BEGIN(InterfaceEntry)
CDAC_TYPE_SIZE(sizeof(InterfaceEntry))
CDAC_TYPE_FIELD(InterfaceEntry, /*pointer*/, MethodTable, cdac_data<InterfaceEntry>::MethodTable)
CDAC_TYPE_FIELD(InterfaceEntry, /*pointer*/, Unknown, cdac_data<InterfaceEntry>::Unknown)
CDAC_TYPE_END(InterfaceEntry)
#endif // FEATURE_COMINTEROP

#ifdef FEATURE_COMWRAPPERS
Expand Down Expand Up @@ -1306,6 +1313,7 @@ CDAC_GLOBAL_POINTER(TearOffAddRef, &g_cdacTearOffAddRef)
CDAC_GLOBAL_POINTER(TearOffAddRefSimple, &g_cdacTearOffAddRefSimple)
CDAC_GLOBAL_POINTER(TearOffAddRefSimpleInner, &g_cdacTearOffAddRefSimpleInner)
CDAC_GLOBAL_POINTER(RCWCleanupList, &g_pRCWCleanupList)
CDAC_GLOBAL(RCWInterfaceCacheSize, uint32, INTERFACE_ENTRY_CACHE_SIZE)
#endif // FEATURE_COMINTEROP

// It is important for the subdescriptor pointers to be the last pointers in the global structure.
Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/vm/runtimecallablewrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,6 @@ private :

// IUnkEntry needs to access m_UnkEntry field
friend IUnkEntry;
// cdac_data<RCW> needs access to m_UnkEntry
friend struct ::cdac_data<RCW>;

private :
Expand Down Expand Up @@ -591,6 +590,7 @@ struct cdac_data<RCW>
static constexpr size_t Flags = offsetof(RCW, m_Flags);
static constexpr size_t CtxCookie = offsetof(RCW, m_UnkEntry) + offsetof(IUnkEntry, m_pCtxCookie);
static constexpr size_t CtxEntry = offsetof(RCW, m_UnkEntry) + offsetof(IUnkEntry, m_pCtxEntry);
static constexpr size_t InterfaceEntries = offsetof(RCW, m_aInterfaceEntries);
};

inline RCW::CreationFlags operator|(RCW::CreationFlags lhs, RCW::CreationFlags rhs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public interface IBuiltInCOM : IContract
// ccw may be any ComCallWrapper in the chain; the implementation navigates to the start.
IEnumerable<COMInterfacePointerData> GetCCWInterfaces(TargetPointer ccw) => throw new NotImplementedException();
IEnumerable<RCWCleanupInfo> GetRCWCleanupList(TargetPointer cleanupListPtr) => throw new NotImplementedException();
IEnumerable<(TargetPointer MethodTable, TargetPointer Unknown)> GetRCWInterfaces(TargetPointer rcw) => throw new NotImplementedException();
TargetPointer GetRCWContext(TargetPointer rcw) => throw new NotImplementedException();
}

public readonly struct BuiltInCOM : IBuiltInCOM
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ public enum DataType
RCWCleanupList,
RCW,
CtxEntry,
InterfaceEntry,


/* GC Data Types */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ public static class Globals
public const string TearOffAddRefSimple = nameof(TearOffAddRefSimple);
public const string TearOffAddRefSimpleInner = nameof(TearOffAddRefSimpleInner);
public const string RCWCleanupList = nameof(RCWCleanupList);
public const string RCWInterfaceCacheSize = nameof(RCWInterfaceCacheSize);

public const string HashMapSlotsPerBucket = nameof(HashMapSlotsPerBucket);
public const string HashMapValueMask = nameof(HashMapValueMask);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,23 @@ private TargetPointer GetSTAThread(Data.RCW rcw)
Data.CtxEntry ctxEntry = _target.ProcessedData.GetOrAdd<Data.CtxEntry>(ctxEntryPtr);
return ctxEntry.STAThread;
}

public IEnumerable<(TargetPointer MethodTable, TargetPointer Unknown)> GetRCWInterfaces(TargetPointer rcw)
{
Data.RCW rcwData = _target.ProcessedData.GetOrAdd<Data.RCW>(rcw);
foreach (Data.InterfaceEntry entry in rcwData.InterfaceEntries)
{
if (entry.Unknown != TargetPointer.Null)
{
yield return (entry.MethodTable, entry.Unknown);
}
}
}

public TargetPointer GetRCWContext(TargetPointer rcw)
{
Data.RCW rcwData = _target.ProcessedData.GetOrAdd<Data.RCW>(rcw);

return rcwData.CtxCookie;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.Diagnostics.DataContractReader.Data;

internal sealed class InterfaceEntry : IData<InterfaceEntry>
{
static InterfaceEntry IData<InterfaceEntry>.Create(Target target, TargetPointer address) => new InterfaceEntry(target, address);
public InterfaceEntry(Target target, TargetPointer address)
{
Target.TypeInfo type = target.GetTypeInfo(DataType.InterfaceEntry);

MethodTable = target.ReadPointer(address + (ulong)type.Fields[nameof(MethodTable)].Offset);
Unknown = target.ReadPointer(address + (ulong)type.Fields[nameof(Unknown)].Offset);
}

public TargetPointer MethodTable { get; init; }
public TargetPointer Unknown { get; init; }
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;

namespace Microsoft.Diagnostics.DataContractReader.Data;

internal sealed class RCW : IData<RCW>
Expand All @@ -15,11 +17,23 @@ public RCW(Target target, TargetPointer address)
Flags = target.Read<uint>(address + (ulong)type.Fields[nameof(Flags)].Offset);
CtxCookie = target.ReadPointer(address + (ulong)type.Fields[nameof(CtxCookie)].Offset);
CtxEntry = target.ReadPointer(address + (ulong)type.Fields[nameof(CtxEntry)].Offset);
TargetPointer interfaceEntriesAddr = address + (ulong)type.Fields[nameof(InterfaceEntries)].Offset;

uint cacheSize = target.ReadGlobal<uint>(Constants.Globals.RCWInterfaceCacheSize);
Target.TypeInfo entryTypeInfo = target.GetTypeInfo(DataType.InterfaceEntry);
uint entrySize = entryTypeInfo.Size!.Value;

for (uint i = 0; i < cacheSize; i++)
{
TargetPointer entryAddress = interfaceEntriesAddr + i * entrySize;
InterfaceEntries.Add(target.ProcessedData.GetOrAdd<Data.InterfaceEntry>(entryAddress));
}
}

public TargetPointer NextCleanupBucket { get; init; }
public TargetPointer NextRCW { get; init; }
public uint Flags { get; init; }
public TargetPointer CtxCookie { get; init; }
public TargetPointer CtxEntry { get; init; }
public List<Data.InterfaceEntry> InterfaceEntries { get; } = new List<Data.InterfaceEntry>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,13 @@ public struct DacpGcHeapAnalyzeData
public int heap_analyze_success; // BOOL
}

public struct DacpCOMInterfacePointerData
{
public ClrDataAddress methodTable;
public ClrDataAddress interfacePtr;
public ClrDataAddress comContext;
}

[GeneratedComInterface]
[Guid("286CA186-E763-4F61-9760-487D43AE4341")]
public unsafe partial interface ISOSEnum
Expand Down Expand Up @@ -753,7 +760,7 @@ public unsafe partial interface ISOSDacInterface
[PreserveSig]
int GetRCWData(ClrDataAddress addr, /*struct DacpRCWData */ void* data);
[PreserveSig]
int GetRCWInterfaces(ClrDataAddress rcw, uint count, /*struct DacpCOMInterfacePointerData*/ void* interfaces, uint* pNeeded);
int GetRCWInterfaces(ClrDataAddress rcw, uint count, [In, Out, MarshalUsing(CountElementName = nameof(count))] DacpCOMInterfacePointerData[]? interfaces, uint* pNeeded);
[PreserveSig]
int GetCCWData(ClrDataAddress ccw, /*struct DacpCCWData */ void* data);
[PreserveSig]
Expand Down Expand Up @@ -795,13 +802,6 @@ public unsafe partial interface ISOSDacInterface
int GetFailedAssemblyDisplayName(ClrDataAddress assembly, uint count, char* name, uint* pNeeded);
};

public struct DacpCOMInterfacePointerData
{
public ClrDataAddress methodTable;
public ClrDataAddress interfacePtr;
public ClrDataAddress comContext;
}

#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value
public struct DacpExceptionObjectData
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3390,8 +3390,93 @@ int ISOSDacInterface.GetPrivateBinPaths(ClrDataAddress appDomain, int count, cha
}
int ISOSDacInterface.GetRCWData(ClrDataAddress addr, void* data)
=> _legacyImpl is not null ? _legacyImpl.GetRCWData(addr, data) : HResults.E_NOTIMPL;
int ISOSDacInterface.GetRCWInterfaces(ClrDataAddress rcw, uint count, void* interfaces, uint* pNeeded)
=> _legacyImpl is not null ? _legacyImpl.GetRCWInterfaces(rcw, count, interfaces, pNeeded) : HResults.E_NOTIMPL;
int ISOSDacInterface.GetRCWInterfaces(ClrDataAddress rcw, uint count, [In, MarshalUsing(CountElementName = nameof(count)), Out] DacpCOMInterfacePointerData[]? interfaces, uint* pNeeded)
{
int hr = HResults.S_OK;
#if DEBUG
int numWritten = 0;
#endif
try
{
if (rcw == 0)
throw new ArgumentException();

TargetPointer rcwPtr = rcw.ToTargetPointer(_target);
IBuiltInCOM builtInCom = _target.Contracts.BuiltInCOM; // E_NOTIMPL if not defined (non-Windows)
IEnumerable<(TargetPointer MethodTable, TargetPointer Unknown)> entries = builtInCom.GetRCWInterfaces(rcwPtr);

if (interfaces == null)
{
if (pNeeded == null)
{
throw new ArgumentException();
}
else
{
*pNeeded = (uint)entries.Count();
#if DEBUG
numWritten = (int)*pNeeded;
#endif
}
}
else
{
TargetPointer ctxCookie = builtInCom.GetRCWContext(rcwPtr);
uint itemIndex = 0;
foreach (var (methodTable, unknown) in entries)
{
if (itemIndex >= count)
{
#if DEBUG
numWritten = (int)itemIndex;
#endif
throw new ArgumentException();
}

interfaces[itemIndex].methodTable = methodTable.ToClrDataAddress(_target);
interfaces[itemIndex].interfacePtr = unknown.ToClrDataAddress(_target);
interfaces[itemIndex].comContext = ctxCookie.ToClrDataAddress(_target);
itemIndex++;
}

if (pNeeded != null)
*pNeeded = itemIndex;
#if DEBUG
numWritten = (int)itemIndex;
#endif
}
}
catch (System.Exception ex)
{
hr = ex.HResult;
}

#if DEBUG
if (_legacyImpl is not null)
{
uint pNeededLocal = 0;
int hrLocal;
DacpCOMInterfacePointerData[]? interfacesLocal = interfaces != null ? new DacpCOMInterfacePointerData[count] : null;
hrLocal = _legacyImpl.GetRCWInterfaces(rcw, count, interfacesLocal, pNeeded == null && interfacesLocal == null ? null : &pNeededLocal);
Debug.ValidateHResult(hr, hrLocal);
if (hr == HResults.S_OK)
{
Debug.Assert(numWritten == pNeededLocal, $"cDAC: {numWritten}, DAC: {pNeededLocal}");
if (interfacesLocal is not null && interfaces is not null)
{
for (int i = 0; i < (int)pNeededLocal; i++)
{
Debug.Assert(interfaces[i].methodTable == interfacesLocal![i].methodTable, $"cDAC: {interfaces[i].methodTable:x}, DAC: {interfacesLocal[i].methodTable:x}");
Debug.Assert(interfaces[i].interfacePtr == interfacesLocal![i].interfacePtr, $"cDAC: {interfaces[i].interfacePtr:x}, DAC: {interfacesLocal[i].interfacePtr:x}");
Debug.Assert(interfaces[i].comContext == interfacesLocal![i].comContext, $"cDAC: {interfaces[i].comContext:x}, DAC: {interfacesLocal[i].comContext:x}");
}
}
}
}
#endif

return hr;
}
int ISOSDacInterface.GetRegisterName(int regName, uint count, char* buffer, uint* pNeeded)
{
int hr = HResults.S_OK;
Expand Down Expand Up @@ -4119,7 +4204,7 @@ int ISOSDacInterface.TraverseRCWCleanupList(ClrDataAddress cleanupListPtr, deleg
if (pCallback is null)
throw new ArgumentException();

Contracts.IBuiltInCOM contract = _target.Contracts.BuiltInCOM;
Contracts.IBuiltInCOM contract = _target.Contracts.BuiltInCOM; // E_NOTIMPL if not defined (non-Windows)
TargetPointer listPtr = cleanupListPtr.ToTargetPointer(_target);

cleanupInfos = contract.GetRCWCleanupList(listPtr);
Expand Down
Loading
Loading