File: DebugExtensions.cs
Web Access
Project: src\src\runtime\src\native\managed\cdac\Microsoft.Diagnostics.DataContractReader.Legacy\Microsoft.Diagnostics.DataContractReader.Legacy.csproj (Microsoft.Diagnostics.DataContractReader.Legacy)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;

namespace Microsoft.Diagnostics.DataContractReader.Legacy;

internal enum HResultValidationMode
{
    /// <summary>
    /// HRESULTs must match exactly.
    /// </summary>
    Exact,

    /// <summary>
    /// Success HRESULTs must match exactly, but any two failing HRESULTs (negative values) are considered equivalent.
    /// This is the recommended default because the cDAC and native DAC may use different exception types for the
    /// same invalid input (e.g., InvalidOperationException vs E_INVALIDARG), producing different failing HRESULTs.
    /// </summary>
    AllowDivergentFailures,

    /// <summary>
    /// Like <see cref="AllowDivergentFailures"/>, but also allows the cDAC to succeed when the native DAC fails.
    /// The native DAC's MetaSig constructor traverses MethodDesc -> Module -> MDImport -> signature blob via
    /// DAC host pointers, and any intermediate read can throw under EX_TRY on certain frames (e.g., EH dispatch).
    /// The cDAC reads the same metadata through contracts (EcmaMetadata -> PEImage -> metadata blob) which uses
    /// a different pointer traversal path that doesn't hit the same failure.
    /// </summary>
    AllowCdacSuccess,
}

internal static class DebugExtensions
{
#if DEBUG
    private const int LastExceptionRingSize = 4;

    [ThreadStatic]
    private static Exception?[]? _debugLastExceptions;

    [ThreadStatic]
    private static int _debugLastExceptionsNextIndex;

    static DebugExtensions()
    {
        AppDomain.CurrentDomain.FirstChanceException += static (_, e) =>
        {
            Exception?[] ring = _debugLastExceptions ??= new Exception?[LastExceptionRingSize];
            ring[_debugLastExceptionsNextIndex] = e.Exception;
            _debugLastExceptionsNextIndex = (_debugLastExceptionsNextIndex + 1) % LastExceptionRingSize;
        };
    }

    private static Exception? FindMatchingException(int hr)
    {
        Exception?[]? ring = _debugLastExceptions;
        if (ring is null)
            return null;

        // Walk from newest to oldest.
        int next = _debugLastExceptionsNextIndex;
        for (int i = 0; i < LastExceptionRingSize; i++)
        {
            int idx = (next - 1 - i + LastExceptionRingSize) % LastExceptionRingSize;
            Exception? ex = ring[idx];
            if (ex is not null && ex.HResult == hr)
                return ex;
        }

        return null;
    }

    private static void ClearLastExceptions()
    {
        Exception?[]? ring = _debugLastExceptions;
        if (ring is not null)
            Array.Clear(ring);
        _debugLastExceptionsNextIndex = 0;
    }
#endif

    extension(Debug)
    {
        [Conditional("DEBUG")]
        internal static void ValidateHResult(
            int cdacHr,
            int dacHr,
            HResultValidationMode mode = HResultValidationMode.AllowDivergentFailures,
            [CallerFilePath] string? filePath = null,
            [CallerLineNumber] int lineNumber = 0)
        {
            bool match = mode switch
            {
                HResultValidationMode.Exact => cdacHr == dacHr,
                HResultValidationMode.AllowDivergentFailures => cdacHr == dacHr || (cdacHr < 0 && dacHr < 0),
                HResultValidationMode.AllowCdacSuccess => cdacHr == dacHr || (cdacHr < 0 && dacHr < 0) || (cdacHr >= 0 && dacHr < 0),
                _ => cdacHr == dacHr,
            };

            if (!match)
            {
                string message = $"HResult mismatch - cDAC: 0x{unchecked((uint)cdacHr):X8}, DAC: 0x{unchecked((uint)dacHr):X8} ({Path.GetFileName(filePath)}:{lineNumber})";
#if DEBUG
                if (cdacHr < 0)
                {
                    Exception? ex = FindMatchingException(cdacHr);
                    if (ex is not null)
                    {
                        message += $"{Environment.NewLine}---- cDAC exception ----{Environment.NewLine}{ex}{Environment.NewLine}---- end cDAC exception ----";
                    }
                }
#endif
                Debug.Assert(false, message);
            }

#if DEBUG
            ClearLastExceptions();
#endif
        }
    }
}