src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/README.md
This project contains SOSDacImpl, which implements the ISOSDacInterface* and
IXCLRDataProcess COM-style APIs by delegating to the cDAC contract layer.
When a method currently delegates to _legacyImpl (returning E_NOTIMPL when null),
replace it with a cDAC implementation following this pattern:
int ISOSDacInterface8.ExampleMethod(uint* pResult)
{
int hr = HResults.S_OK;
try
{
// 1. Validate pointer arguments inside the try block
if (pResult is null)
throw new ArgumentException();
// 2. Get the relevant contract and call it
IGC gc = _target.Contracts.GC;
*pResult = gc.SomeMethod();
}
catch (System.Exception ex)
{
hr = ex.HResult;
}
// 3. Cross-validate with legacy DAC in debug builds
#if DEBUG
if (_legacyImpl8 is not null)
{
uint resultLocal;
int hrLocal = _legacyImpl8.ExampleMethod(&resultLocal);
Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}");
if (hr == HResults.S_OK)
{
Debug.Assert(*pResult == resultLocal);
}
}
#endif
return hr;
}
int HResult codes, not exceptions.
Use HResults.S_OK, HResults.S_FALSE, HResults.E_INVALIDARG, etc.ArgumentException. The catch block converts this to an HResult code.ex.HResult. When the native DAC has an explicit
readability check (e.g., ptr.IsValid() or DACGetMethodTableFromObjectPointer
returning NULL), catch VirtualReadException specifically and return the same
HResult the native DAC returns (typically E_INVALIDARG). Avoid catching all
exceptions and mapping to a single HRESULT, as this can mask unrelated bugs.#if DEBUG, call the legacy implementation (if
available) and assert the results match. This catches discrepancies during testing.Some cDAC methods create child objects (e.g., ClrDataMethodInstance,
ClrDataFrame) that delegate certain operations to a legacy counterpart. This is
a temporary implementation workaround to let us create the cDAC incrementally that
should be removed before cDAC ships to customers. In these cases, the legacy call
that obtains the counterpart must be outside #if DEBUG, because the result is
used functionally, not just for validation.
For example, EnumMethodInstanceByAddress passes legacyMethod to
ClrDataMethodInstance, which delegates GetTokenAndScope and other calls to it.
If the legacy enumeration only runs inside #if DEBUG, those delegated calls fail
in Release builds.
Rule of thumb: if a legacy call's result is stored and passed to another
object, keep it outside #if DEBUG. Only the assertion that compares
HResults/values belongs inside #if DEBUG.
Several ISOSDacInterface8 methods use a two-call pattern where the caller first
queries the needed buffer size, then calls again with a sufficiently large buffer:
int GetSomeTable(uint count, Data* buffer, uint* pNeeded)
The protocol is:
*pNeeded to the required count (if pNeeded is not null).count > 0 && buffer is null: throw ArgumentException.count < needed: return S_FALSE (buffer too small, but *pNeeded is set).count >= needed: populate buffer and return S_OK.This matches the native implementation in src/coreclr/debug/daccess/request.cpp.
TargetPointer → ClrDataAddress: use pointer.ToClrDataAddress(_target).
On 32-bit targets, this sign-extends the value (e.g., 0xAA000000 becomes
0xFFFFFFFF_AA000000). This matches native DAC behavior.ClrDataAddress → TargetPointer: use address.ToTargetPointer(_target).Both are defined in ConversionExtensions.cs.