File: Contracts\StackWalk\FrameHandling\WasmFrameHandler.cs
Web Access
Project: 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 Microsoft.Diagnostics.DataContractReader.Data;

namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers;

/// <summary>
/// Frame handler for CoreCLR on WebAssembly.
/// </summary>
/// <remarks>
/// WebAssembly has no native register context (see <see cref="WasmContext"/>). Seeding the
/// initial stack walk context therefore comes from the explicit Frame chain rather than a
/// captured <c>DT_CONTEXT</c>: the innermost transition frame carries the managed linear stack
/// pointer. The base <see cref="BaseFrameHandler.HandleInlinedCallFrame"/> already reads that
/// <c>InlinedCallFrame.CallSiteSP</c> (plus the caller return address and callee-saved frame
/// pointer) into the three synthetic <see cref="WasmContext"/> slots, which is the common
/// P/Invoke-boundary seeding path; an inlined P/Invoke from R2R code instead stores a marker and is
/// resolved from its R2R shadow frame. The software/faulting exception frame handlers likewise read a
/// serialized <see cref="WasmContext"/> blob from the frame's <c>TargetContext</c>, restoring
/// IP, SP and the frame pointer.
///
/// Hijack frames are a debugger / GC-suspension concept that is not yet supported on WASM.
/// </remarks>
internal sealed class WasmFrameHandler(Target target, ContextHolder<WasmContext> contextHolder)
    : BaseFrameHandler(target, contextHolder), IPlatformFrameHandler
{
    // INLINED_PINVOKE_FROM_R2R from src/coreclr/vm/frames.h. An R2R inlined P/Invoke has no native
    // return address on WASM, so the runtime stores this marker and derives IP/SP from CallSiteSP.
    private const ulong InlinedPInvokeFromR2R = 1;

    private readonly ContextHolder<WasmContext> _holder = contextHolder;

    public override void HandleInlinedCallFrame(InlinedCallFrame inlinedCallFrame)
    {
        // Native InlinedCallFrame::UpdateRegDisplay_Impl returns before any update, including the
        // interpreter-frame stash below, when the frame has no active call.
        if (inlinedCallFrame.CallerReturnAddress == TargetCodePointer.Null)
            return;

        if (inlinedCallFrame.CallerReturnAddress.Value == InlinedPInvokeFromR2R)
        {
            // Mirrors InlinedCallFrame::UpdateRegDisplay_Impl in src/coreclr/vm/wasm/helpers.cpp.
            // If no R2R virtual IP can be recovered the IP is left null (not managed code), and the
            // stack walker fails the walk as native does, rather than treating the marker as an address.
            Wasm.WasmUnwinder unwinder = new(_target, new Wasm.WasmR2RInfo(_target));
            _holder.Context.StackPointer = inlinedCallFrame.CallSiteSP;
            _holder.Context.InstructionPointer = unwinder.GetVirtualIP(inlinedCallFrame.CallSiteSP);
            // Native GetWasmFramePointerFromStackPointer: a funclet reports its establishing method's frame.
            _holder.Context.FramePointer = unwinder.TryGetLogicalFramePointer(inlinedCallFrame.CallSiteSP, out TargetPointer framePointer)
                ? framePointer
                : TargetPointer.Null;
        }
        else
        {
            base.HandleInlinedCallFrame(inlinedCallFrame);
        }

        // When the frame directly above this P/Invoke transition is an InterpreterFrame, stash its
        // address in the synthetic first-argument register so the subsequent interpreter virtual
        // unwind (InterpreterVirtualUnwind -> GetFirstArgReg) can recover the owning InterpreterFrame.
        // Mirrors the per-architecture handlers (e.g. AMD64FrameHandler) and the native
        // SetFirstArgReg(context->InterpreterWalkFramePointer) contract on WASM.
        Data.Frame? next = GetNextFrame(inlinedCallFrame.Address);
        if (next is not null && _frameHelpers.GetFrameType(next.Identifier) == FrameType.InterpreterFrame)
        {
            if (!_holder.Context.TrySetRegister(WasmContext.InterpreterWalkFramePointerRegister, new TargetNUInt(next.Address.Value)))
                throw new InvalidOperationException($"Failed to set WASM interpreter frame-pointer register '{WasmContext.InterpreterWalkFramePointerRegister}'.");
        }
    }

    // Native restores FP with IP and SP, but the CalleeSavedRegisters descriptor is empty on WASM,
    // so the base handler would leave FP stale.
    public override void HandleSoftwareExceptionFrame(SoftwareExceptionFrame softwareExceptionFrame)
    {
        ContextHolder<WasmContext> saved = new();
        saved.ReadFromAddress(_target, softwareExceptionFrame.TargetContext);

        _holder.Context.InstructionPointer = saved.Context.InstructionPointer;
        _holder.Context.StackPointer = saved.Context.StackPointer;
        _holder.Context.FramePointer = saved.Context.FramePointer;
    }

    // Mirrors TransitionFrame::UpdateRegDisplay_Impl in src/coreclr/vm/wasm/helpers.cpp. With a recorded
    // R2R stack pointer and a known return address, the caller is the R2R frame at that stack pointer
    // (TransitionFrame::GetSP); otherwise the caller's stack pointer is the end of the TransitionBlock.
    public override void HandleTransitionFrame(FramedMethodFrame framedMethodFrame)
    {
        Data.TransitionBlock transitionBlock = _target.ProcessedData.GetOrAdd<Data.TransitionBlock>(framedMethodFrame.TransitionBlockPtr);
        TargetCodePointer instructionPointer = _frameHelpers.GetTransitionBlockReturnAddress(transitionBlock);
        TargetPointer stackPointer = transitionBlock.StackPointer ?? TargetPointer.Null;

        _holder.Context.InstructionPointer = instructionPointer;
        if (stackPointer != TargetPointer.Null && instructionPointer != TargetCodePointer.Null)
        {
            _holder.Context.StackPointer = stackPointer;
            // Native GetWasmFramePointerFromStackPointer: a funclet reports its establishing method's frame.
            Wasm.WasmUnwinder unwinder = new(_target, new Wasm.WasmR2RInfo(_target));
            _holder.Context.FramePointer = unwinder.TryGetLogicalFramePointer(stackPointer, out TargetPointer framePointer)
                ? framePointer
                : TargetPointer.Null;
        }
        else
        {
            _holder.Context.StackPointer = framedMethodFrame.TransitionBlockPtr + Data.TransitionBlock.GetSize(_target);
            _holder.Context.FramePointer = TargetPointer.Null;
        }
    }

    public void HandleHijackFrame(HijackFrame frame)
        => throw new PlatformNotSupportedException("HijackFrame handling is not supported on WASM.");
}