File: SOSDacImpl.IXCLRDataProcess.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.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;
using System.Text;
using Microsoft.Diagnostics.DataContractReader.Contracts;
using Microsoft.Diagnostics.DataContractReader.Contracts.Extensions;

namespace Microsoft.Diagnostics.DataContractReader.Legacy;

/// <summary>
/// Implementation of IXCLRDataProcess* interfaces intended to be passed out to consumers
/// interacting with the DAC via those COM interfaces.
/// </summary>
public sealed unsafe partial class SOSDacImpl : IXCLRDataProcess, IXCLRDataProcess2
{
    int IXCLRDataProcess.Flush()
    {
        _target.Flush();

        // Flush is always propagated — it's cache management, not data retrieval.
        if (_legacyProcess is not null)
            return _legacyProcess.Flush();

        return HResults.S_OK;
    }

    int IXCLRDataProcess.StartEnumTasks(ulong* handle)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.StartEnumTasks(handle) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.EnumTask(ulong* handle, DacComNullableByRef<IXCLRDataTask> task)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.EnumTask(handle, task) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.EndEnumTasks(ulong handle)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.EndEnumTasks(handle) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.GetTaskByOSThreadID(uint osThreadID, DacComNullableByRef<IXCLRDataTask> task)
    {
        // Find the thread corresponding to the OS thread ID
        Contracts.IThread contract = _target.Contracts.Thread;
        TargetPointer thread = contract.GetThreadStoreData().FirstThread;
        TargetPointer matchingThread = TargetPointer.Null;
        while (thread != TargetPointer.Null)
        {
            Contracts.ThreadData threadData = contract.GetThreadData(thread);
            if (threadData.OSId.Value == osThreadID)
            {
                matchingThread = thread;
                break;
            }

            thread = threadData.NextThread;
        }

        if (matchingThread == TargetPointer.Null)
            return HResults.E_INVALIDARG;

        IXCLRDataTask? legacyTask = null;
        if (_legacyProcess is not null)
        {
            DacComNullableByRef<IXCLRDataTask> legacyTaskOut = new(isNullRef: false);
            int hr = _legacyProcess.GetTaskByOSThreadID(osThreadID, legacyTaskOut);
            if (hr < 0)
                return hr;
            legacyTask = legacyTaskOut.Interface;
        }

        task.Interface = new ClrDataTask(matchingThread, _target, legacyTask);
        return HResults.S_OK;
    }

    int IXCLRDataProcess.GetTaskByUniqueID(ulong taskID, DacComNullableByRef<IXCLRDataTask> task)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.GetTaskByUniqueID(taskID, task) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.GetFlags(uint* flags)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.GetFlags(flags) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.IsSameObject(IXCLRDataProcess* process)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.IsSameObject(process) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.GetManagedObject(DacComNullableByRef<IXCLRDataValue> value)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.GetManagedObject(value) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.GetDesiredExecutionState(uint* state)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.GetDesiredExecutionState(state) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.SetDesiredExecutionState(uint state)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.SetDesiredExecutionState(state) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.GetAddressType(ClrDataAddress address, /*CLRDataAddressType*/ uint* type)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.GetAddressType(address, type) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.GetRuntimeNameByAddress(
        ClrDataAddress address,
        uint flags,
        uint bufLen,
        uint* nameLen,
        char* nameBuf,
        ClrDataAddress* displacement)
    {
        int hr = HResults.S_OK;
        try
        {
            if (flags != 0)
                throw new ArgumentException("Flags are not supported", nameof(flags));

            TargetCodePointer codeAddr = address.ToTargetCodePointer(_target);

            // IsPossibleCodeAddress - validate the address is readable
            if (!_target.TryRead(codeAddr, out byte _))
                throw new ArgumentException("Address is not readable", nameof(address));

            IExecutionManager eman = _target.Contracts.ExecutionManager;
            string? resultName = null;

            // Try stub classification
            CodeKind codeKind = eman.GetCodeKind(codeAddr);
            if (codeKind == CodeKind.StubPrecode || codeKind == CodeKind.FixupPrecode)
            {
                IPrecodeStubs precodeStubs = _target.Contracts.PrecodeStubs;
                TargetPointer entryPoint = precodeStubs.GetPrecodeEntryPointFromInteriorAddress(codeAddr, codeKind == CodeKind.FixupPrecode);
                TargetPointer methodDesc = eman.NonVirtualEntry2MethodDesc(new TargetCodePointer(entryPoint.Value));
                if (methodDesc != TargetPointer.Null)
                {
                    if (displacement is not null)
                        *displacement = codeAddr.ToAddress(_target).Value - entryPoint;
                    IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem;
                    MethodDescHandle mdh = rts.GetMethodDescHandle(methodDesc);
                    StringBuilder sb = new StringBuilder();
                    TypeNameBuilder.AppendMethodInternal(_target, sb, mdh, TypeNameFormat.FormatSignature
                        | TypeNameFormat.FormatNamespace
                        | TypeNameFormat.FormatFullInst);
                    resultName = sb.ToString();
                }
            }
            if (resultName is null)
            {
                resultName = GetStubName(codeKind);
                if (resultName is not null && displacement is not null)
                    *displacement = 0;
            }

            // try aux symbols
            if (resultName is null && _target.Contracts.AuxiliarySymbols.TryGetAuxiliarySymbolName(address.ToTargetPointer(_target), out string? auxSymbolName))
            {
                resultName = auxSymbolName;
                if (displacement is not null)
                    *displacement = 0;
            }

            if (resultName is null)
            {
                throw new InvalidCastException();
            }

            OutputBufferHelpers.CopyStringToBuffer(nameBuf, bufLen, nameLen, resultName, out bool truncated);

            if (truncated)
                hr = HResults.S_FALSE;
        }
        catch (System.Exception ex)
        {
            hr = ex.HResult;
        }

#if DEBUG
        if (_legacyProcess is not null)
        {
            uint nameLenLocal = 0;
            char[] nameBufLocal = new char[bufLen > 0 ? bufLen : 1];
            ClrDataAddress displacementLocal = default;
            int hrLocal;
            fixed (char* pNameBufLocal = nameBufLocal)
            {
                hrLocal = _legacyProcess.GetRuntimeNameByAddress(
                    address, flags, bufLen,
                    nameLen is null ? null : &nameLenLocal,
                    nameBuf is null ? null : pNameBufLocal,
                    displacement is null ? null : &displacementLocal);
            }

            Debug.ValidateHResult(hr, hrLocal);
            if (hr == HResults.S_OK || hr == HResults.S_FALSE)
            {
                Debug.Assert(nameLen is null || *nameLen == nameLenLocal);
                if (nameBuf is not null)
                {
                    Debug.Assert(new ReadOnlySpan<char>(nameBuf, (int)nameLenLocal)
                        .SequenceEqual(nameBufLocal.AsSpan(0, (int)nameLenLocal)));
                }
                if (displacement is not null)
                {
                    Debug.Assert(*displacement == displacementLocal);
                }
            }
        }
#endif

        return hr;
    }

    private static string? GetStubName(Contracts.CodeKind codeKind)
    {
        if (codeKind == Contracts.CodeKind.Unknown || codeKind == Contracts.CodeKind.Jitted || codeKind == Contracts.CodeKind.ReadyToRun)
            return null;
        if (codeKind == Contracts.CodeKind.StubPrecode || codeKind == Contracts.CodeKind.FixupPrecode)
            return "Prestub";
        return codeKind.ToString();
    }

    int IXCLRDataProcess.StartEnumAppDomains(ulong* handle)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.StartEnumAppDomains(handle) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.EnumAppDomain(ulong* handle, /*IXCLRDataAppDomain*/ void** appDomain)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.EnumAppDomain(handle, appDomain) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.EndEnumAppDomains(ulong handle)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.EndEnumAppDomains(handle) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.GetAppDomainByUniqueID(ulong id, /*IXCLRDataAppDomain*/ void** appDomain)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.GetAppDomainByUniqueID(id, appDomain) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.StartEnumAssemblies(ulong* handle)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.StartEnumAssemblies(handle) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.EnumAssembly(ulong* handle, DacComNullableByRef<IXCLRDataAssembly> assembly)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.EnumAssembly(handle, assembly) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.EndEnumAssemblies(ulong handle)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.EndEnumAssemblies(handle) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.StartEnumModules(ulong* handle)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.StartEnumModules(handle) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.EnumModule(ulong* handle, DacComNullableByRef<IXCLRDataModule> mod)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.EnumModule(handle, mod) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.EndEnumModules(ulong handle)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.EndEnumModules(handle) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.GetModuleByAddress(ClrDataAddress address, DacComNullableByRef<IXCLRDataModule> mod)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.GetModuleByAddress(address, mod) : HResults.E_NOTIMPL;

    internal sealed class EnumMethodInstances : IEnum<MethodDescHandle>
    {
        private readonly Target _target;
        private readonly TargetPointer _mainMethodDesc;
        public readonly TargetPointer _appDomain;
        private readonly ILoader _loader;
        private readonly IRuntimeTypeSystem _rts;
        private readonly ICodeVersions _cv;
        public IEnumerator<MethodDescHandle> Enumerator { get; set; } = Enumerable.Empty<MethodDescHandle>().GetEnumerator();
        public TargetPointer LegacyHandle { get; set; } = TargetPointer.Null;

        public EnumMethodInstances(Target target, TargetPointer methodDesc, TargetPointer appDomain)
        {
            _target = target;
            _mainMethodDesc = methodDesc;
            if (appDomain == TargetPointer.Null)
            {
                TargetPointer appDomainPointer = _target.ReadGlobalPointer(Constants.Globals.AppDomain);
                _appDomain = _target.ReadPointer(appDomainPointer);
            }
            else
            {
                _appDomain = appDomain;
            }

            _loader = _target.Contracts.Loader;
            _rts = _target.Contracts.RuntimeTypeSystem;
            _cv = _target.Contracts.CodeVersions;
        }

        public int Start()
        {
            MethodDescHandle mainMD = _rts.GetMethodDescHandle(_mainMethodDesc);
            if (!HasClassOrMethodInstantiation(mainMD) && !_cv.HasNativeCodeAnyVersion(_mainMethodDesc))
            {
                return HResults.S_FALSE;
            }

            Enumerator = IterateMethodInstances().GetEnumerator();

            return HResults.S_OK;
        }

        private IEnumerable<MethodDescHandle> IterateMethodInstantiations(Contracts.ModuleHandle moduleHandle)
        {
            IEnumerable<TargetPointer> methodInstantiations = _loader.GetInstantiatedMethods(moduleHandle);

            foreach (TargetPointer methodPtr in methodInstantiations)
            {
                yield return _rts.GetMethodDescHandle(methodPtr);
            }
        }

        private IEnumerable<Contracts.TypeHandle> IterateTypeParams(Contracts.ModuleHandle moduleHandle)
        {
            IEnumerable<TargetPointer> typeParams = _loader.GetAvailableTypeParams(moduleHandle);

            foreach (TargetPointer type in typeParams)
            {
                yield return _rts.GetTypeHandle(type);
            }
        }

        private IEnumerable<Contracts.ModuleHandle> IterateModules()
        {
            ILoader loader = _target.Contracts.Loader;
            IEnumerable<Contracts.ModuleHandle> modules = loader.GetModuleHandles(
                _appDomain,
                AssemblyIterationFlags.IncludeLoaded | AssemblyIterationFlags.IncludeExecution);

            foreach (Contracts.ModuleHandle moduleHandle in modules)
            {
                yield return moduleHandle;
            }
        }

        private IEnumerable<MethodDescHandle> IterateMethodInstances()
        {
            /*
            There are 4 cases for method instances:
            1. Non-generic method on non-generic type  (There is 1 MethodDesc for the method (excluding unboxing stubs, and such)
            2. Generic method on non-generic type (There is a generic defining method + a instantiated method for each particular instantiation)
            3. Non-generic method on generic type (There is 1 method for each generic instance created + 1 for the method on the uninstantiated generic type)
            4. Generic method on Generic type (There are N generic defining methods where N is the number of generic instantiations of the generic type + 1 on the uninstantiated generic types + M different generic instances of the method)
            */

            MethodDescHandle mainMD = _rts.GetMethodDescHandle(_mainMethodDesc);

            if (!HasClassOrMethodInstantiation(mainMD))
            {
                // case 1
                // no method or class instantiation, then it's not generic.
                if (_cv.HasNativeCodeAnyVersion(_mainMethodDesc))
                {
                    yield return mainMD;
                }
                yield break;
            }

            TargetPointer mtAddr = _rts.GetMethodTable(mainMD);
            TypeHandle mainMT = _rts.GetTypeHandle(mtAddr);
            TargetPointer mainModule = _rts.GetModule(mainMT);
            uint mainMTToken = _rts.GetTypeDefToken(mainMT);
            uint mainMDToken = _rts.GetMethodToken(mainMD);
            ushort slotNum = _rts.GetSlotNumber(mainMD);

            if (HasMethodInstantiation(mainMD))
            {
                // case 2/4
                // 2 is trivial, 4 is covered because the defining method on a generic type is not instantiated
                foreach (Contracts.ModuleHandle moduleHandle in IterateModules())
                {
                    foreach (MethodDescHandle methodDesc in IterateMethodInstantiations(moduleHandle))
                    {
                        TypeHandle methodTypeHandle = _rts.GetTypeHandle(_rts.GetMethodTable(methodDesc));

                        if (mainModule != _rts.GetModule(methodTypeHandle)) continue;
                        if (mainMDToken != _rts.GetMethodToken(methodDesc)) continue;

                        if (_cv.HasNativeCodeAnyVersion(methodDesc.Address))
                        {
                            yield return methodDesc;
                        }
                    }
                }

                yield break;
            }

            if (HasClassInstantiation(mainMD))
            {
                // case 3
                // class instantiations are only interesting if the method is not generic
                foreach (Contracts.ModuleHandle moduleHandle in IterateModules())
                {
                    if (HasClassInstantiation(mainMD))
                    {
                        foreach (Contracts.TypeHandle typeParam in IterateTypeParams(moduleHandle))
                        {
                            uint typeParamToken = _rts.GetTypeDefToken(typeParam);

                            // not a MethodTable
                            if (typeParamToken == 0) continue;

                            // Check the class token
                            if (mainMTToken != typeParamToken) continue;

                            // Check the module is correct
                            if (mainModule != _rts.GetModule(typeParam)) continue;

                            TargetPointer cmt = _rts.GetCanonicalMethodTable(typeParam);
                            TypeHandle cmtHandle = _rts.GetTypeHandle(cmt);

                            TargetPointer methodDescAddr = _rts.GetMethodDescForSlot(cmtHandle, slotNum);
                            if (methodDescAddr == TargetPointer.Null) continue;
                            MethodDescHandle methodDesc = _rts.GetMethodDescHandle(methodDescAddr);

                            if (_cv.HasNativeCodeAnyVersion(methodDescAddr))
                            {
                                yield return methodDesc;
                            }
                        }
                    }
                }

                yield break;
            }

        }

        private bool HasClassOrMethodInstantiation(MethodDescHandle md)
        {
            return HasClassInstantiation(md) || HasMethodInstantiation(md);
        }

        private bool HasClassInstantiation(MethodDescHandle md)
        {
            IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem;

            TargetPointer mtAddr = rts.GetMethodTable(md);
            TypeHandle mt = rts.GetTypeHandle(mtAddr);
            return !rts.GetInstantiation(mt).IsEmpty;
        }

        private bool HasMethodInstantiation(MethodDescHandle md)
        {
            IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem;

            if (rts.IsGenericMethodDefinition(md)) return true;
            return !rts.GetGenericMethodInstantiation(md).IsEmpty;
        }
    }

    int IXCLRDataProcess.StartEnumMethodInstancesByAddress(ClrDataAddress address, IXCLRDataAppDomain? appDomain, ulong* handle)
    {
        int hr = HResults.S_FALSE;
        *handle = 0;

        // Start the legacy enumeration to keep it in sync with the cDAC enumeration.
        // EnumMethodInstanceByAddress passes the legacy method instance to ClrDataMethodInstance,
        // which delegates some operations to it.
        ulong handleLocal = default;
        int hrLocal = default;
        if (_legacyProcess is not null)
        {
            hrLocal = _legacyProcess.StartEnumMethodInstancesByAddress(address, appDomain, &handleLocal);
        }

        try
        {
            TargetCodePointer methodAddr = address.ToTargetCodePointer(_target);

            // ClrDataAccess::IsPossibleCodeAddress
            // Does a trivial check on the readability of the address
            bool isTriviallyReadable = _target.TryRead(methodAddr, out byte _);
            if (!isTriviallyReadable)
                throw new ArgumentException();

            IExecutionManager eman = _target.Contracts.ExecutionManager;
            if (eman.GetCodeBlockHandle(methodAddr) is CodeBlockHandle cbh &&
                eman.GetMethodDesc(cbh) is TargetPointer methodDesc)
            {
                EnumMethodInstances emi = new(_target, methodDesc, TargetPointer.Null);
                emi.LegacyHandle = handleLocal;

                hr = emi.Start();
                if (hr == HResults.S_OK)
                {
                    *handle = (ulong)((IEnum<MethodDescHandle>)emi).GetHandle();
                    // Legacy handle ownership transferred to emi — don't clean up below.
                    handleLocal = default;
                }
            }
        }
        catch (System.Exception ex)
        {
            hr = ex.HResult;
        }
        finally
        {
            // The legacy enumeration is started eagerly (before the cDAC try block) so
            // that EnumMethodInstanceByAddress can advance both enumerations in lockstep.
            // If the cDAC side fails to produce an enum (exception, no code block found,
            // or emi.Start() returns S_FALSE), the legacy handle would be orphaned because
            // the caller receives *handle == 0 and has no way to call End. Clean it up here.
            if (_legacyProcess is not null && handleLocal != default)
            {
                _legacyProcess.EndEnumMethodInstancesByAddress(handleLocal);
            }
        }

#if DEBUG
        if (_legacyProcess is not null)
        {
            Debug.ValidateHResult(hr, hrLocal);
        }
#endif
        return hr;
    }

    int IXCLRDataProcess.EnumMethodInstanceByAddress(ulong* handle, DacComNullableByRef<IXCLRDataMethodInstance> method)
    {
        int hr = HResults.S_OK;

        GCHandle gcHandle = GCHandle.FromIntPtr((IntPtr)(*handle));
        if (gcHandle.Target is not EnumMethodInstances emi) return HResults.E_INVALIDARG;

        // Advance the legacy enumeration to keep it in sync with the cDAC enumeration.
        // The legacy method instance is passed to ClrDataMethodInstance for delegation.
        IXCLRDataMethodInstance? legacyMethod = null;
        int hrLocal = default;
        if (_legacyProcess is not null)
        {
            ulong legacyHandle = emi.LegacyHandle;
            DacComNullableByRef<IXCLRDataMethodInstance> legacyMethodOut = new(isNullRef: false);
            hrLocal = _legacyProcess.EnumMethodInstanceByAddress(&legacyHandle, legacyMethodOut);
            legacyMethod = legacyMethodOut.Interface;
            emi.LegacyHandle = legacyHandle;
        }

        try
        {
            if (emi.Enumerator.MoveNext())
            {
                MethodDescHandle methodDesc = emi.Enumerator.Current;
                method.Interface = new ClrDataMethodInstance(_target, methodDesc, emi._appDomain, legacyMethod);
            }
            else
            {
                hr = HResults.S_FALSE;
            }
        }
        catch (System.Exception ex)
        {
            // The cDAC's IterateMethodInstances() implementation is incomplete compared
            // to the native DAC's EnumMethodInstances::Next(). The native DAC uses a
            // MethodIterator backed by AppDomain assembly iteration with EX_TRY/EX_CATCH
            // error handling around each step. The cDAC re-implements this with
            // IterateModules()/IterateMethodInstantiations()/IterateTypeParams() which
            // call into IRuntimeTypeSystem and ILoader contracts. These contract calls
            // (e.g. GetMethodTable, GetTypeHandle, GetMethodDescForSlot, GetModule,
            // GetTypeDefToken) can throw when encountering method descs or type handles
            // from assemblies/modules that the cDAC cannot fully process. This has been
            // observed for generic method instantiations (cases 2-4 in
            // IterateMethodInstances) in the SOS.WebApp3 integration test.
            //
            // Fall back to the legacy DAC result when available, otherwise propagate the error.
            if (_legacyProcess is not null)
            {
                hr = hrLocal;
                method.Interface = legacyMethod;
            }
            else
            {
                hr = ex.HResult;
            }
        }

#if DEBUG
        if (_legacyProcess is not null)
        {
            Debug.ValidateHResult(hr, hrLocal);
        }
#endif

        return hr;
    }

    int IXCLRDataProcess.EndEnumMethodInstancesByAddress(ulong handle)
    {
        int hr = HResults.S_OK;

        GCHandle gcHandle = GCHandle.FromIntPtr((IntPtr)handle);
        if (gcHandle.Target is not EnumMethodInstances emi) return HResults.E_INVALIDARG;
        gcHandle.Free();

        if (_legacyProcess != null && emi.LegacyHandle != TargetPointer.Null)
        {
            int hrLocal = _legacyProcess.EndEnumMethodInstancesByAddress(emi.LegacyHandle);
            if (hrLocal < 0)
                return hrLocal;
        }

        return hr;
    }

    int IXCLRDataProcess.GetDataByAddress(
        ClrDataAddress address,
        uint flags,
        IXCLRDataAppDomain? appDomain,
        IXCLRDataTask? tlsTask,
        uint bufLen,
        uint* nameLen,
        char* nameBuf,
        DacComNullableByRef<IXCLRDataValue> value,
        ClrDataAddress* displacement)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.GetDataByAddress(address, flags, appDomain, tlsTask, bufLen, nameLen, nameBuf, value, displacement) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.GetExceptionStateByExceptionRecord(EXCEPTION_RECORD64* record, DacComNullableByRef<IXCLRDataExceptionState> exState)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.GetExceptionStateByExceptionRecord(record, exState) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.TranslateExceptionRecordToNotification(EXCEPTION_RECORD64* record, [MarshalUsing(typeof(UniqueComInterfaceMarshaller<IXCLRDataExceptionNotification>))] IXCLRDataExceptionNotification notify)
    {
        // notify must be unique so that we can cast it to ComObject, call FinalRelease on it, and deterministically release.
        // This is required because notify is a stack allocated object created by the caller.
        // If the object goes out of scope before we dispose and finalize it, we can crash during GC.
        ComObject comObj = (ComObject)(object)notify;

        int hr = HResults.S_OK;
        try
        {
            Span<TargetPointer> exInfo = stackalloc TargetPointer[EXCEPTION_RECORD64.ExceptionMaximumParameters];
            for (int i = 0; i < EXCEPTION_RECORD64.ExceptionMaximumParameters; i++)
                exInfo[i] = new TargetPointer(record->ExceptionInformation[i]);

            INotifications notifications = _target.Contracts.Notifications;
            if (!notifications.TryParseNotification(exInfo, out NotificationData? notification))
                return HResults.E_INVALIDARG;

            switch (notification)
            {
                case ModuleLoadNotificationData moduleLoad:
                {
                    IXCLRDataModule? legacyModule = null;
                    if (_legacyImpl is not null)
                    {
                        DacComNullableByRef<IXCLRDataModule> legacyModuleOut = new(isNullRef: false);
                        _legacyImpl.GetModule(moduleLoad.ModuleAddress.ToClrDataAddress(_target), legacyModuleOut);
                        legacyModule = legacyModuleOut.Interface;
                    }

                    notify.OnModuleLoaded(new ClrDataModule(moduleLoad.ModuleAddress, _target, legacyModule));
                    break;
                }

                case ModuleUnloadNotificationData moduleUnload:
                {
                    IXCLRDataModule? legacyModule = null;
                    if (_legacyImpl is not null)
                    {
                        DacComNullableByRef<IXCLRDataModule> legacyModuleOut = new(isNullRef: false);
                        _legacyImpl.GetModule(moduleUnload.ModuleAddress.ToClrDataAddress(_target), legacyModuleOut);
                        legacyModule = legacyModuleOut.Interface;
                    }

                    notify.OnModuleUnloaded(new ClrDataModule(moduleUnload.ModuleAddress, _target, legacyModule));
                    break;
                }

                case JitNotificationData jit:
                {
                    TargetPointer appDomainPointer = _target.ReadGlobalPointer(Constants.Globals.AppDomain);
                    TargetPointer appDomain = _target.ReadPointer(appDomainPointer);

                    IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem;
                    MethodDescHandle methodDesc = rts.GetMethodDescHandle(jit.MethodDescAddress);

                    ClrDataMethodInstance methodInst = new(_target, methodDesc, appDomain, null);
                    notify.OnCodeGenerated(methodInst);
                    if (notify is IXCLRDataExceptionNotification5 notify5)
                    {
                        notify5.OnCodeGenerated2(methodInst, jit.NativeCodeAddress.ToClrDataAddress(_target));
                    }
                    break;
                }

                case ExceptionNotificationData exception:
                {
                    if (notify is IXCLRDataExceptionNotification2 notify2)
                    {
                        IThread thread = _target.Contracts.Thread;
                        Contracts.ThreadData threadData = thread.GetThreadData(exception.ThreadAddress);
                        TargetPointer thrownObjectHandle = thread.GetCurrentExceptionHandle(exception.ThreadAddress);
                        notify2.OnException(new ClrDataExceptionState(
                            _target,
                            exception.ThreadAddress,
                            (uint)CLRDataExceptionStateFlag.CLRDATA_EXCEPTION_DEFAULT,
                            thrownObjectHandle,
                            threadData.FirstNestedException,
                            null));
                    }
                    else
                        return HResults.E_INVALIDARG;
                    break;
                }

                case GcNotificationData gc:
                {
                    if (gc.IsSupportedEvent)
                    {
                        if (notify is IXCLRDataExceptionNotification3 notify3)
                        {
                            notify3.OnGcEvent(new GcEvtArgs
                            {
                                type = gc.EventData.EventType switch
                                {
                                    GcEventType.MarkEnd => GcEvtArgs.GcEvt_t.GC_MARK_END,
                                    _ => GcEvtArgs.GcEvt_t.GC_EVENT_TYPE_MAX,
                                },
                                condemnedGeneration = gc.EventData.CondemnedGeneration,
                            });
                        }
                        hr = HResults.S_OK;
                    }
                    else
                        hr = HResults.E_FAIL;
                    break;
                }

                case ExceptionCatcherEnterNotificationData exceptionCatcherEnter:
                {
                    if (notify is IXCLRDataExceptionNotification4 notify4)
                    {
                        TargetPointer appDomainPointer = _target.ReadGlobalPointer(Constants.Globals.AppDomain);
                        TargetPointer appDomain = _target.ReadPointer(appDomainPointer);
                        IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem;
                        MethodDescHandle methodDesc = rts.GetMethodDescHandle(exceptionCatcherEnter.MethodDescAddress);
                        notify4.ExceptionCatcherEnter(new ClrDataMethodInstance(_target, methodDesc, appDomain, null), exceptionCatcherEnter.NativeOffset);
                    }
                    break;
                }
            }
        }
        catch (System.Exception ex)
        {
            hr = ex.HResult;
        }
        finally
        {
            comObj.FinalRelease();
        }
        return hr;
    }

    int IXCLRDataProcess.Request(uint reqCode, uint inBufferSize, byte* inBuffer, uint outBufferSize, byte* outBuffer)
    {
        int hr = HResults.E_INVALIDARG;

        if (reqCode == (uint)CLRDataGeneralRequest.CLRDATA_REQUEST_REVISION)
        {
            if (inBufferSize == 0 && inBuffer is null && outBufferSize == sizeof(uint) && outBuffer is not null)
            {
                // Revision 10: Fixed DefaultCOMImpl::Release() to use pre-decrement (--mRef).
                // Consumers that previously compensated for the broken ref counting (e.g., ClrMD)
                // should check this revision to avoid double-freeing.
                *(uint*)outBuffer = 10;
                hr = HResults.S_OK;
            }
        }
        else
        {
            return LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.Request(reqCode, inBufferSize, inBuffer, outBufferSize, outBuffer) : HResults.E_NOTIMPL;
        }
#if DEBUG
        if (_legacyProcess is not null)
        {
            byte[] localBuffer = new byte[(int)outBufferSize];
            fixed (byte* localOutBuffer = localBuffer)
            {
                int hrLocal = _legacyProcess.Request(reqCode, inBufferSize, inBuffer, outBufferSize, localOutBuffer);
                Debug.ValidateHResult(hr, hrLocal);
                if (hr == HResults.S_OK && reqCode == (uint)CLRDataGeneralRequest.CLRDATA_REQUEST_REVISION)
                {
                    Debug.Assert(outBufferSize == sizeof(uint) && outBuffer is not null);
                    uint legacyRevision = *(uint*)localOutBuffer;
                    uint revision = *(uint*)outBuffer;
                    Debug.Assert(revision == legacyRevision);
                }
            }
        }
#endif
        return hr;
    }

    int IXCLRDataProcess.CreateMemoryValue(
        IXCLRDataAppDomain? appDomain,
        IXCLRDataTask? tlsTask,
        IXCLRDataTypeInstance? type,
        ClrDataAddress addr,
        DacComNullableByRef<IXCLRDataValue> value)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.CreateMemoryValue(appDomain, tlsTask, type, addr, value) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.SetAllTypeNotifications(IXCLRDataModule? mod, uint flags)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.SetAllTypeNotifications(mod, flags) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.SetAllCodeNotifications(IXCLRDataModule? mod, uint flags)
    {
        int hr = HResults.S_OK;
        try
        {
            if (!CodeNotificationFlagsConverter.IsValid(flags))
                throw new ArgumentException("Invalid code notification flags");

            TargetPointer moduleAddr = TargetPointer.Null;
            if (mod is not null)
            {
                if (mod is not ClrDataModule cdm)
                    throw new ArgumentException();
                moduleAddr = cdm.Address;
            }

            _target.Contracts.CodeNotifications.SetAllCodeNotifications(moduleAddr, CodeNotificationFlagsConverter.FromCom(flags));
        }
        catch (System.Exception ex)
        {
            hr = ex.HResult;
        }

        // No #if DEBUG validation: SetAllCodeNotifications is a write operation.
        // Both the cDAC and legacy DAC independently write to g_pNotificationTable.

        return hr;
    }

    int IXCLRDataProcess.GetTypeNotifications(
        uint numTokens,
        /*IXCLRDataModule*/ void** mods,
        IXCLRDataModule? singleMod,
        [In, MarshalUsing(CountElementName = nameof(numTokens))] /*mdTypeDef*/ uint[]? tokens,
        [In, Out, MarshalUsing(CountElementName = nameof(numTokens))] uint[]? flags)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.GetTypeNotifications(numTokens, mods, singleMod, tokens, flags) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.SetTypeNotifications(
        uint numTokens,
        /*IXCLRDataModule*/ void** mods,
        IXCLRDataModule? singleMod,
        [In, MarshalUsing(CountElementName = nameof(numTokens))] /*mdTypeDef*/ uint[]? tokens,
        [In, MarshalUsing(CountElementName = nameof(numTokens))] uint[]? flags,
        uint singleFlags)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.SetTypeNotifications(numTokens, mods, singleMod, tokens, flags, singleFlags) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.GetCodeNotifications(
        uint numTokens,
        /*IXCLRDataModule*/ void** mods,
        IXCLRDataModule? singleMod,
        [In, MarshalUsing(CountElementName = nameof(numTokens))] /*mdMethodDef*/ uint[]? tokens,
        [In, Out, MarshalUsing(CountElementName = nameof(numTokens))] uint[]? flags)
    {
        int hr = HResults.S_OK;
        ICodeNotifications codeNotif = _target.Contracts.CodeNotifications;

        try
        {
            // Match legacy DAC (daccess.cpp ClrDataAccess::GetCodeNotifications):
            // tokens and flags are both required; exactly one of mods/singleMod must be non-null.
            if (tokens is null || flags is null ||
                (mods is null && singleMod is null) ||
                (mods is not null && singleMod is not null))
                throw new ArgumentException();

            TargetPointer moduleAddr = TargetPointer.Null;
            if (singleMod is not null)
            {
                if (singleMod is not ClrDataModule singleCdm)
                    throw new ArgumentException();
                moduleAddr = singleCdm.Address;
            }

            for (uint i = 0; i < numTokens; i++)
            {
                if (singleMod is null)
                    moduleAddr = GetModuleAddress(mods[i]);

                flags[i] = CodeNotificationFlagsConverter.ToCom(codeNotif.GetCodeNotification(moduleAddr, tokens[i]));
            }
        }
        catch (System.Exception ex)
        {
            hr = ex.HResult;
        }

        // No #if DEBUG validation: GetCodeNotifications is a read, but both cDAC and
        // legacy DAC allocate the table on-demand when called, which would cause
        // dual-allocation. Validation is safe at a higher layer when a dump is used.

        return hr;
    }

    int IXCLRDataProcess.SetCodeNotifications(
        uint numTokens,
        /*IXCLRDataModule*/ void** mods,
        IXCLRDataModule? singleMod,
        [In, MarshalUsing(CountElementName = nameof(numTokens))] /*mdMethodDef */ uint[]? tokens,
        [In, MarshalUsing(CountElementName = nameof(numTokens))] uint[]? flags,
        uint singleFlags)
    {
        // Behavior difference from the legacy DAC: the legacy DAC performs an upfront
        // capacity check (numTokens > table size returns E_OUTOFMEMORY with no writes
        // performed). The cDAC's CodeNotifications contract does not expose a capacity
        // check, so this batch wrapper writes entries one at a time. If the in-target
        // table fills up part-way through, entries written before the overflow remain
        // set and the first failing per-entry SetCodeNotification surfaces a COMException
        // with HResult == E_FAIL, which is mapped to the returned hr below. Callers that
        // depend on atomic batch semantics should size their batches conservatively.
        int hr = HResults.S_OK;

        try
        {
            if (tokens is null ||
                (mods is null && singleMod is null) ||
                (mods is not null && singleMod is not null))
                throw new ArgumentException();

            // Validate flags.
            if (flags is not null)
            {
                for (uint check = 0; check < numTokens; check++)
                {
                    if (!CodeNotificationFlagsConverter.IsValid(flags[check]))
                        throw new ArgumentException("Invalid code notification flags");
                }
            }
            else if (!CodeNotificationFlagsConverter.IsValid(singleFlags))
            {
                throw new ArgumentException("Invalid code notification flags");
            }

            TargetPointer moduleAddr = TargetPointer.Null;
            if (singleMod is not null)
            {
                if (singleMod is not ClrDataModule singleCdm)
                    throw new ArgumentException();
                moduleAddr = singleCdm.Address;
            }

            for (uint i = 0; i < numTokens; i++)
            {
                if (singleMod is null)
                    moduleAddr = GetModuleAddress(mods[i]);

                uint f = flags is not null ? flags[i] : singleFlags;
                _target.Contracts.CodeNotifications.SetCodeNotification(moduleAddr, tokens[i], CodeNotificationFlagsConverter.FromCom(f));
            }
        }
        catch (System.Exception ex)
        {
            hr = ex.HResult;
        }

        // No #if DEBUG validation: SetCodeNotifications is a write operation.
        // Both the cDAC and legacy DAC independently write to g_pNotificationTable.

        return hr;
    }

    int IXCLRDataProcess.GetOtherNotificationFlags(uint* flags)
    {
        int hr = HResults.S_OK;
        try
        {
            *flags = _target.Read<uint>(_target.ReadGlobalPointer(Constants.Globals.DacNotificationFlags));
        }
        catch (System.Exception ex)
        {
            hr = ex.HResult;
        }
#if DEBUG
        if (_legacyProcess is not null)
        {
            uint flagsLocal;
            int hrLocal = _legacyProcess.GetOtherNotificationFlags(&flagsLocal);
            Debug.ValidateHResult(hr, hrLocal);
            Debug.Assert(*flags == flagsLocal);
        }
#endif
        return hr;
    }
    int IXCLRDataProcess.SetOtherNotificationFlags(uint flags)
    {
        int hr = HResults.S_OK;
        try
        {
            if ((flags & ~((uint)CLRDataOtherNotifyFlag.CLRDATA_NOTIFY_ON_MODULE_LOAD |
                           (uint)CLRDataOtherNotifyFlag.CLRDATA_NOTIFY_ON_MODULE_UNLOAD |
                           (uint)CLRDataOtherNotifyFlag.CLRDATA_NOTIFY_ON_EXCEPTION |
                           (uint)CLRDataOtherNotifyFlag.CLRDATA_NOTIFY_ON_EXCEPTION_CATCH_ENTER)) != 0)
            {
                hr = HResults.E_INVALIDARG;
            }
            else
            {
                TargetPointer dacNotificationFlags = _target.ReadGlobalPointer(Constants.Globals.DacNotificationFlags);
                _target.Write<uint>(dacNotificationFlags, flags);
            }
        }
        catch (System.Exception ex)
        {
            hr = ex.HResult;
        }
#if DEBUG
        if (_legacyProcess is not null)
        {
            int hrLocal = default;
            uint flagsLocal = default;
            // have to read the flags like this and not with GetOtherNotificationFlags
            // because the legacy DAC cache will not be updated when we set the flags in cDAC
            // so we need to verify without using the legacy DAC
            hrLocal = HResults.S_OK;
            try
            {
                flagsLocal = _target.Read<uint>(_target.ReadGlobalPointer(Constants.Globals.DacNotificationFlags));
            }
            catch (System.Exception ex)
            {
                hrLocal = ex.HResult;
            }
            Debug.ValidateHResult(hr, hrLocal);
            if (hr == HResults.S_OK)
            {
                Debug.Assert(flags == flagsLocal);
            }
            // update the DAC cache
            _legacyProcess.SetOtherNotificationFlags(flags);
        }
#endif
        return hr;
    }

    int IXCLRDataProcess.StartEnumMethodDefinitionsByAddress(ClrDataAddress address, ulong* handle)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.StartEnumMethodDefinitionsByAddress(address, handle) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.EnumMethodDefinitionByAddress(ulong* handle, DacComNullableByRef<IXCLRDataMethodDefinition> method)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.EnumMethodDefinitionByAddress(handle, method) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.EndEnumMethodDefinitionsByAddress(ulong handle)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.EndEnumMethodDefinitionsByAddress(handle) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.FollowStub(
        uint inFlags,
        ClrDataAddress inAddr,
        /*struct CLRDATA_FOLLOW_STUB_BUFFER*/ void* inBuffer,
        ClrDataAddress* outAddr,
        /*struct CLRDATA_FOLLOW_STUB_BUFFER*/ void* outBuffer,
        uint* outFlags)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.FollowStub(inFlags, inAddr, inBuffer, outAddr, outBuffer, outFlags) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.FollowStub2(
        IXCLRDataTask? task,
        uint inFlags,
        ClrDataAddress inAddr,
        /*struct CLRDATA_FOLLOW_STUB_BUFFER*/ void* inBuffer,
        ClrDataAddress* outAddr,
        /*struct CLRDATA_FOLLOW_STUB_BUFFER*/ void* outBuffer,
        uint* outFlags)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.FollowStub2(task, inFlags, inAddr, inBuffer, outAddr, outBuffer, outFlags) : HResults.E_NOTIMPL;

    int IXCLRDataProcess.DumpNativeImage(
        ClrDataAddress loadedBase,
        char* name,
        /*IXCLRDataDisplay*/ void* display,
        /*IXCLRLibrarySupport*/ void* libSupport,
        /*IXCLRDisassemblySupport*/ void* dis)
        => LegacyFallbackHelper.CanFallback() && _legacyProcess is not null ? _legacyProcess.DumpNativeImage(loadedBase, name, display, libSupport, dis) : HResults.E_NOTIMPL;

    int IXCLRDataProcess2.GetGcNotification(GcEvtArgs* gcEvtArgs)
    {
        int hr = HResults.E_NOTIMPL;
#if DEBUG
        if (_legacyProcess2 is not null)
        {
            int hrLocal = _legacyProcess2.GetGcNotification(gcEvtArgs);
            Debug.ValidateHResult(hr, hrLocal);
        }
#endif
        return hr;
    }

    int IXCLRDataProcess2.SetGcNotification(GcEvtArgs gcEvtArgs)
    {
        int hr = HResults.S_OK;
        try
        {
            if (gcEvtArgs.type >= GcEvtArgs.GcEvt_t.GC_EVENT_TYPE_MAX)
                throw new ArgumentException();
            _target.Contracts.Notifications.SetGcNotification(gcEvtArgs.condemnedGeneration);
        }
        catch (System.Exception ex)
        {
            hr = ex.HResult;
        }
#if DEBUG
        if (_legacyProcess2 is not null)
        {
            // update the DAC cache
            int hrLocal = _legacyProcess2.SetGcNotification(gcEvtArgs);
            Debug.ValidateHResult(hr, hrLocal);
        }
#endif
        return hr;
    }

    private static TargetPointer GetModuleAddress(void* comModulePtr)
    {
        if (System.Runtime.InteropServices.ComWrappers.TryGetObject((nint)comModulePtr, out object? obj))
        {
            if (obj is ClrDataModule cdm)
                return cdm.Address;
        }
        throw new ArgumentException("Could not resolve module address from COM pointer");
    }
}