File: Contracts\CallingConvention\CallingConvention_1.cs
Web Access
Project: src\runtime\src\native\managed\cdac\Microsoft.Diagnostics.DataContractReader.Contracts\Microsoft.Diagnostics.DataContractReader.Contracts.csproj (Microsoft.Diagnostics.DataContractReader.Contracts)
// 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.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using Internal.CallingConvention;
using Internal.CorConstants;
using Internal.JitInterface;
using Microsoft.Diagnostics.DataContractReader.Contracts.CallingConventionHelpers;
using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers;
using Microsoft.Diagnostics.DataContractReader.SignatureHelpers;

using CallingConventions = Internal.CallingConvention.CallingConventions;
using CdacCorElementType = Microsoft.Diagnostics.DataContractReader.Contracts.CorElementType;

namespace Microsoft.Diagnostics.DataContractReader.Contracts;

internal sealed class CallingConvention_1 : ICallingConvention
{
    private readonly Target _target;
    private readonly Dictionary<ModuleHandle, SignatureTypeInfoProvider> _signatureTypeInfoProviders = [];

    internal CallingConvention_1(Target target)
    {
        _target = target;
    }

    public void Flush(FlushScope scope)
    {
        _signatureTypeInfoProviders.Clear();
    }

    public bool TryComputeArgGCRefMapBlob(MethodDescHandle methodDesc, out byte[] blob)
    {
        try
        {
            byte[]? result = ComputeArgGCRefMapBlobCore(methodDesc);
            if (result is null)
            {
                blob = [];
                return false;
            }
            blob = result;
            return true;
        }
        catch (NotImplementedException)
        {
            // Any unported ABI path, including NIEs from GetArgumentLayout,
            // maps to a clean decline (false).
            blob = [];
            return false;
        }
    }

    private MethodSignature<SignatureTypeInfo> DecodeMethodSignature(MethodDescHandle methodDesc)
    {
        IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem;
        ITypeHandle owningTypeHandle = rts.GetTypeHandle(rts.GetMethodTable(methodDesc));
        ModuleHandle moduleHandle = GetModuleHandle(owningTypeHandle);
        MetadataReader metadataReader = GetMetadataReader(moduleHandle);

        if (!rts.TryGetMethodSignature(methodDesc, out ReadOnlySpan<byte> signature))
        {
            throw new InvalidOperationException("Method has no signature.");
        }

        SignatureTypeInfoProvider provider = GetSignatureTypeInfoProvider(moduleHandle);
        SignatureTypeContext context = new(methodDesc, provider.FromExactType(owningTypeHandle));
        RuntimeSignatureDecoder<SignatureTypeInfo, SignatureTypeContext> decoder =
            new(provider, _target, metadataReader, context);

        unsafe
        {
            fixed (byte* signaturePointer = signature)
            {
                BlobReader blobReader = new(signaturePointer, signature.Length);
                return decoder.DecodeMethodSignature(ref blobReader);
            }
        }
    }

    internal SignatureTypeInfo GetFieldTypeInfo(TargetPointer fieldDesc, SignatureTypeInfo owningType)
    {
        IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem;
        ITypeHandle enclosingType = rts.GetTypeHandle(rts.GetMTOfEnclosingClass(fieldDesc));
        ModuleHandle moduleHandle = GetModuleHandle(enclosingType);
        MetadataReader metadataReader = GetMetadataReader(moduleHandle);

        uint memberDef = rts.GetFieldDescMemberDef(fieldDesc);
        FieldDefinitionHandle fieldDefinitionHandle = (FieldDefinitionHandle)MetadataTokens.Handle((int)memberDef);
        FieldDefinition fieldDefinition = metadataReader.GetFieldDefinition(fieldDefinitionHandle);

        SignatureTypeContext context = new(Method: null, owningType);
        RuntimeSignatureDecoder<SignatureTypeInfo, SignatureTypeContext> decoder =
            new(GetSignatureTypeInfoProvider(moduleHandle), _target, metadataReader, context);
        BlobReader blobReader = metadataReader.GetBlobReader(fieldDefinition.Signature);
        return decoder.DecodeFieldSignature(ref blobReader);
    }

    private SignatureTypeInfoProvider GetSignatureTypeInfoProvider(ModuleHandle moduleHandle)
    {
        if (_signatureTypeInfoProviders.TryGetValue(moduleHandle, out SignatureTypeInfoProvider? provider))
        {
            return provider;
        }

        provider = new SignatureTypeInfoProvider(_target, moduleHandle);
        _signatureTypeInfoProviders[moduleHandle] = provider;
        return provider;
    }

    private ModuleHandle GetModuleHandle(ITypeHandle typeHandle)
    {
        TargetPointer modulePointer = _target.Contracts.RuntimeTypeSystem.GetModule(typeHandle);
        if (modulePointer == TargetPointer.Null)
        {
            throw new InvalidOperationException("Type has no module.");
        }

        return _target.Contracts.Loader.GetModuleHandleFromModulePtr(modulePointer);
    }

    private MetadataReader GetMetadataReader(ModuleHandle moduleHandle)
        => _target.Contracts.EcmaMetadata.GetMetadata(moduleHandle)
            ?? throw new InvalidOperationException("Cannot read metadata for module.");

    // Result of GetArgumentLayout: a single ArgIterator walk produces the
    // per-argument locations the encoder iterates plus the x86 callee-pop
    // stack-byte count it needs for the WriteStackPop prefix. Bundled so the
    // implementation builds ArgIterator once per method instead of twice.
    private readonly record struct ArgumentLayout(
        IReadOnlyList<ArgumentLocation> Arguments,
        uint CbStackPop);

    private ArgumentLayout GetArgumentLayout(MethodDescHandle methodDesc)
    {
        IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem;
        IRuntimeInfo runtimeInfo = _target.Contracts.RuntimeInfo;

        MethodSignature<SignatureTypeInfo> methodSig = DecodeMethodSignature(methodDesc);

        bool isVarArg = methodSig.Header.CallingConvention is SignatureCallingConvention.VarArgs;

        bool hasThis = methodSig.Header.IsInstance;
        bool requiresInstArg = false;
        bool isAsync = false;
        try
        {
            GenericContextLoc ctxLoc = rts.GetGenericContextLoc(methodDesc);
            requiresInstArg = ctxLoc is GenericContextLoc.InstArgMethodDesc or GenericContextLoc.InstArgMethodTable;
            isAsync = rts.IsAsyncMethod(methodDesc);
        }
        catch
        {
        }

        CdacTypeHandle[] parameterTypes = new CdacTypeHandle[methodSig.ParameterTypes.Length];
        for (int i = 0; i < parameterTypes.Length; i++)
        {
            parameterTypes[i] = new CdacTypeHandle(methodSig.ParameterTypes[i], _target, this);
        }

        CdacTypeHandle returnType = new CdacTypeHandle(methodSig.ReturnType, _target, this);

        TransitionBlock transitionBlock = BuildTransitionBlock(runtimeInfo);

        CallingConventions callingConventions = hasThis
            ? CallingConventions.ManagedInstance
            : CallingConventions.ManagedStatic;

        ArgIteratorData<CdacTypeHandle> argIteratorData = new ArgIteratorData<CdacTypeHandle>(
            hasThis, isVarArg: isVarArg, parameterTypes, returnType);

        bool isWindows = runtimeInfo.GetTargetOperatingSystem() == RuntimeInfoOperatingSystem.Windows;

        ArgIterator<CdacTypeHandle> argit = new ArgIterator<CdacTypeHandle>(
            transitionBlock,
            argIteratorData,
            callingConventions,
            hasParamType: requiresInstArg,
            hasAsyncContinuation: isAsync,
            extraFunctionPointerArg: false,
            forcedByRefParams: new bool[parameterTypes.Length],
            skipFirstArg: false,
            extraObjectFirstArg: false,
            isWindows: isWindows,
            objectTypeHandle: GetObjectTypeHandle(rts),
            intPtrTypeHandle: GetIntPtrTypeHandle(rts));

        List<ArgumentLocation> arguments = new();

        if (hasThis)
        {
            TargetPointer methodTablePtr = rts.GetMethodTable(methodDesc);
            ITypeHandle owningType = rts.GetTypeHandle(methodTablePtr);
            bool isValueTypeThis = rts.IsValueType(owningType) && !rts.IsUnboxingStub(methodDesc);

            arguments.Add(new ArgumentLocation
            {
                Offset = transitionBlock.ThisOffset,
                ElementType = isValueTypeThis ? CdacCorElementType.ValueType : CdacCorElementType.Class,
                IsThis = true,
                IsValueTypeThis = isValueTypeThis,
            });
        }

        if (argit.HasParamType)
        {
            arguments.Add(new ArgumentLocation
            {
                Offset = argit.GetParamTypeArgOffset(),
                ElementType = CdacCorElementType.I,
                IsParamType = true,
            });
        }

        if (argit.HasAsyncContinuation)
        {
            arguments.Add(new ArgumentLocation
            {
                Offset = argit.GetAsyncContinuationArgOffset(),
                ElementType = CdacCorElementType.Object,
            });
        }

        // VarArgs: mirror the runtime's FakeGcScanRoots short-circuit -- emit
        // the VASigCookie slot and stop. The variadic tail is reported via
        // the cookie's signature at GC scan time, not via this contract.
        // CbStackPop is 0 for VarArgs on x86 (caller cleans up), and
        // argit.CbStackPop() is unsafe to call on the VarArgs-configured
        // iterator -- short-circuit both here.
        if (isVarArg)
        {
            arguments.Add(new ArgumentLocation
            {
                Offset = argit.GetVASigCookieOffset(),
                ElementType = CdacCorElementType.I,
                IsVASigCookie = true,
            });
            return new ArgumentLayout(arguments, CbStackPop: 0);
        }

        int argIndex = 0;
        int argOffset;
        while ((argOffset = argit.GetNextOffset()) != TransitionBlock.InvalidOffset)
        {
            if (argIndex < parameterTypes.Length)
            {
                SignatureTypeInfo typeInfo = methodSig.ParameterTypes[argIndex];
                CdacCorElementType elemType = typeInfo.ElementType;

                if (argOffset == TransitionBlock.StructInRegsOffset)
                {
                    // SystemV-AMD64 struct-in-registers.
                    SYSTEMV_AMD64_CORINFO_STRUCT_REG_PASSING_DESCRIPTOR sysvDesc;
                    parameterTypes[argIndex].GetSystemVAmd64PassStructInRegisterDescriptor(out sysvDesc);
                    ArgLocDesc loc = argit.GetArgLoc(argOffset) ?? throw new InvalidOperationException("ArgIterator returned null ArgLocDesc for struct-in-registers argument");

                    arguments.Add(new ArgumentLocation
                    {
                        Offset = argOffset,
                        ElementType = elemType,
                        TypeInfo = typeInfo,
                        IsStructPassedInRegs = true,
                        SysVEightByteDescriptor = sysvDesc,
                        SysVIdxGenReg = loc.m_idxGenReg,
                    });
                    argIndex++;
                    continue;
                }

                bool passedByRef = elemType == CdacCorElementType.ValueType
                    && transitionBlock.IsArgPassedByRef(parameterTypes[argIndex]);

                // Detect ByRefLike value types (Span<T>, ReadOnlySpan<T>,
                // ref structs in general). The runtime emits one INTERIOR
                // token per managed-pointer field inside the unboxed struct
                // via ByRefPointerOffsetsReporter, in addition to any REF
                // tokens from GCDesc. SignatureTypeInfo retains the generic
                // definition when an exact constructed type is unavailable,
                // which is sufficient for inspecting byref-like field shape.
                bool isByRefLikeStruct = false;
                if (elemType == CdacCorElementType.ValueType && !passedByRef)
                {
                    ITypeHandle? probe = typeInfo.ExactTypeHandle ?? typeInfo.GenericTypeDefinition;
                    if (probe is not null)
                    {
                        isByRefLikeStruct = rts.IsByRefLike(probe);
                    }
                }

                arguments.Add(new ArgumentLocation
                {
                    Offset = argOffset,
                    ElementType = elemType,
                    TypeInfo = typeInfo,
                    IsPassedByRef = passedByRef,
                    IsByRefLikeStruct = isByRefLikeStruct,
                });
            }
            argIndex++;
        }

        // CbStackPop is only consumed on x86; skip the call elsewhere.
        uint cbStackPop = runtimeInfo.GetTargetArchitecture() == RuntimeInfoArchitecture.X86
            ? argit.CbStackPop()
            : 0;

        return new ArgumentLayout(arguments, cbStackPop);
    }

    private static TransitionBlock BuildTransitionBlock(IRuntimeInfo runtimeInfo)
    {
        RuntimeInfoArchitecture arch = runtimeInfo.GetTargetArchitecture();
        RuntimeInfoOperatingSystem os = runtimeInfo.GetTargetOperatingSystem();

        Internal.TypeSystem.TargetArchitecture targetArch = arch switch
        {
            RuntimeInfoArchitecture.X86 => Internal.TypeSystem.TargetArchitecture.X86,
            RuntimeInfoArchitecture.X64 => Internal.TypeSystem.TargetArchitecture.X64,
            RuntimeInfoArchitecture.Arm => Internal.TypeSystem.TargetArchitecture.ARM,
            RuntimeInfoArchitecture.Arm64 => Internal.TypeSystem.TargetArchitecture.ARM64,
            RuntimeInfoArchitecture.LoongArch64 => Internal.TypeSystem.TargetArchitecture.LoongArch64,
            RuntimeInfoArchitecture.RiscV64 => Internal.TypeSystem.TargetArchitecture.RiscV64,
            RuntimeInfoArchitecture.Wasm => Internal.TypeSystem.TargetArchitecture.Wasm32,
            _ => throw new NotSupportedException($"Unsupported architecture: {arch}"),
        };

        bool isWindows = os == RuntimeInfoOperatingSystem.Windows;
        bool isApplePlatform = os == RuntimeInfoOperatingSystem.Apple;

        return TransitionBlock.FromTarget(targetArch, isWindows, isApplePlatform, isArmel: false);
    }

    // Well-known type handles passed to ArgIterator. The shared iterator only
    // dereferences them when extraObjectFirstArg / extraFunctionPointerArg are
    // set; this contract never sets either, so the lookups are cheap insurance
    // against a future cDAC change tripping a NullReferenceException deep in
    // GetArgumentType.
    private CdacTypeHandle GetObjectTypeHandle(IRuntimeTypeSystem rts)
    {
        TargetPointer objectMt = rts.GetWellKnownMethodTable(WellKnownMethodTable.Object);
        return new CdacTypeHandle(rts.GetTypeHandle(objectMt), _target, this);
    }

    private CdacTypeHandle GetIntPtrTypeHandle(IRuntimeTypeSystem rts)
    {
        return new CdacTypeHandle(rts.GetPrimitiveType(CdacCorElementType.I), _target, this);
    }

    // =====================================================================
    // GCRefMap blob encoder. Produces byte-for-byte the same output as the
    // runtime's ComputeCallRefMap (frames.cpp) via the shared ArgIterator
    // walk above. Used by the cdacstress ArgIterator sub-check.
    // =====================================================================

    private const int MaxGCRefMapBlobLength = 252;
    private const int MaxByRefLikeRecursionDepth = 16;

    private byte[]? ComputeArgGCRefMapBlobCore(MethodDescHandle methodDesc)
    {
        IRuntimeInfo runtimeInfo = _target.Contracts.RuntimeInfo;
        IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem;

        RuntimeInfoArchitecture arch = runtimeInfo.GetTargetArchitecture();
        bool isX86 = arch is RuntimeInfoArchitecture.X86;

        int pointerSize = _target.PointerSize;
        TransitionBlock tb = BuildTransitionBlock(runtimeInfo);

        SortedDictionary<int, GCRefMapToken> tokens = new();
        ArgumentLayout enumeration = GetArgumentLayout(methodDesc);

        GenericContextLoc ctxLoc = GenericContextLoc.None;

        foreach (ArgumentLocation arg in enumeration.Arguments)
        {
            GCRefMapToken token;
            if (arg.IsThis)
            {
                token = arg.IsValueTypeThis ? GCRefMapToken.Interior : GCRefMapToken.Ref;
            }
            else if (arg.IsVASigCookie)
            {
                token = GCRefMapToken.VASigCookie;
            }
            else if (arg.IsStructPassedInRegs)
            {
                // Mirrors ArgDestination::ReportPointersFromStructInRegisters
                // in src/coreclr/vm/argdestination.h.
                int genRegOffset = tb.OffsetOfArgumentRegisters + arg.SysVIdxGenReg * pointerSize;
                for (int i = 0; i < arg.SysVEightByteDescriptor.eightByteCount; i++)
                {
                    SystemVClassificationType cls = (i == 0)
                        ? arg.SysVEightByteDescriptor.eightByteClassifications0
                        : arg.SysVEightByteDescriptor.eightByteClassifications1;
                    int size = (i == 0)
                        ? arg.SysVEightByteDescriptor.eightByteSizes0
                        : arg.SysVEightByteDescriptor.eightByteSizes1;

                    // SSE eightbytes go to XMM regs; don't advance genRegOffset.
                    if (cls == SystemVClassificationType.SystemVClassificationTypeSSE)
                        continue;

                    if (cls == SystemVClassificationType.SystemVClassificationTypeIntegerReference)
                        tokens[genRegOffset] = GCRefMapToken.Ref;
                    else if (cls == SystemVClassificationType.SystemVClassificationTypeIntegerByRef)
                        tokens[genRegOffset] = GCRefMapToken.Interior;

                    genRegOffset += size;
                }
                continue;
            }
            else if (arg.IsParamType)
            {
                // Resolve InstArgMethodDesc vs InstArgMethodTable on demand
                // (cheaper than caching when most methods aren't generic).
                if (ctxLoc == GenericContextLoc.None)
                    ctxLoc = SafeGetGenericContextLoc(rts, methodDesc);

                token = ctxLoc switch
                {
                    GenericContextLoc.InstArgMethodDesc => GCRefMapToken.MethodParam,
                    GenericContextLoc.InstArgMethodTable => GCRefMapToken.TypeParam,
                    _ => GCRefMapToken.Skip,
                };
                if (token == GCRefMapToken.Skip)
                    continue;
            }
            else
            {
                switch ((CorElementType)arg.ElementType)
                {
                    case CorElementType.Class:
                    case CorElementType.String:
                    case CorElementType.Object:
                    case CorElementType.Array:
                    case CorElementType.SzArray:
                        token = GCRefMapToken.Ref;
                        break;

                    case CorElementType.Byref:
                        token = GCRefMapToken.Interior;
                        break;

                    case CorElementType.ValueType:
                        if (arg.IsPassedByRef)
                        {
                            token = GCRefMapToken.Interior;
                        }
                        else
                        {
                            bool emitted = false;

                            if (arg.IsByRefLikeStruct)
                            {
                                // ByRefLike value type (Span<T>, ReadOnlySpan<T>,
                                // ByteRef, any ref struct). Mirrors the runtime's
                                // ByRefPointerOffsetsReporter (siginfo.cpp): walk
                                // the type's instance fields and emit INTERIOR
                                // for each ELEMENT_TYPE_BYREF field at its
                                // in-struct offset. ELEMENT_TYPE_PTR / IntPtr /
                                // void* fields are explicitly NOT reported
                                // (so QCallTypeHandle, ObjectHandleOnStack,
                                // StringHandleOnStack contribute nothing).
                                //
                                // For uncached generic instantiations (Span<int>
                                // whose closed MT isn't loaded), the field
                                // layout lives on the open generic (Span<T>).
                                // The byref/ptr distinction is preserved at the
                                // FieldDesc level regardless of which T closes
                                // the type.
                                EmitByRefLikeInterior(
                                    rts,
                                    arg.TypeInfo,
                                    arg.Offset,
                                    tokens);
                                emitted = true;
                            }

                            if (arg.TypeInfo.ExactTypeHandle is ITypeHandle typeHandle && rts.ContainsGCPointers(typeHandle))
                            {
                                // By-value struct with embedded GC pointers: emit one
                                // Ref token per pointer slot inside the struct. Mirrors
                                // the runtime's ReportPointersFromValueTypeArg
                                // (siginfo.cpp). The GCDesc series Offset is relative
                                // to a boxed object's start (including the leading MT
                                // pointer); subtract pointerSize to translate to the
                                // unboxed in-frame layout.
                                int structFieldStart = arg.Offset - pointerSize;
                                foreach ((uint seriesOffset, uint seriesSize) in rts.GetGCDescSeries(typeHandle))
                                {
                                    int seriesBase = structFieldStart + (int)seriesOffset;
                                    for (int subOff = 0; subOff < (int)seriesSize; subOff += pointerSize)
                                    {
                                        tokens[seriesBase + subOff] = GCRefMapToken.Ref;
                                    }
                                }
                                emitted = true;
                            }

                            if (!emitted)
                                continue;
                            continue;
                        }
                        break;

                    default:
                        continue;
                }
            }

            tokens[arg.Offset] = token;
        }

        // No GC-significant arguments. On non-x86 the empty blob is just the
        // pending byte flush. On x86 it still carries the WriteStackPop prefix,
        // so emit that first.
        if (tokens.Count == 0)
        {
            if (!isX86)
                return EmptyGCRefMapBlob();
            GCRefMapEncoder enc0 = default;
            enc0.WriteStackPop(enumeration.CbStackPop / (uint)pointerSize);
            return enc0.Flush();
        }

        // Walk positions 0..maxPos and look up each one's offset in the token
        // map. This is necessary on x86 because pos-order and offset-order
        // diverge there (argument registers occupy the highest offsets but
        // the lowest positions). On non-x86 the mapping is monotonic so we
        // could iterate the offset map directly, but using OffsetFromGCRefMapPos
        // for both keeps the code path uniform.

        // For x86 we need to know how many slot positions exist (we'd otherwise
        // miss high-pos register slots when the offset map's max is on the
        // stack). Walk every recorded offset and compute its position; for x86
        // OffsetFromGCRefMapPos is bijective so the inverse is well-defined.
        int maxPos = -1;
        foreach (int offset in tokens.Keys)
        {
            int pos = GCRefMapPosFromOffset(tb, offset, isX86, pointerSize);
            if (pos < 0)
                return null;  // alignment / out-of-range -- conservative skip
            if (pos > maxPos) maxPos = pos;
        }

        GCRefMapEncoder enc = default;
        if (isX86)
            enc.WriteStackPop(enumeration.CbStackPop / (uint)pointerSize);

        for (int pos = 0; pos <= maxPos; pos++)
        {
            int offset = tb.OffsetFromGCRefMapPos(pos);
            if (tokens.TryGetValue(offset, out GCRefMapToken token) && token != GCRefMapToken.Skip)
            {
                enc.WriteToken((uint)pos, (byte)token);
                if (enc.Length > MaxGCRefMapBlobLength)
                    return null;
            }
        }
        return enc.Flush();
    }

    // Inverse of TransitionBlock.OffsetFromGCRefMapPos. On non-x86 the mapping
    // is offset = first + pos*ptr, so pos = (offset - first) / ptr. On x86 the
    // first NumArgumentRegisters positions are argument registers laid out at
    // OffsetOfArgumentRegisters + ARGUMENTREGISTERS_SIZE - (pos+1)*ptr; the
    // remaining positions are stack args at OffsetOfArgs + (pos - n)*ptr.
    // Returns -1 on misalignment.
    private static int GCRefMapPosFromOffset(TransitionBlock tb, int offset, bool isX86, int pointerSize)
    {
        if (!isX86)
        {
            int delta = offset - tb.OffsetOfFirstGCRefMapSlot;
            if (delta < 0 || delta % pointerSize != 0) return -1;
            return delta / pointerSize;
        }

        // x86: arg registers come first in pos order, then stack args.
        int argRegBase = tb.OffsetOfArgumentRegisters;
        int argRegEnd = argRegBase + tb.NumArgumentRegisters * pointerSize;
        if (offset >= argRegBase && offset < argRegEnd)
        {
            int delta = offset - argRegBase;
            if (delta % pointerSize != 0) return -1;
            // Reverse: pos = NumArgumentRegisters - 1 - (delta / ptr)
            return tb.NumArgumentRegisters - 1 - (delta / pointerSize);
        }
        if (offset >= tb.OffsetOfArgs)
        {
            int delta = offset - tb.OffsetOfArgs;
            if (delta % pointerSize != 0) return -1;
            return tb.NumArgumentRegisters + (delta / pointerSize);
        }
        return -1;
    }

    private static GenericContextLoc SafeGetGenericContextLoc(IRuntimeTypeSystem rts, MethodDescHandle md)
    {
        try
        {
            return rts.GetGenericContextLoc(md);
        }
        catch
        {
            return GenericContextLoc.None;
        }
    }

    // Mirror of runtime ByRefPointerOffsetsReporter (siginfo.cpp): walk the
    // instance fields of a ByRefLike value type and emit one INTERIOR token
    // per ELEMENT_TYPE_BYREF field at its offset within the unboxed struct
    // (so absolute offset is baseOffset + fieldOffset). Recurses into nested
    // ByRefLike value-type fields. ELEMENT_TYPE_PTR / IntPtr / void* fields
    // are deliberately skipped to match runtime behavior for QCall-style
    // handle wrappers.
    private void EmitByRefLikeInterior(
        IRuntimeTypeSystem rts,
        SignatureTypeInfo byRefLikeType,
        int baseOffset,
        SortedDictionary<int, GCRefMapToken> tokens)
    {
        // Bound recursion just in case the data is corrupt / cycles in a dump.
        EmitByRefLikeInteriorRecursive(
            rts,
            byRefLikeType,
            baseOffset,
            tokens,
            depth: 0);
    }

    private void EmitByRefLikeInteriorRecursive(
        IRuntimeTypeSystem rts,
        SignatureTypeInfo byRefLikeType,
        int baseOffset,
        SortedDictionary<int, GCRefMapToken> tokens,
        int depth)
    {
        if (depth > MaxByRefLikeRecursionDepth)
            return;

        ITypeHandle? layoutType = byRefLikeType.ExactTypeHandle ?? byRefLikeType.GenericTypeDefinition;
        if (layoutType is null)
            return;

        IEnumerable<TargetPointer> fieldDescs;
        try
        {
            fieldDescs = rts.GetFieldDescList(layoutType);
        }
        catch
        {
            return;
        }

        foreach (TargetPointer fdPtr in fieldDescs)
        {
            bool isStatic;
            CorElementType fieldType;
            uint fieldOffset;
            try
            {
                isStatic = rts.IsFieldDescStatic(fdPtr);
                if (isStatic)
                    continue;
                fieldType = rts.GetFieldDescType(fdPtr);
                fieldOffset = rts.GetFieldDescOffset(fdPtr, fieldDef: null);
            }
            catch
            {
                continue;
            }

            int absOffset = baseOffset + (int)fieldOffset;

            if (fieldType == CorElementType.Byref)
            {
                tokens[absOffset] = GCRefMapToken.Interior;
            }
            else if (fieldType == CorElementType.ValueType)
            {
                SignatureTypeInfo nestedType;
                try
                {
                    nestedType = GetFieldTypeInfo(fdPtr, byRefLikeType);
                }
                catch
                {
                    continue;
                }

                ITypeHandle? nestedProbe =
                    nestedType.ExactTypeHandle ?? nestedType.GenericTypeDefinition;
                if (nestedProbe is null || !rts.IsByRefLike(nestedProbe))
                    continue;

                EmitByRefLikeInteriorRecursive(
                    rts,
                    nestedType,
                    absOffset,
                    tokens,
                    depth + 1);
            }
        }
    }

    private static byte[] EmptyGCRefMapBlob()
    {
        GCRefMapEncoder enc = default;
        return enc.Flush();
    }

    // Bit-stream encoder mirroring native GCRefMapBuilder (inc/gcrefmap.h).
    // Every encoding rule -- AppendBit's 7-bit chunks with high-bit
    // continuation, WriteToken's delta encoding, Flush's final byte --
    // matches byte-for-byte.
    private struct GCRefMapEncoder
    {
        private int _pendingByte;
        private int _bits;
        private uint _pos;
        private List<byte> _bytes;

        public int Length => _bytes?.Count ?? 0;

        private void AppendBit(uint bit)
        {
            _bytes ??= new List<byte>(8);
            if (bit != 0)
            {
                while (_bits >= 7)
                {
                    _bytes.Add((byte)(_pendingByte | 0x80));
                    _pendingByte = 0;
                    _bits -= 7;
                }
                _pendingByte |= 1 << _bits;
            }
            _bits++;
        }

        private void AppendTwoBit(uint bits)
        {
            AppendBit(bits & 1);
            AppendBit(bits >> 1);
        }

        private void AppendInt(uint val)
        {
            do
            {
                AppendBit(val & 1);
                AppendBit((val >> 1) & 1);
                AppendBit((val >> 2) & 1);
                val >>= 3;
                AppendBit(val != 0 ? 1u : 0u);
            }
            while (val != 0);
        }

        // x86-only prefix: encode the callee-popped stack-byte count in
        // pointer-size units before any tokens. Mirrors native
        // GCRefMapBuilder::WriteStackPop (inc/gcrefmap.h). Must be called
        // before the first WriteToken.
        public void WriteStackPop(uint stackPop)
        {
            if (stackPop < 3)
            {
                AppendTwoBit(stackPop);
            }
            else
            {
                AppendTwoBit(3);
                AppendInt(stackPop - 3);
            }
        }

        public void WriteToken(uint pos, uint token)
        {
            uint posDelta = pos - _pos;
            _pos = pos + 1;

            if (posDelta != 0)
            {
                if (posDelta < 4)
                {
                    while (posDelta > 0)
                    {
                        AppendTwoBit(0);
                        posDelta--;
                    }
                }
                else
                {
                    AppendTwoBit(3);
                    AppendInt((posDelta - 4) << 1);
                }
            }

            if (token < 3)
            {
                AppendTwoBit(token);
            }
            else
            {
                AppendTwoBit(3);
                AppendInt(((token - 3) << 1) | 1);
            }
        }

        public byte[] Flush()
        {
            _bytes ??= new List<byte>(1);
            if ((_pendingByte & 0x7F) != 0 || _pos == 0)
                _bytes.Add((byte)(_pendingByte & 0x7F));

            return _bytes.ToArray();
        }
    }
}