Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
695f29e
Convert funceval to UCO pattern
am11 Apr 12, 2026
688ca70
Rename FuncEvalInvokeContract to FuncEvalInvokeArgs
am11 Apr 14, 2026
5d89dfb
Remove UnpackFuncEvalResult function
am11 Apr 14, 2026
4d3f798
Merge branch 'main' into feature/MDCS-to-UCOA-pattern2
am11 Apr 14, 2026
cd5d779
Simplification and mopping
am11 Apr 14, 2026
3711c83
Merge branch 'main' into feature/MDCS-to-UCOA-pattern2
am11 Apr 14, 2026
a5ed117
Address CR feedback
am11 Apr 14, 2026
5306777
Merge branch 'main' into feature/MDCS-to-UCOA-pattern2
am11 Apr 14, 2026
e2d4338
Make UCO MethodDesc globals DAC-accessible
am11 Apr 14, 2026
ab3068a
Merge branch 'main' into feature/MDCS-to-UCOA-pattern2
am11 Apr 14, 2026
89fe5b5
Relax boxed value handling
am11 Apr 14, 2026
309e6de
Merge branch 'main' into feature/MDCS-to-UCOA-pattern2
am11 Apr 14, 2026
43c19bf
Remove main entrypoints checks
am11 Apr 14, 2026
83b9e1f
Merge branch 'main' into feature/MDCS-to-UCOA-pattern2
am11 Apr 14, 2026
ee224c6
Improve func-eval return handling and boxing logic
am11 Apr 15, 2026
2bc283c
Merge branch 'main' into feature/MDCS-to-UCOA-pattern2
am11 Apr 15, 2026
d80c094
Revert "Remove main entrypoints checks"
am11 Apr 15, 2026
a889daa
Revert "Make UCO MethodDesc globals DAC-accessible"
am11 Apr 15, 2026
8f751e3
Move GC protection further up
am11 Apr 15, 2026
409cfb2
Merge branch 'main' into feature/MDCS-to-UCOA-pattern2
am11 Apr 15, 2026
f11c087
Refactor MethodTable assignment logic in funceval.cpp
am11 Apr 15, 2026
e23a645
Merge branch 'main' into feature/MDCS-to-UCOA-pattern2
am11 Apr 15, 2026
2f3be5a
Merge branch 'main' into feature/MDCS-to-UCOA-pattern2
am11 Apr 15, 2026
d92f2ee
Fix regression
am11 Apr 15, 2026
cf6e647
Merge branch 'main' into feature/MDCS-to-UCOA-pattern2
am11 Apr 20, 2026
71942f3
Merge branch 'main' into feature/MDCS-to-UCOA-pattern2
am11 Apr 29, 2026
0d996a1
Fix cases reported by @tommcdon
am11 May 1, 2026
3dd0326
Merge dotnet/main into feature/MDCS-to-UCOA-pattern2
am11 May 1, 2026
9d1d53b
GC protect stack byref
am11 May 2, 2026
12cd600
Merge branch 'main' into feature/MDCS-to-UCOA-pattern2
am11 May 2, 2026
b9ea697
Specialize byref handling
am11 May 2, 2026
faea9da
Shorten comments
am11 May 2, 2026
cd8bb99
Merge branch 'main' into feature/MDCS-to-UCOA-pattern2
am11 May 5, 2026
dc82d12
Cleanup attribute removed from main
am11 May 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Reflection;
using System.Runtime;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Runtime.Versioning;
Expand Down Expand Up @@ -674,6 +675,104 @@ internal static unsafe void CallDefaultConstructor(object* pObj, delegate*<objec
*pException = ex;
}
}

[StructLayout(LayoutKind.Sequential)]
private unsafe struct FuncEvalInvokeArgs
{
public nint MethodHandle;
public nint OwnerType;
public object* ThisObj;
public object?[]* Args;
public int IsNewObj;
// Byref-like value types (for example Span<T>) cannot go through heap boxes:
// the GC does not track embedded byrefs in boxed payloads. Native funceval
// passes raw byrefs in RawByRefs[i] and leaves Args[i] null.
public IntPtr* RawByRefs;
}

[UnmanagedCallersOnly]
private static unsafe void InvokeFuncEval(FuncEvalInvokeArgs* pInvokeArgs, object* pResult, Exception* pException)
{
try
{
RuntimeMethodHandleInternal handle = new RuntimeMethodHandleInternal(pInvokeArgs->MethodHandle);
RuntimeType? ownerType = pInvokeArgs->OwnerType != 0
? RuntimeTypeHandle.GetRuntimeTypeFromHandle(pInvokeArgs->OwnerType)
: null;
MethodBase method = RuntimeType.GetMethodBase(ownerType, handle)!;

object? thisObj = *pInvokeArgs->ThisObj;
object?[]? args = *pInvokeArgs->Args;
bool isNewObj = pInvokeArgs->IsNewObj != 0;

// Call RuntimeMethodHandle.InvokeMethod directly instead of public
// MethodBase.Invoke:
// 1. Public invoke blocks byref-like signatures (ContainsStackPointers).
// 2. ConstructorInfo.Invoke forces isConstructor=false, which breaks
// value-type ctor funceval by mutating a temporary copy.
Signature sig = method switch
{
RuntimeMethodInfo rmi => rmi.Signature,
RuntimeConstructorInfo rci => rci.Signature,
_ => throw new NotSupportedException(),
};

int argCount = args is null ? 0 : args.Length;
if (sig.Arguments.Length != argCount)
{
throw new TargetParameterCountException();
}

// Build QCall byref arguments (same shape as MethodBaseInvoker):
// value types point at boxed payload; reference types point at args[] slots.
// Register this unmanaged storage as byrefs so GC can retarget them.
IntPtr* pByRefStorage = stackalloc IntPtr[Math.Max(argCount, 1)];
Comment thread
am11 marked this conversation as resolved.
NativeMemory.Clear(pByRefStorage, (nuint)Math.Max(argCount, 1) * (nuint)sizeof(IntPtr));
GCFrameRegistration regByRefStorage =
new((void**)pByRefStorage, (uint)argCount, areByRefs: true);

object? result;
GCFrameRegistration.RegisterForGCReporting(&regByRefStorage);
try
{
IntPtr* pRawByRefs = pInvokeArgs->RawByRefs;
for (int i = 0; i < argCount; i++)
{
IntPtr raw = pRawByRefs != null ? pRawByRefs[i] : IntPtr.Zero;
if (raw != IntPtr.Zero)
{
// Native side already provided a stable byref for this arg.
*(IntPtr*)(pByRefStorage + i) = raw;
continue;
}

ref object? slot = ref args![i];
if (sig.Arguments[i].IsValueType)
{
// Value-type args are pre-boxed by native ReadAndBoxArgValue.
*(ByReference*)(pByRefStorage + i) = ByReference.Create(ref slot!.GetRawData());
}
else
{
*(ByReference*)(pByRefStorage + i) = ByReference.Create(ref slot);
}
}

result = RuntimeMethodHandle.InvokeMethod(thisObj, (void**)pByRefStorage, sig, isConstructor: isNewObj);
}
finally
{
GCFrameRegistration.UnregisterForGCReporting(&regByRefStorage);
}

if (result is not null)
*pResult = result;
}
catch (Exception ex)
{
*pException = ex;
}
}
}
// Helper class to assist with unsafe pinning of arbitrary objects.
// It's used by VM code.
Expand Down Expand Up @@ -724,7 +823,7 @@ internal unsafe struct MethodDesc
internal unsafe struct MethodDescChunk
{
public MethodTable* MethodTable;
public MethodDescChunk* Next;
public MethodDescChunk* Next;
public byte Size; // The size of this chunk minus 1 (in multiples of MethodDesc::ALIGNMENT)
public byte Count; // The number of MethodDescs in this chunk minus 1
public ushort FlagsAndTokenRange;
Expand Down Expand Up @@ -1117,11 +1216,11 @@ internal unsafe struct MethodTableAuxiliaryData
private const uint enum_flag_HasCheckedCanCompareBitsOrUseFastGetHashCode = 0x0002; // Whether we have checked the overridden Equals or GetHashCode
private const uint enum_flag_CanCompareBitsOrUseFastGetHashCode = 0x0004; // Is any field type or sub field type overridden Equals or GetHashCode

private const uint enum_flag_Initialized = 0x0001;
private const uint enum_flag_HasCheckedStreamOverride = 0x0400;
private const uint enum_flag_StreamOverriddenRead = 0x0800;
private const uint enum_flag_StreamOverriddenWrite = 0x1000;
private const uint enum_flag_EnsuredInstanceActive = 0x2000;
private const uint enum_flag_Initialized = 0x0001;
private const uint enum_flag_HasCheckedStreamOverride = 0x0400;
private const uint enum_flag_StreamOverriddenRead = 0x0800;
private const uint enum_flag_StreamOverriddenWrite = 0x1000;
private const uint enum_flag_EnsuredInstanceActive = 0x2000;


public bool HasCheckedCanCompareBitsOrUseFastGetHashCode => (Flags & enum_flag_HasCheckedCanCompareBitsOrUseFastGetHashCode) != 0;
Expand Down
63 changes: 60 additions & 3 deletions src/coreclr/debug/daccess/dacdbiimplstackwalk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@

typedef IDacDbiInterface::StackWalkHandle StackWalkHandle;


// Persistent data needed to do a stackwalk. This is allocated on the forDbi heap.
// It can survive across multiple DD calls.
// However, it has data structures that have raw pointers into the DAC cache, and so it must
Expand Down Expand Up @@ -423,6 +422,11 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetStackWalkCurrentFrameInfo(Stac
{
ftResult = kRuntimeEntryPointFrame;
}
// RuntimeHelpers.InvokeFuncEval (managed funceval trampoline)
else if (pMD == g_pInvokeFuncEvalMethodDesc)
{
ftResult = kRuntimeEntryPointFrame;
}
else
{
ftResult = kManagedStackFrame;
Expand All @@ -436,8 +440,32 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetStackWalkCurrentFrameInfo(Stac
// fall through
//
case StackFrameIterator::SFITER_SKIPPED_FRAME_FUNCTION:
ftResult = kExplicitFrame;
fInitFrameData = TRUE;
{
// Suppress explicit Frames belonging to the managed funceval trampoline.
// We identify them structurally: the next Frame in the chain is a
// FuncEvalFrame, which means this Frame is scaffolding the trampoline
// (typically the InlinedCallFrame for its inner QCall) erected between
// the user method and the FuncEvalFrame.
Frame *pCurFrame = pIter->m_crawl.GetFrame();
bool isFuncEvalScaffolding = false;
if (pCurFrame != NULL && pCurFrame != FRAME_TOP &&
pCurFrame->GetFrameIdentifier() != FrameIdentifier::FuncEvalFrame)
{
Frame *pNextFrame = pCurFrame->Next();
isFuncEvalScaffolding =
(pNextFrame != NULL && pNextFrame != FRAME_TOP &&
pNextFrame->GetFrameIdentifier() == FrameIdentifier::FuncEvalFrame);
}
if (isFuncEvalScaffolding)
{
ftResult = kRuntimeEntryPointFrame;
}
else
{
ftResult = kExplicitFrame;
fInitFrameData = TRUE;
}
}
break;

case StackFrameIterator::SFITER_NO_FRAME_TRANSITION:
Expand Down Expand Up @@ -530,6 +558,22 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetCountOfInternalFrames(VMPTR_Th
}
#endif // FEATURE_INTERPRETER

// Skip explicit Frames belonging to the managed funceval trampoline
// (RuntimeHelpers.InvokeFuncEval). They are an implementation detail of
// debugger function evaluation and should not appear in the stack shown
// to debugger clients. Identified structurally as scaffolding immediately
// preceding a FuncEvalFrame.
if (pFrame->GetFrameIdentifier() != FrameIdentifier::FuncEvalFrame)
{
Frame *pNextFrame = pFrame->Next();
if (pNextFrame != NULL && pNextFrame != FRAME_TOP &&
pNextFrame->GetFrameIdentifier() == FrameIdentifier::FuncEvalFrame)
{
pFrame = pNextFrame;
continue;
}
}

CorDebugInternalFrameType ift = GetInternalFrameType(pFrame);
if (ift != STUBFRAME_NONE)
{
Expand Down Expand Up @@ -596,6 +640,19 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::EnumerateInternalFrames(VMPTR_Thr
}
#endif // FEATURE_INTERPRETER

// Skip explicit Frames belonging to the managed funceval trampoline
// (RuntimeHelpers.InvokeFuncEval). See GetCountOfInternalFrames for rationale.
if (pFrame->GetFrameIdentifier() != FrameIdentifier::FuncEvalFrame)
{
Frame *pNextFrame = pFrame->Next();
if (pNextFrame != NULL && pNextFrame != FRAME_TOP &&
pNextFrame->GetFrameIdentifier() == FrameIdentifier::FuncEvalFrame)
{
pFrame = pNextFrame;
continue;
}
}

// check if the internal frame is interesting
frameData.stubFrame.frameType = GetInternalFrameType(pFrame);
if (frameData.stubFrame.frameType != STUBFRAME_NONE)
Expand Down
6 changes: 6 additions & 0 deletions src/coreclr/debug/ee/controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6782,6 +6782,12 @@ bool DebuggerStepper::IsInterestingFrame(FrameInfo * pFrame)
{
return false;
}

// Ignore the managed funceval trampoline (RuntimeHelpers.InvokeFuncEval)
if (pFrame->md == g_pInvokeFuncEvalMethodDesc)
{
return false;
}
}

return true;
Expand Down
Loading
Loading