// 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.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;
using System.Threading;
using Microsoft.Diagnostics.DataContractReader.Contracts;
using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers;
namespace Microsoft.Diagnostics.DataContractReader.Legacy;
[GeneratedComClass]
public sealed unsafe partial class ClrDataStackWalk : IXCLRDataStackWalk
{
private readonly Lock _apiLock;
private readonly TargetPointer _threadAddr;
private readonly CLRDataStackWalkFlag _flags;
private readonly Target _target;
private readonly IXCLRDataStackWalk? _legacyImpl;
private readonly ThreadData _threadData;
private bool _currentFrameIsValid;
private IEnumerator<IStackDataFrameHandle> _dataFrames;
private ulong _stackPointerBeforeFiltering;
private ulong _stackSizeSkipped;
public ClrDataStackWalk(TargetPointer threadAddr, CLRDataStackWalkFlag flags, Target target, IXCLRDataStackWalk? legacyImpl, Lock apiLock)
{
_apiLock = apiLock;
_threadAddr = threadAddr;
_flags = flags;
_target = target;
_legacyImpl = legacyImpl;
_threadData = _target.Contracts.Thread.GetThreadData(_threadAddr);
_dataFrames = _target.Contracts.StackWalk.CreateStackWalk(_threadData).GetEnumerator();
// IEnumerator<T> begins before the first element.
// Call MoveNext() to set _dataFrames.Current to the first element.
_currentFrameIsValid = MoveNextLegacyVisible();
}
/// <summary>
/// Advance the enumerator to the next frame that the legacy SOSDAC stack walker
/// would have surfaced.
/// </summary>
private bool MoveNextLegacyVisible()
{
if (!_dataFrames.MoveNext())
{
return false;
}
IStackWalk stackWalk = _target.Contracts.StackWalk;
_stackPointerBeforeFiltering = stackWalk.GetStackPointer(_dataFrames.Current).Value;
do
{
_stackSizeSkipped = unchecked(stackWalk.GetStackPointer(_dataFrames.Current).Value - _stackPointerBeforeFiltering);
if (IsLegacyVisible(_dataFrames.Current))
{
return true;
}
}
while (_dataFrames.MoveNext());
return false;
}
internal bool IsLegacyVisible(IStackDataFrameHandle frame)
=> frame.State is StackWalkState.Frameless
|| ((_flags & CLRDataStackWalkFlag.CLRDATA_SIMPFRAME_RUNTIME_UNMANAGED_CODE) != 0
&& frame.State is StackWalkState.Frame or StackWalkState.SkippedFrame);
private void Reseed(byte[] context, bool isFirst)
{
_dataFrames.Dispose();
_dataFrames = _target.Contracts.StackWalk.CreateStackWalk(_threadData, context, isFirst).GetEnumerator();
_currentFrameIsValid = MoveNextLegacyVisible();
}
int IXCLRDataStackWalk.GetContext(uint contextFlags, uint contextBufSize, uint* contextSize, [MarshalUsing(CountElementName = "contextBufSize"), Out] byte[] contextBuf)
{
using Lock.Scope scope = _apiLock.EnterScope();
int hr = HResults.S_OK;
if (_currentFrameIsValid)
{
IStackWalk sw = _target.Contracts.StackWalk;
IStackDataFrameHandle dataFrame = _dataFrames.Current;
byte[] context = sw.GetRawContext(dataFrame);
if (context.Length > contextBufSize)
hr = HResults.E_INVALIDARG;
if (contextSize is not null)
{
*contextSize = (uint)context.Length;
}
context.CopyTo(contextBuf);
}
else
{
hr = HResults.S_FALSE;
}
#if DEBUG
if (_legacyImpl is not null)
{
byte[] localContextBuf = new byte[contextBufSize];
int hrLocal = _legacyImpl.GetContext(contextFlags, contextBufSize, null, localContextBuf);
Debug.ValidateHResult(hr, hrLocal);
if (hr == HResults.S_OK)
{
IPlatformAgnosticContext contextStruct = IPlatformAgnosticContext.GetContextForPlatform(_target);
IPlatformAgnosticContext localContextStruct = IPlatformAgnosticContext.GetContextForPlatform(_target);
contextStruct.FillFromBuffer(contextBuf);
localContextStruct.FillFromBuffer(localContextBuf);
Debug.Assert(contextStruct.Equals(localContextStruct));
}
}
#endif
return hr;
}
int IXCLRDataStackWalk.GetFrame(DacComNullableByRef<IXCLRDataFrame> frame)
{
using Lock.Scope scope = _apiLock.EnterScope();
int hr = HResults.S_OK;
int hrLocal = HResults.S_OK;
IXCLRDataFrame? legacyFrame = null;
if (_legacyImpl is not null)
{
DacComNullableByRef<IXCLRDataFrame> legacyFrameOut = new(isNullRef: false);
hrLocal = _legacyImpl.GetFrame(legacyFrameOut);
if (hrLocal >= 0)
legacyFrame = legacyFrameOut.Interface;
}
try
{
if (!_currentFrameIsValid)
throw new ArgumentException();
frame.Interface = new ClrDataFrame(_target, _threadAddr, _dataFrames.Current, legacyFrame, _apiLock);
}
catch (System.Exception ex)
{
hr = ex.HResult;
}
if (_legacyImpl is not null)
Debug.ValidateHResult(hr, hrLocal);
return hr;
}
int IXCLRDataStackWalk.GetFrameType(CLRDataSimpleFrameType* simpleType, CLRDataDetailedFrameType* detailedType)
{
using Lock.Scope scope = _apiLock.EnterScope();
int hr = HResults.S_OK;
try
{
if (_currentFrameIsValid)
{
IStackDataFrameHandle dataFrame = _dataFrames.Current;
if (simpleType is not null)
{
*simpleType = dataFrame.State switch
{
StackWalkState.Frameless => CLRDataSimpleFrameType.CLRDATA_SIMPFRAME_MANAGED_METHOD,
StackWalkState.Frame or StackWalkState.SkippedFrame => CLRDataSimpleFrameType.CLRDATA_SIMPFRAME_RUNTIME_UNMANAGED_CODE,
_ => CLRDataSimpleFrameType.CLRDATA_SIMPFRAME_UNRECOGNIZED,
};
}
if (detailedType is not null)
{
*detailedType = dataFrame.IsExceptionFrame
? CLRDataDetailedFrameType.CLRDATA_DETFRAME_EXCEPTION_FILTER
: CLRDataDetailedFrameType.CLRDATA_DETFRAME_UNRECOGNIZED;
}
}
else
{
hr = HResults.S_FALSE;
}
}
catch (System.Exception ex)
{
hr = ex.HResult;
}
#if DEBUG
if (_legacyImpl is not null)
{
CLRDataSimpleFrameType simpleTypeLocal = 0;
CLRDataDetailedFrameType detailedTypeLocal = 0;
int hrLocal = _legacyImpl.GetFrameType(&simpleTypeLocal, &detailedTypeLocal);
Debug.ValidateHResult(hr, hrLocal);
if (hr == HResults.S_OK)
{
if (simpleType is not null)
Debug.Assert(*simpleType == simpleTypeLocal, $"cDAC: {*simpleType:x}, DAC: {simpleTypeLocal:x}");
if (detailedType is not null)
Debug.Assert(*detailedType == detailedTypeLocal, $"cDAC: {*detailedType:x}, DAC: {detailedTypeLocal:x}");
}
}
#endif
return hr;
}
int IXCLRDataStackWalk.GetStackSizeSkipped(ulong* stackSizeSkipped)
{
using Lock.Scope scope = _apiLock.EnterScope();
int hr = HResults.S_OK;
try
{
*stackSizeSkipped = _stackSizeSkipped;
}
catch (System.Exception ex)
{
hr = ex.HResult;
}
#if DEBUG
if (_legacyImpl is not null)
{
ulong stackSizeSkippedLocal = 0;
int hrLocal = _legacyImpl.GetStackSizeSkipped(stackSizeSkipped is null ? null : &stackSizeSkippedLocal);
Debug.ValidateHResult(hr, hrLocal);
if (hr == HResults.S_OK)
{
Debug.Assert(*stackSizeSkipped == stackSizeSkippedLocal, $"cDAC: {*stackSizeSkipped:x}, DAC: {stackSizeSkippedLocal:x}");
}
}
#endif
return hr;
}
int IXCLRDataStackWalk.Next()
{
using Lock.Scope scope = _apiLock.EnterScope();
int hr;
try
{
_currentFrameIsValid = MoveNextLegacyVisible();
hr = _currentFrameIsValid ? HResults.S_OK : HResults.S_FALSE;
}
catch (System.Exception ex)
{
hr = ex.HResult;
}
// Advance the legacy stack walk to keep it in sync with the cDAC walk.
// GetFrame() passes the legacy frame to ClrDataFrame, which delegates
// GetArgumentByIndex/GetLocalVariableByIndex to it. If we don't advance
// the legacy walk here, those calls operate on the wrong frame.
if (_legacyImpl is not null)
{
int hrLocal = _legacyImpl.Next();
#if DEBUG
Debug.ValidateHResult(hr, hrLocal);
#endif
}
return hr;
}
int IXCLRDataStackWalk.Request(uint reqCode, uint inBufferSize, byte* inBuffer, uint outBufferSize, byte* outBuffer)
{
using Lock.Scope scope = _apiLock.EnterScope();
const uint DACSTACKPRIV_REQUEST_FRAME_DATA = 0xf0000000;
int hr = HResults.S_OK;
try
{
switch (reqCode)
{
case (uint)CLRDataGeneralRequest.CLRDATA_REQUEST_REVISION:
if (inBufferSize != 0 || inBuffer != null || outBufferSize != sizeof(uint) || outBuffer == null)
throw new ArgumentException("Invalid buffer parameters for CLRDATA_REQUEST_REVISION");
*(uint*)outBuffer = 1;
hr = HResults.S_OK;
break;
case (uint)CLRDataStackWalkRequest.CLRDATA_STACK_WALK_REQUEST_SET_FIRST_FRAME:
if (inBufferSize != sizeof(uint) || inBuffer == null || outBufferSize != 0)
throw new ArgumentException("Invalid buffer parameters for CLRDATA_STACK_WALK_REQUEST_SET_FIRST_FRAME");
hr = HResults.S_OK; // no-op for the case where we use this
break;
case DACSTACKPRIV_REQUEST_FRAME_DATA:
if (inBufferSize != 0 || inBuffer != null || outBufferSize != sizeof(ulong) || outBuffer == null)
throw new ArgumentException("Invalid buffer parameters for DACSTACKPRIV_REQUEST_FRAME_DATA");
if (!_currentFrameIsValid)
throw new ArgumentException("Invalid frame");
IStackWalk sw = _target.Contracts.StackWalk;
IStackDataFrameHandle frameData = _dataFrames.Current;
TargetPointer frameAddr = sw.GetFrameAddress(frameData);
*(ulong*)outBuffer = frameAddr.ToClrDataAddress(_target);
hr = HResults.S_OK;
break;
default:
throw new ArgumentException("Invalid request code");
}
}
catch (System.Exception ex)
{
hr = ex.HResult;
}
#if DEBUG
if (_legacyImpl is not null)
{
byte[] localOutBuffer = new byte[(int)outBufferSize];
fixed (byte* localOutBufferPtr = localOutBuffer)
{
int hrLocal = _legacyImpl.Request(reqCode, inBufferSize, inBuffer, outBufferSize, localOutBufferPtr);
Debug.ValidateHResult(hr, hrLocal);
if (hr == HResults.S_OK)
Debug.Assert(new ReadOnlySpan<byte>(outBuffer, (int)outBufferSize).SequenceEqual(localOutBuffer));
}
}
#endif
return hr;
}
int IXCLRDataStackWalk.SetContext(uint contextSize, [In, MarshalUsing(CountElementName = "contextSize")] byte[] context)
{
using Lock.Scope scope = _apiLock.EnterScope();
int hr = HResults.S_OK;
try
{
uint platformContextSize = IPlatformAgnosticContext.GetContextForPlatform(_target).Size;
if (context is null || contextSize < platformContextSize)
throw new ArgumentException("Invalid context buffer");
bool isFirst = _currentFrameIsValid && _dataFrames.Current.IsActiveFrame;
Reseed(context, isFirst);
}
catch (System.Exception ex)
{
hr = ex.HResult;
}
if (_legacyImpl is not null)
{
int hrLocal = _legacyImpl.SetContext(contextSize, context);
#if DEBUG
Debug.ValidateHResult(hr, hrLocal);
#endif
}
return hr;
}
int IXCLRDataStackWalk.SetContext2(uint flags, uint contextSize, [In, MarshalUsing(CountElementName = "contextSize")] byte[] context)
{
using Lock.Scope scope = _apiLock.EnterScope();
return HResults.E_NOTIMPL;
}
}