From 5391e9ec4fb4ab1bcebe4dbfc33bf4b8fdf01981 Mon Sep 17 00:00:00 2001 From: Rachel Jarvi Date: Tue, 28 Apr 2026 14:27:00 -0700 Subject: [PATCH 001/115] [cDAC] Adding IsLeafFrame and GetContext DacDbi APIs (#127195) Assuming we don't care about datatargets that don't implement `GetThreadContext`. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/design/datacontracts/Thread.md | 35 ++++- src/coreclr/debug/daccess/dacdbiimpl.cpp | 52 +------ .../debug/daccess/dacdbiimplstackwalk.cpp | 8 +- src/coreclr/debug/inc/amd64/primitives.h | 4 +- src/coreclr/debug/inc/arm/primitives.h | 3 - src/coreclr/debug/inc/arm64/primitives.h | 5 +- src/coreclr/debug/inc/i386/primitives.h | 4 +- .../debug/inc/loongarch64/primitives.h | 5 +- src/coreclr/debug/inc/riscv64/primitives.h | 5 +- .../vm/datadescriptor/datadescriptor.inc | 3 - src/coreclr/vm/threads.h | 3 - .../Contracts/IThread.cs | 8 + .../StackWalk/Context/AMD64Context.cs | 4 +- .../StackWalk/Context/ARM64Context.cs | 9 +- .../Contracts/StackWalk/Context/ARMContext.cs | 4 +- .../StackWalk/Context/ContextHolder.cs | 3 +- .../Context/IPlatformAgnosticContext.cs | 3 +- .../StackWalk/Context/IPlatformContext.cs | 3 +- .../StackWalk/Context/LoongArch64Context.cs | 7 +- .../StackWalk/Context/RISCV64Context.cs | 4 +- .../Contracts/StackWalk/Context/X86Context.cs | 4 +- .../Contracts/StackWalk/StackWalk_1.cs | 58 +++----- .../Contracts/Thread_1.cs | 28 ++++ .../Data/Thread.cs | 2 - .../Dbi/DacDbiImpl.cs | 78 +++++++++- .../Dbi/IDacDbiInterface.cs | 4 +- .../DacDbi/DacDbiStackWalkDumpTests.cs | 138 ++++++++++++++++++ .../tests/DumpTests/StackWalkDumpTests.cs | 21 +++ .../MockDescriptors/MockDescriptors.Thread.cs | 16 +- src/native/managed/cdac/tests/ThreadTests.cs | 26 +--- 30 files changed, 372 insertions(+), 175 deletions(-) create mode 100644 src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiStackWalkDumpTests.cs diff --git a/docs/design/datacontracts/Thread.md b/docs/design/datacontracts/Thread.md index f2e07c8debe428..6dc9596712f18c 100644 --- a/docs/design/datacontracts/Thread.md +++ b/docs/design/datacontracts/Thread.md @@ -5,6 +5,13 @@ This contract is for reading and iterating the threads of the process. ## APIs of contract ``` csharp +[Flags] +enum ThreadContextSource +{ + None = 0, + Debugger = 1, +} + record struct ThreadStoreData ( int ThreadCount, TargetPointer FirstThread, @@ -52,7 +59,8 @@ ThreadStoreCounts GetThreadCounts(); ThreadData GetThreadData(TargetPointer threadPointer); void GetStackLimitData(TargetPointer threadPointer, out TargetPointer stackBase, out TargetPointer stackLimit, out TargetPointer frameAddress); TargetPointer IdToThread(uint id); -TargetPointer GetThreadLocalStaticBase(TargetPointer threadPointer, int indexOffset, int indexType); +TargetPointer GetThreadLocalStaticBase(TargetPointer threadPointer, TargetPointer tlsIndexPtr); +byte[] GetContext(TargetPointer threadPointer, ThreadContextSource contextSource, uint contextFlags); ``` ## Version 1 @@ -107,6 +115,7 @@ The contract additionally depends on these data descriptors | `Thread` | `CurrentCustomDebuggerNotification` | Handle to the current custom debugger notification object | | `Thread` | `LinkNext` | Pointer to get next thread | | `Thread` | `ExceptionTracker` | Pointer to exception tracking information | +| `Thread` | `DebuggerFilterContext` | Pointer to the debugger filter context for the thread | | `Thread` | `RuntimeThreadLocals` | Pointer to some thread-local storage | | `Thread` | `ThreadLocalDataPtr` | Pointer to thread local data structure | | `Thread` | `UEWatsonBucketTrackerBuckets` | Pointer to thread Watson buckets data (optional, Windows only) | @@ -329,4 +338,28 @@ byte[] IThread.GetWatsonBuckets(TargetPointer threadPointer) return span.ToArray(); } +byte[] IThread.GetContext(TargetPointer threadPointer, ThreadContextSource contextSource, uint contextFlags) +{ + // Allocate a context buffer for the target platform + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(target); + byte[] bytes = new byte[context.Size]; + + TargetPointer filterContext = TargetPointer.Null; + + if (contextSource.HasFlag(ThreadContextSource.Debugger)) + filterContext = target.ReadPointer(threadPointer + /* Thread::DebuggerFilterContext offset */); + + if (filterContext != TargetPointer.Null) + { + // Use the filter context directly + target.ReadBuffer(filterContext, bytes); + return bytes; + } + + // Fall back to the OS thread context + ulong osId = target.ReadNUInt(threadPointer + /* Thread::OSId offset */); + target.GetThreadContext(osId, contextFlags, bytes); + return bytes; +} + ``` diff --git a/src/coreclr/debug/daccess/dacdbiimpl.cpp b/src/coreclr/debug/daccess/dacdbiimpl.cpp index a294a1c0e30137..fcd5363107575e 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.cpp +++ b/src/coreclr/debug/daccess/dacdbiimpl.cpp @@ -5863,58 +5863,10 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetContext(VMPTR_Thread vmThread, { // If the filter context is NULL, then we use the true context of the thread. pContextBuffer->ContextFlags = DT_CONTEXT_ALL; - HRESULT hr = m_pTarget->GetThreadContext(pThread->GetOSThreadId(), + IfFailThrow(m_pTarget->GetThreadContext(pThread->GetOSThreadId(), pContextBuffer->ContextFlags, sizeof(DT_CONTEXT), - reinterpret_cast(pContextBuffer)); - if (hr == E_NOTIMPL) - { - // GetThreadContext is not implemented on this data target. - // That's why we have to make do with context we can obtain from Frames explicitly stored in Thread object. - // It suffices for managed debugging stackwalk. - REGDISPLAY tmpRd = {}; - T_CONTEXT tmpContext = {}; - FillRegDisplay(&tmpRd, &tmpContext); - - // Going through thread Frames and looking for first (deepest one) one that - // that has context available for stackwalking (SP and PC) - // For example: RedirectedThreadFrame, InlinedCallFrame, DynamicHelperFrame - Frame *frame = pThread->GetFrame(); - - while (frame != NULL && frame != FRAME_TOP) - { -#ifdef FEATURE_INTERPRETER - if (frame->GetFrameIdentifier() == FrameIdentifier::InterpreterFrame) - { - PTR_InterpreterFrame pInterpreterFrame = dac_cast(frame); - pInterpreterFrame->SetContextToInterpMethodContextFrame(&tmpContext); - CopyMemory(pContextBuffer, &tmpContext, sizeof(*pContextBuffer)); - return S_OK; - } -#endif // FEATURE_INTERPRETER - frame->UpdateRegDisplay(&tmpRd); - if (GetRegdisplaySP(&tmpRd) != 0 && GetControlPC(&tmpRd) != 0) - { - UpdateContextFromRegDisp(&tmpRd, &tmpContext); - CopyMemory(pContextBuffer, &tmpContext, sizeof(*pContextBuffer)); - pContextBuffer->ContextFlags = DT_CONTEXT_CONTROL - #if defined(TARGET_AMD64) || defined(TARGET_ARM) - | DT_CONTEXT_INTEGER // DT_CONTEXT_INTEGER is needed to include the frame register on ARM32 and AMD64 architectures - // DT_CONTEXT_CONTROL already includes the frame register for X86 and ARM64 architectures - #endif - ; - return S_OK; - } - frame = frame->Next(); - } - - // It looks like this thread is not running managed code. - ZeroMemory(pContextBuffer, sizeof(*pContextBuffer)); - } - else - { - IfFailThrow(hr); - } + reinterpret_cast(pContextBuffer))); } else { diff --git a/src/coreclr/debug/daccess/dacdbiimplstackwalk.cpp b/src/coreclr/debug/daccess/dacdbiimplstackwalk.cpp index c63d28d2d1f06c..7e0b3280d3589d 100644 --- a/src/coreclr/debug/daccess/dacdbiimplstackwalk.cpp +++ b/src/coreclr/debug/daccess/dacdbiimplstackwalk.cpp @@ -723,7 +723,6 @@ FramePointer DacDbiInterfaceImpl::GetFramePointerWorker(StackFrameIterator * pIt } // Return TRUE if the specified CONTEXT is the CONTEXT of the leaf frame. -// @dbgtodo filter CONTEXT - Currently we check for the filter CONTEXT first. HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::IsLeafFrame(VMPTR_Thread vmThread, const DT_CONTEXT * pContext, OUT BOOL * pResult) { DD_ENTER_MAY_THROW; @@ -733,7 +732,12 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::IsLeafFrame(VMPTR_Thread vmThread { DT_CONTEXT ctxLeaf; - IfFailThrow(GetContext(vmThread, &ctxLeaf)); + Thread * pThread = vmThread.GetDacPtr(); + ctxLeaf.ContextFlags = DT_CONTEXT_ALL; + IfFailThrow(m_pTarget->GetThreadContext(pThread->GetOSThreadId(), + ctxLeaf.ContextFlags, + sizeof(DT_CONTEXT), + reinterpret_cast(&ctxLeaf))); // Call a platform-specific helper to compare the two contexts. *pResult = CompareControlRegisters(pContext, &ctxLeaf); diff --git a/src/coreclr/debug/inc/amd64/primitives.h b/src/coreclr/debug/inc/amd64/primitives.h index 83afbbbba7d327..678badee1136a9 100644 --- a/src/coreclr/debug/inc/amd64/primitives.h +++ b/src/coreclr/debug/inc/amd64/primitives.h @@ -147,14 +147,12 @@ inline void CORDbgSetSP(DT_CONTEXT *context, LPVOID rsp) #define CORDbgSetFP(context, rbp) #define CORDbgGetFP(context) 0 -// compare the RIP, RSP, and RBP inline BOOL CompareControlRegisters(const DT_CONTEXT * pCtx1, const DT_CONTEXT * pCtx2) { LIMITED_METHOD_DAC_CONTRACT; if ((pCtx1->Rip == pCtx2->Rip) && - (pCtx1->Rsp == pCtx2->Rsp) && - (pCtx1->Rbp == pCtx2->Rbp)) + (pCtx1->Rsp == pCtx2->Rsp)) { return TRUE; } diff --git a/src/coreclr/debug/inc/arm/primitives.h b/src/coreclr/debug/inc/arm/primitives.h index 269281eb006bed..bb585412a26e02 100644 --- a/src/coreclr/debug/inc/arm/primitives.h +++ b/src/coreclr/debug/inc/arm/primitives.h @@ -117,13 +117,10 @@ inline LPVOID CORDbgGetFP(DT_CONTEXT* context) return (LPVOID)(UINT_PTR)0; } -// compare the EIP, ESP, and EBP inline BOOL CompareControlRegisters(const DT_CONTEXT * pCtx1, const DT_CONTEXT * pCtx2) { LIMITED_METHOD_DAC_CONTRACT; - // @ARMTODO: Sort out frame registers - if ((pCtx1->Pc == pCtx2->Pc) && (pCtx1->Sp == pCtx2->Sp)) { diff --git a/src/coreclr/debug/inc/arm64/primitives.h b/src/coreclr/debug/inc/arm64/primitives.h index 5f8b5262d993e4..cf4023e7ab2ca9 100644 --- a/src/coreclr/debug/inc/arm64/primitives.h +++ b/src/coreclr/debug/inc/arm64/primitives.h @@ -132,11 +132,8 @@ inline BOOL CompareControlRegisters(const DT_CONTEXT * pCtx1, const DT_CONTEXT * { LIMITED_METHOD_DAC_CONTRACT; - // @ARMTODO: Sort out frame registers - if ((pCtx1->Pc == pCtx2->Pc) && - (pCtx1->Sp == pCtx2->Sp) && - (pCtx1->Fp == pCtx2->Fp)) + (pCtx1->Sp == pCtx2->Sp)) { return TRUE; } diff --git a/src/coreclr/debug/inc/i386/primitives.h b/src/coreclr/debug/inc/i386/primitives.h index 757614e185fa00..d0c55986ea96e1 100644 --- a/src/coreclr/debug/inc/i386/primitives.h +++ b/src/coreclr/debug/inc/i386/primitives.h @@ -108,14 +108,12 @@ inline LPVOID CORDbgGetFP(DT_CONTEXT* context) return (LPVOID)(UINT_PTR)context->Ebp; } -// compare the EIP, ESP, and EBP inline BOOL CompareControlRegisters(const DT_CONTEXT * pCtx1, const DT_CONTEXT * pCtx2) { LIMITED_METHOD_DAC_CONTRACT; if ((pCtx1->Eip == pCtx2->Eip) && - (pCtx1->Esp == pCtx2->Esp) && - (pCtx1->Ebp == pCtx2->Ebp)) + (pCtx1->Esp == pCtx2->Esp)) { return TRUE; } diff --git a/src/coreclr/debug/inc/loongarch64/primitives.h b/src/coreclr/debug/inc/loongarch64/primitives.h index cfe46955fe7c8a..f6309552e5ebf3 100644 --- a/src/coreclr/debug/inc/loongarch64/primitives.h +++ b/src/coreclr/debug/inc/loongarch64/primitives.h @@ -118,11 +118,8 @@ inline BOOL CompareControlRegisters(const DT_CONTEXT * pCtx1, const DT_CONTEXT * { LIMITED_METHOD_DAC_CONTRACT; - // TODO-LoongArch64: Sort out frame registers - if ((pCtx1->Pc == pCtx2->Pc) && - (pCtx1->Sp == pCtx2->Sp) && - (pCtx1->Fp == pCtx2->Fp)) + (pCtx1->Sp == pCtx2->Sp)) { return TRUE; } diff --git a/src/coreclr/debug/inc/riscv64/primitives.h b/src/coreclr/debug/inc/riscv64/primitives.h index 17ace22981c77d..ed4f15d6018a63 100644 --- a/src/coreclr/debug/inc/riscv64/primitives.h +++ b/src/coreclr/debug/inc/riscv64/primitives.h @@ -119,11 +119,8 @@ inline BOOL CompareControlRegisters(const DT_CONTEXT * pCtx1, const DT_CONTEXT * { LIMITED_METHOD_DAC_CONTRACT; - // TODO-RISCV64: Sort out frame registers - if ((pCtx1->Pc == pCtx2->Pc) && - (pCtx1->Sp == pCtx2->Sp) && - (pCtx1->Fp == pCtx2->Fp)) + (pCtx1->Sp == pCtx2->Sp)) { return TRUE; } diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 0abc2b43839d49..64528180895016 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -45,9 +45,6 @@ CDAC_TYPE_FIELD(Thread, T_POINTER, CachedStackBase, cdac_data::CachedSta CDAC_TYPE_FIELD(Thread, T_POINTER, CachedStackLimit, cdac_data::CachedStackLimit) CDAC_TYPE_FIELD(Thread, T_POINTER, ExceptionTracker, cdac_data::ExceptionTracker) CDAC_TYPE_FIELD(Thread, T_POINTER, DebuggerFilterContext, cdac_data::DebuggerFilterContext) -#ifdef PROFILING_SUPPORTED -CDAC_TYPE_FIELD(Thread, T_POINTER, ProfilerFilterContext, cdac_data::ProfilerFilterContext) -#endif // PROFILING_SUPPORTED CDAC_TYPE_FIELD(Thread, TYPE(ObjectHandle), ExposedObject, cdac_data::ExposedObject) CDAC_TYPE_FIELD(Thread, TYPE(ObjectHandle), LastThrownObject, cdac_data::LastThrownObject) CDAC_TYPE_FIELD(Thread, T_UINT32, LastThrownObjectIsUnhandled, cdac_data::LastThrownObjectIsUnhandled) diff --git a/src/coreclr/vm/threads.h b/src/coreclr/vm/threads.h index ade9b6ad804998..18aad8dcd2c5e3 100644 --- a/src/coreclr/vm/threads.h +++ b/src/coreclr/vm/threads.h @@ -3768,9 +3768,6 @@ struct cdac_data "Thread::m_ExceptionState is of type ThreadExceptionState"); static constexpr size_t ExceptionTracker = offsetof(Thread, m_ExceptionState) + offsetof(ThreadExceptionState, m_pCurrentTracker); static constexpr size_t DebuggerFilterContext = offsetof(Thread, m_debuggerFilterContext); -#ifdef PROFILING_SUPPORTED - static constexpr size_t ProfilerFilterContext = offsetof(Thread, m_pProfilerFilterContext); -#endif // PROFILING_SUPPORTED #ifndef TARGET_UNIX static constexpr size_t UEWatsonBucketTrackerBuckets = offsetof(Thread, m_ExceptionState) + offsetof(ThreadExceptionState, m_UEWatsonBucketTracker) + offsetof(EHWatsonBucketTracker, m_WatsonUnhandledInfo.m_pUnhandledBuckets); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs index 8d30a898632f76..08a6d8d291850c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs @@ -5,6 +5,13 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts; +[Flags] +public enum ThreadContextSource +{ + None = 0, + Debugger = 1, +} + public record struct ThreadStoreData( int ThreadCount, TargetPointer FirstThread, @@ -60,6 +67,7 @@ void GetStackLimitData(TargetPointer threadPointer, out TargetPointer stackBase, TargetPointer GetThreadLocalStaticBase(TargetPointer threadPointer, TargetPointer tlsIndexPtr) => throw new NotImplementedException(); TargetPointer GetCurrentExceptionHandle(TargetPointer threadPointer) => throw new NotImplementedException(); byte[] GetWatsonBuckets(TargetPointer threadPointer) => throw new NotImplementedException(); + byte[] GetContext(TargetPointer threadPointer, ThreadContextSource contextSource, uint contextFlags) => throw new NotImplementedException(); } public readonly struct Thread : IThread diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64Context.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64Context.cs index 8ab2e2468da526..2ef6e567420edc 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64Context.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64Context.cs @@ -31,7 +31,9 @@ public enum ContextFlagsValues : uint } public readonly uint Size => 0x4d0; - public readonly uint DefaultContextFlags => (uint)ContextFlagsValues.CONTEXT_ALL; + public readonly uint FullContextFlags => (uint)ContextFlagsValues.CONTEXT_FULL; + + public readonly uint AllContextFlags => (uint)ContextFlagsValues.CONTEXT_ALL; public readonly int StackPointerRegister => 4; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM64Context.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM64Context.cs index f88f9a9ecfb62b..1f9508e517e1cb 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM64Context.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARM64Context.cs @@ -24,7 +24,7 @@ public enum ContextFlagsValues : uint CONTEXT_X18 = CONTEXT_ARM64 | 0x10, CONTEXT_XSTATE = CONTEXT_ARM64 | 0x20, CONTEXT_FULL = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_FLOATING_POINT, - CONTEXT_ALL = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_FLOATING_POINT | CONTEXT_DEBUG_REGISTERS | CONTEXT_X18, + CONTEXT_ALL = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_FLOATING_POINT | CONTEXT_DEBUG_REGISTERS, // // This flag is set by the unwinder if it has unwound to a call @@ -38,10 +38,9 @@ public enum ContextFlagsValues : uint public readonly uint Size => 0x390; - public readonly uint DefaultContextFlags => (uint)(ContextFlagsValues.CONTEXT_CONTROL | - ContextFlagsValues.CONTEXT_INTEGER | - ContextFlagsValues.CONTEXT_FLOATING_POINT | - ContextFlagsValues.CONTEXT_DEBUG_REGISTERS); + public readonly uint FullContextFlags => (uint)ContextFlagsValues.CONTEXT_FULL; + + public readonly uint AllContextFlags => (uint)ContextFlagsValues.CONTEXT_ALL; public readonly int StackPointerRegister => 31; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARMContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARMContext.cs index abbcd8805257a8..8fedf1cdd5a83e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARMContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ARMContext.cs @@ -29,7 +29,9 @@ public enum ContextFlagsValues : uint } public readonly uint Size => 0x1a0; - public readonly uint DefaultContextFlags => (uint)ContextFlagsValues.CONTEXT_ALL; + public readonly uint FullContextFlags => (uint)ContextFlagsValues.CONTEXT_FULL; + + public readonly uint AllContextFlags => (uint)ContextFlagsValues.CONTEXT_ALL; public readonly int StackPointerRegister => 13; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ContextHolder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ContextHolder.cs index c2bffd2ad361b8..c952ea17a50829 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ContextHolder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/ContextHolder.cs @@ -12,7 +12,8 @@ public sealed class ContextHolder : IPlatformAgnosticContext, IEquatable Context.Size; - public uint DefaultContextFlags => Context.DefaultContextFlags; + public uint FullContextFlags => Context.FullContextFlags; + public uint AllContextFlags => Context.AllContextFlags; public int StackPointerRegister => Context.StackPointerRegister; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs index c95012b12b74a8..6d42bba8fff30d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs @@ -8,7 +8,8 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; public interface IPlatformAgnosticContext { public abstract uint Size { get; } - public abstract uint DefaultContextFlags { get; } + public abstract uint FullContextFlags { get; } + public abstract uint AllContextFlags { get; } public int StackPointerRegister { get; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformContext.cs index df26023ee54a4f..01ef3f60aed42a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformContext.cs @@ -6,7 +6,8 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; public interface IPlatformContext { uint Size { get; } - uint DefaultContextFlags { get; } + uint FullContextFlags { get; } + uint AllContextFlags { get; } int StackPointerRegister { get; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/LoongArch64Context.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/LoongArch64Context.cs index 6acf124a0d11c1..5fbe851e1b7105 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/LoongArch64Context.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/LoongArch64Context.cs @@ -36,10 +36,9 @@ public enum ContextFlagsValues : uint public readonly uint Size => 0x320; - public readonly uint DefaultContextFlags => (uint)(ContextFlagsValues.CONTEXT_CONTROL | - ContextFlagsValues.CONTEXT_INTEGER | - ContextFlagsValues.CONTEXT_FLOATING_POINT | - ContextFlagsValues.CONTEXT_DEBUG_REGISTERS); + public readonly uint FullContextFlags => (uint)ContextFlagsValues.CONTEXT_FULL; + + public readonly uint AllContextFlags => (uint)ContextFlagsValues.CONTEXT_ALL; public readonly int StackPointerRegister => 3; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/RISCV64Context.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/RISCV64Context.cs index d401d90d89cda3..e4afafa0ecfece 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/RISCV64Context.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/RISCV64Context.cs @@ -36,7 +36,9 @@ public enum ContextFlagsValues : uint public readonly uint Size => 0x220; - public readonly uint DefaultContextFlags => (uint)ContextFlagsValues.CONTEXT_ALL; + public readonly uint FullContextFlags => (uint)ContextFlagsValues.CONTEXT_FULL; + + public readonly uint AllContextFlags => (uint)ContextFlagsValues.CONTEXT_ALL; public readonly int StackPointerRegister => 2; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86Context.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86Context.cs index 505da9a8d52889..3a32e77bb76df8 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86Context.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86Context.cs @@ -38,7 +38,9 @@ public enum ContextFlagsValues : uint } public readonly uint Size => 0x2cc; - public readonly uint DefaultContextFlags => (uint)ContextFlagsValues.CONTEXT_ALL; + public readonly uint FullContextFlags => (uint)ContextFlagsValues.CONTEXT_FULL; + + public readonly uint AllContextFlags => (uint)ContextFlagsValues.CONTEXT_ALL; public readonly int StackPointerRegister => 4; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs index e7a3222806464b..3a09197254291a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs @@ -49,6 +49,12 @@ private record StackDataFrameHandle( bool IsActiveFrame = false) : IStackDataFrameHandle { } + private enum ContextFlags + { + Full = 0x1, + All = 0x2, + } + private class StackWalkData(IPlatformAgnosticContext context, StackWalkState state, FrameIterator frameIter, ThreadData threadData) { public IPlatformAgnosticContext Context { get; set; } = context; @@ -106,7 +112,7 @@ public StackDataFrameHandle ToDataFrame() } IEnumerable IStackWalk.CreateStackWalk(ThreadData threadData) - => CreateStackWalkCore(threadData, skipInitialFrames: false); + => CreateStackWalkCore(threadData, skipInitialFrames: false, flags: ContextFlags.All); /// /// Core stack walk implementation. @@ -121,10 +127,16 @@ IEnumerable IStackWalk.CreateStackWalk(ThreadData threadD /// Must be false for ClrDataStackWalk, which advances the cDAC and legacy DAC in /// lockstep and must yield the same frame sequence (including initial skipped frames). /// - private IEnumerable CreateStackWalkCore(ThreadData threadData, bool skipInitialFrames) + private IEnumerable CreateStackWalkCore(ThreadData threadData, bool skipInitialFrames, ContextFlags flags) { IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(_target); - FillContextFromThread(context, threadData); + uint contextFlags = flags switch + { + ContextFlags.Full => context.FullContextFlags, + ContextFlags.All => context.AllContextFlags, + _ => throw new ArgumentOutOfRangeException(nameof(flags)), + }; + FillContextFromThread(context, threadData, contextFlags); StackWalkState state = IsManaged(context.InstructionPointer, out _) ? StackWalkState.SW_FRAMELESS : StackWalkState.SW_FRAME; FrameIterator frameIterator = new(_target, threadData); @@ -176,7 +188,7 @@ private IEnumerable CreateStackWalkCore(ThreadData thread IReadOnlyList IStackWalk.WalkStackReferences(ThreadData threadData) { - IEnumerable stackFrames = CreateStackWalkCore(threadData, skipInitialFrames: true); + IEnumerable stackFrames = CreateStackWalkCore(threadData, skipInitialFrames: true, flags: ContextFlags.Full); IEnumerable frames = stackFrames.Select(AssertCorrectHandle); IEnumerable gcFrames = Filter(frames); @@ -758,39 +770,13 @@ private bool IsManaged(TargetPointer ip, [NotNullWhen(true)] out CodeBlockHandle return false; } - private void FillContextFromThread(IPlatformAgnosticContext context, ThreadData threadData) + private void FillContextFromThread(IPlatformAgnosticContext context, ThreadData threadData, uint flags) { - byte[] bytes = new byte[context.Size]; - Span buffer = new Span(bytes); - - // Match the native DacStackReferenceWalker behavior: if the thread has a - // FilterContext or ProfilerFilterContext set, use that instead of calling - // GetThreadContext. During debugger breaks, GC stress redirection, or - // profiler stack walks, these contexts hold the correct managed frame state. - Data.Thread thread = _target.ProcessedData.GetOrAdd(threadData.ThreadAddress); - - TargetPointer filterContext = thread.DebuggerFilterContext; - if (filterContext == TargetPointer.Null) - filterContext = thread.ProfilerFilterContext; - - if (filterContext != TargetPointer.Null) - { - _target.ReadBuffer(filterContext.Value, buffer); - context.FillFromBuffer(buffer); - return; - } - - // The underlying ICLRDataTarget.GetThreadContext has some variance depending on the host. - // SOS's managed implementation sets the ContextFlags to platform specific values defined in ThreadService.cs (diagnostics repo) - // SOS's native implementation keeps the ContextFlags passed into this function. - // To match the DAC behavior, the DefaultContextFlags are what the DAC passes in in DacGetThreadContext. - // In most implementations, this will be overridden by the host, but in some cases, it may not be. - if (!_target.TryGetThreadContext(threadData.OSId.Value, context.DefaultContextFlags, buffer)) - { - throw new InvalidOperationException($"GetThreadContext failed for thread {threadData.OSId.Value}"); - } - - context.FillFromBuffer(buffer); + byte[] bytes = _target.Contracts.Thread.GetContext( + threadData.ThreadAddress, + ThreadContextSource.Debugger, + flags); + context.FillFromBuffer(bytes); } private static StackDataFrameHandle AssertCorrectHandle(IStackDataFrameHandle stackDataFrameHandle) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs index 4e2d60fc66f341..7f06982a505985 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; namespace Microsoft.Diagnostics.DataContractReader.Contracts; @@ -269,4 +270,31 @@ byte[] IThread.GetWatsonBuckets(TargetPointer threadPointer) _target.ReadBuffer(readFrom, rval); return rval; } + + byte[] IThread.GetContext(TargetPointer threadPointer, ThreadContextSource contextSource, uint contextFlags) + { + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(_target); + byte[] bytes = new byte[context.Size]; + Span buffer = new Span(bytes); + + Data.Thread thread = _target.ProcessedData.GetOrAdd(threadPointer); + + TargetPointer filterContext = TargetPointer.Null; + + if (contextSource.HasFlag(ThreadContextSource.Debugger)) + filterContext = thread.DebuggerFilterContext; + + if (filterContext != TargetPointer.Null) + { + _target.ReadBuffer(filterContext.Value, buffer); + return bytes; + } + + if (!_target.TryGetThreadContext(thread.OSId.Value, contextFlags, buffer)) + { + throw new InvalidOperationException($"GetThreadContext failed for thread {thread.OSId.Value}"); + } + + return bytes; + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs index 13164ca06db7cf..7474f8b0272a31 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs @@ -34,7 +34,6 @@ public Thread(Target target, TargetPointer address) UEWatsonBucketTrackerBuckets = target.ReadPointerFieldOrNull(address, type, nameof(UEWatsonBucketTrackerBuckets)); ThreadLocalDataPtr = target.ReadPointerField(address, type, nameof(ThreadLocalDataPtr)); DebuggerFilterContext = target.ReadPointerField(address, type, nameof(DebuggerFilterContext)); - ProfilerFilterContext = target.ReadPointerFieldOrNull(address, type, nameof(ProfilerFilterContext)); CurrentCustomDebuggerNotification = target.ReadDataField(address, type, nameof(CurrentCustomDebuggerNotification)); } @@ -54,6 +53,5 @@ public Thread(Target target, TargetPointer address) public TargetPointer UEWatsonBucketTrackerBuckets { get; init; } public TargetPointer ThreadLocalDataPtr { get; init; } public TargetPointer DebuggerFilterContext { get; init; } - public TargetPointer ProfilerFilterContext { get; init; } public ObjectHandle CurrentCustomDebuggerNotification { get; init; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs index 547f4bbbf59621..032640bdd23ab7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs @@ -8,6 +8,7 @@ using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; using DACF = Microsoft.Diagnostics.DataContractReader.Contracts.DebuggerAssemblyControlFlags; namespace Microsoft.Diagnostics.DataContractReader.Legacy; @@ -927,11 +928,80 @@ public int GetStackParameterSize(ulong controlPC, uint* pRetVal) public int GetFramePointer(nuint pSFIHandle, ulong* pRetVal) => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.GetFramePointer(pSFIHandle, pRetVal) : HResults.E_NOTIMPL; - public int IsLeafFrame(ulong vmThread, nint pContext, Interop.BOOL* pResult) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.IsLeafFrame(vmThread, pContext, pResult) : HResults.E_NOTIMPL; + public int IsLeafFrame(ulong vmThread, byte* pContext, Interop.BOOL* pResult) + { + *pResult = Interop.BOOL.FALSE; + int hr = HResults.S_OK; + try + { + IPlatformAgnosticContext leafCtx = IPlatformAgnosticContext.GetContextForPlatform(_target); + uint allFlags = leafCtx.AllContextFlags; + byte[] leafContext = _target.Contracts.Thread.GetContext(new TargetPointer(vmThread), ThreadContextSource.None, allFlags); + leafCtx.FillFromBuffer(leafContext); + + // Read the given context from the native buffer. + IPlatformAgnosticContext givenCtx = IPlatformAgnosticContext.GetContextForPlatform(_target); + givenCtx.FillFromBuffer(new Span(pContext, leafContext.Length)); + + *pResult = givenCtx.StackPointer == leafCtx.StackPointer + && givenCtx.InstructionPointer == leafCtx.InstructionPointer + ? Interop.BOOL.TRUE : Interop.BOOL.FALSE; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacy is not null) + { + Interop.BOOL resultLocal; + int hrLocal = _legacy.IsLeafFrame(vmThread, pContext, &resultLocal); + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + Debug.Assert(*pResult == resultLocal, $"cDAC: {*pResult}, DAC: {resultLocal}"); + } +#endif + return hr; + } - public int GetContext(ulong vmThread, nint pContextBuffer) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.GetContext(vmThread, pContextBuffer) : HResults.E_NOTIMPL; + public int GetContext(ulong vmThread, byte* pContextBuffer) + { + int hr = HResults.S_OK; + try + { + uint allFlags = IPlatformAgnosticContext.GetContextForPlatform(_target).AllContextFlags; + byte[] context = _target.Contracts.Thread.GetContext(new TargetPointer(vmThread), ThreadContextSource.Debugger, allFlags); + + context.AsSpan().CopyTo(new Span(pContextBuffer, context.Length)); + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacy is not null) + { + uint contextSize = IPlatformAgnosticContext.GetContextForPlatform(_target).Size; + byte[] localContextBuf = new byte[contextSize]; + fixed (byte* pLocal = localContextBuf) + { + int hrLocal = _legacy.GetContext(vmThread, pLocal); + Debug.ValidateHResult(hr, hrLocal); + + if (hr == HResults.S_OK) + { + IPlatformAgnosticContext contextStruct = IPlatformAgnosticContext.GetContextForPlatform(_target); + IPlatformAgnosticContext localContextStruct = IPlatformAgnosticContext.GetContextForPlatform(_target); + contextStruct.FillFromBuffer(new Span(pContextBuffer, (int)contextSize)); + localContextStruct.FillFromBuffer(localContextBuf); + + Debug.Assert(contextStruct.Equals(localContextStruct)); + } + } + } +#endif + return hr; + } public int ConvertContextToDebuggerRegDisplay(nint pInContext, nint pOutDRD, Interop.BOOL fActive) => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.ConvertContextToDebuggerRegDisplay(pInContext, pOutDRD, fActive) : HResults.E_NOTIMPL; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs index fe2d48073b3961..28970787a37c94 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs @@ -320,10 +320,10 @@ public unsafe partial interface IDacDbiInterface int GetFramePointer(nuint pSFIHandle, ulong* pRetVal); [PreserveSig] - int IsLeafFrame(ulong vmThread, nint pContext, Interop.BOOL* pResult); + int IsLeafFrame(ulong vmThread, byte* pContext, Interop.BOOL* pResult); [PreserveSig] - int GetContext(ulong vmThread, nint pContextBuffer); + int GetContext(ulong vmThread, byte* pContextBuffer); [PreserveSig] int ConvertContextToDebuggerRegDisplay(nint pInContext, nint pOutDRD, Interop.BOOL fActive); diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiStackWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiStackWalkDumpTests.cs new file mode 100644 index 00000000000000..14b061abe0fc8e --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiStackWalkDumpTests.cs @@ -0,0 +1,138 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; +using Microsoft.Diagnostics.DataContractReader.Legacy; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Dump-based integration tests for DacDbiImpl stack walk methods (IsLeafFrame, GetContext). +/// Uses the StackWalk debuggee (full dump). +/// +public class DacDbiStackWalkDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "StackWalk"; + protected override string DumpType => "full"; + + private DacDbiImpl CreateDacDbi() => new DacDbiImpl(Target, legacyObj: null); + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public unsafe void GetContext_Succeeds_ForCrashingThread(TestConfiguration config) + { + InitializeDumpTest(config); + DacDbiImpl dbi = CreateDacDbi(); + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + uint contextSize = IPlatformAgnosticContext.GetContextForPlatform(Target).Size; + byte[] contextBuffer = new byte[contextSize]; + + fixed (byte* pContext = contextBuffer) + { + int hr = dbi.GetContext(crashingThread.ThreadAddress, pContext); + Assert.Equal(System.HResults.S_OK, hr); + } + + IPlatformAgnosticContext ctx = IPlatformAgnosticContext.GetContextForPlatform(Target); + ctx.FillFromBuffer(contextBuffer); + Assert.NotEqual(TargetPointer.Null, ctx.InstructionPointer); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public unsafe void GetContext_MatchesContractGetContext(TestConfiguration config) + { + InitializeDumpTest(config); + DacDbiImpl dbi = CreateDacDbi(); + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + uint contextSize = IPlatformAgnosticContext.GetContextForPlatform(Target).Size; + + byte[] dbiContextBuffer = new byte[contextSize]; + fixed (byte* pContext = dbiContextBuffer) + { + int hr = dbi.GetContext(crashingThread.ThreadAddress, pContext); + Assert.Equal(System.HResults.S_OK, hr); + } + + uint allFlags = IPlatformAgnosticContext.GetContextForPlatform(Target).AllContextFlags; + byte[] contractContext = Target.Contracts.Thread.GetContext(crashingThread.ThreadAddress, ThreadContextSource.Debugger, allFlags); + + IPlatformAgnosticContext dbiCtx = IPlatformAgnosticContext.GetContextForPlatform(Target); + IPlatformAgnosticContext contractCtx = IPlatformAgnosticContext.GetContextForPlatform(Target); + dbiCtx.FillFromBuffer(dbiContextBuffer); + contractCtx.FillFromBuffer(contractContext); + + Assert.Equal(contractCtx.InstructionPointer, dbiCtx.InstructionPointer); + Assert.Equal(contractCtx.StackPointer, dbiCtx.StackPointer); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public unsafe void IsLeafFrame_TrueForLeafContext(TestConfiguration config) + { + InitializeDumpTest(config); + DacDbiImpl dbi = CreateDacDbi(); + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + + uint allFlags = IPlatformAgnosticContext.GetContextForPlatform(Target).AllContextFlags; + byte[] leafContext = Target.Contracts.Thread.GetContext(crashingThread.ThreadAddress, ThreadContextSource.None, allFlags); + + Interop.BOOL result; + fixed (byte* pContext = leafContext) + { + int hr = dbi.IsLeafFrame(crashingThread.ThreadAddress, pContext, &result); + Assert.Equal(System.HResults.S_OK, hr); + } + + Assert.Equal(Interop.BOOL.TRUE, result); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public unsafe void IsLeafFrame_FalseForNonLeafContext(TestConfiguration config) + { + InitializeDumpTest(config); + DacDbiImpl dbi = CreateDacDbi(); + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + + uint allFlags = IPlatformAgnosticContext.GetContextForPlatform(Target).AllContextFlags; + byte[] leafContext = Target.Contracts.Thread.GetContext(crashingThread.ThreadAddress, ThreadContextSource.None, allFlags); + IPlatformAgnosticContext leafCtx = IPlatformAgnosticContext.GetContextForPlatform(Target); + leafCtx.FillFromBuffer(leafContext); + + IStackWalk sw = Target.Contracts.StackWalk; + + // Find a frame whose SP+IP differs from the leaf context + byte[]? nonLeafContext = sw.CreateStackWalk(crashingThread) + .Select(sw.GetRawContext) + .FirstOrDefault(ctx => + { + IPlatformAgnosticContext frameCtx = IPlatformAgnosticContext.GetContextForPlatform(Target); + frameCtx.FillFromBuffer(ctx); + return frameCtx.StackPointer != leafCtx.StackPointer + || frameCtx.InstructionPointer != leafCtx.InstructionPointer; + }); + + Assert.NotNull(nonLeafContext); + + Interop.BOOL result; + fixed (byte* pContext = nonLeafContext) + { + int hr = dbi.IsLeafFrame(crashingThread.ThreadAddress, pContext, &result); + Assert.Equal(System.HResults.S_OK, hr); + } + + Assert.Equal(Interop.BOOL.FALSE, result); + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs index 92e3dc12f1f7a7..c051f3aacf74ac 100644 --- a/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs @@ -331,4 +331,25 @@ public unsafe void VarargPInvoke_GetCodeHeaderDataWithInvalidPrecodeAddress(Test Assert.Fail("Expected to find a frame with a valid entry point"); } + + // ========== GetContext API tests ========== + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + public void GetContext_ReturnsNonEmptyContext(TestConfiguration config) + { + InitializeDumpTest(config); + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + uint allFlags = Contracts.StackWalkHelpers.IPlatformAgnosticContext.GetContextForPlatform(Target).AllContextFlags; + byte[] context = Target.Contracts.Thread.GetContext(crashingThread.ThreadAddress, ThreadContextSource.None, allFlags); + + Assert.NotNull(context); + Assert.True(context.Length > 0, "Expected non-empty context"); + + var ctx = Contracts.StackWalkHelpers.IPlatformAgnosticContext.GetContextForPlatform(Target); + ctx.FillFromBuffer(context); + Assert.NotEqual(TargetPointer.Null, ctx.InstructionPointer); + } } diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs index bfa7b2a8b5416b..cf6dd55ee67f3b 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs @@ -187,9 +187,8 @@ internal sealed class MockThread : TypedView private const string ThreadLocalDataPtrFieldName = "ThreadLocalDataPtr"; private const string UEWatsonBucketTrackerBucketsFieldName = "UEWatsonBucketTrackerBuckets"; private const string DebuggerFilterContextFieldName = "DebuggerFilterContext"; - private const string ProfilerFilterContextFieldName = "ProfilerFilterContext"; - public static Layout CreateLayout(MockTarget.Architecture architecture, bool hasProfilingSupport = true) + public static Layout CreateLayout(MockTarget.Architecture architecture) { SequentialLayoutBuilder layoutBuilder = new SequentialLayoutBuilder("Thread", architecture) .AddUInt32Field(IdFieldName) @@ -210,11 +209,6 @@ public static Layout CreateLayout(MockTarget.Architecture architectu .AddPointerField(UEWatsonBucketTrackerBucketsFieldName) .AddPointerField(DebuggerFilterContextFieldName); - if (hasProfilingSupport) - { - layoutBuilder.AddPointerField(ProfilerFilterContextFieldName); - } - return layoutBuilder.Build(); } @@ -307,19 +301,19 @@ internal sealed class MockThreadBuilder private MockThread? _previousThread; - public MockThreadBuilder(MockMemorySpace.Builder builder, bool hasProfilingSupport = true) - : this(builder, (DefaultAllocationRangeStart, DefaultAllocationRangeEnd), hasProfilingSupport) + public MockThreadBuilder(MockMemorySpace.Builder builder) + : this(builder, (DefaultAllocationRangeStart, DefaultAllocationRangeEnd)) { } - public MockThreadBuilder(MockMemorySpace.Builder builder, (ulong Start, ulong End) allocationRange, bool hasProfilingSupport = true) + public MockThreadBuilder(MockMemorySpace.Builder builder, (ulong Start, ulong End) allocationRange) { Builder = builder; _allocator = Builder.CreateAllocator(allocationRange.Start, allocationRange.End); TargetTestHelpers helpers = builder.TargetTestHelpers; ExceptionInfoLayout = MockExceptionInfo.CreateLayout(helpers.Arch); - ThreadLayout = MockThread.CreateLayout(helpers.Arch, hasProfilingSupport); + ThreadLayout = MockThread.CreateLayout(helpers.Arch); ThreadStoreLayout = MockThreadStore.CreateLayout(helpers.Arch); GCAllocContextLayout = MockGCAllocContext.CreateLayout(helpers.Arch); EEAllocContextLayout = MockEEAllocContext.CreateLayout(helpers.Arch, GCAllocContextLayout); diff --git a/src/native/managed/cdac/tests/ThreadTests.cs b/src/native/managed/cdac/tests/ThreadTests.cs index a9960aaf5d9c0b..d314c9f5cf2432 100644 --- a/src/native/managed/cdac/tests/ThreadTests.cs +++ b/src/native/managed/cdac/tests/ThreadTests.cs @@ -12,11 +12,10 @@ public unsafe class ThreadTests { private static TestPlaceholderTarget CreateTarget( MockTarget.Architecture arch, - Action configure, - bool hasProfilingSupport = true) + Action configure) { TestPlaceholderTarget.Builder targetBuilder = new(arch); - MockThreadBuilder threadBuilder = new(targetBuilder.MemoryBuilder, hasProfilingSupport: hasProfilingSupport); + MockThreadBuilder threadBuilder = new(targetBuilder.MemoryBuilder); configure(threadBuilder); TestPlaceholderTarget target = targetBuilder @@ -277,25 +276,4 @@ public void GetCurrentExceptionHandle_HandlePointsToNull(MockTarget.Architecture TargetPointer thrownObjectHandle = contract.GetCurrentExceptionHandle(new TargetPointer(thread!.Address)); Assert.Equal(TargetPointer.Null, thrownObjectHandle); } - - [Theory] - [ClassData(typeof(MockTarget.StdArch))] - public void GetThreadData_NoProfilerFilterContext(MockTarget.Architecture arch) - { - const uint id = 1; - const ulong osId = 1234; - MockThread? thread = null; - - TestPlaceholderTarget target = CreateTarget( - arch, - threadBuilder => thread = threadBuilder.AddThread(id, osId), - hasProfilingSupport: false); - - IThread contract = target.Contracts.Thread; - Assert.NotNull(contract); - - ThreadData data = contract.GetThreadData(new TargetPointer(thread!.Address)); - Assert.Equal(id, data.Id); - Assert.Equal(new TargetNUInt(osId), data.OSId); - } } From 9a6034a3eabe6da4109a930020af0b832bac4e6e Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Wed, 29 Apr 2026 04:19:20 +0200 Subject: [PATCH 002/115] JIT: fix Vector{128,256}.ConvertToInt32 use-before-def in gtNewSimdCvtNode (#127524) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes a use-before-def in the non-AVX-512 saturation path of `Compiler::gtNewSimdCvtNode` that produces wrong results for `Vector{128,256,512}.ConvertToInt32(Vector*)` when the input is not invariant or a local. ## Repro (DOTNET_EnableAVX512=0) ```cs [MethodImpl(MethodImplOptions.NoInlining)] static Vector512 Test() => Vector512.ConvertToInt32(Vector512.Create(float.MinValue)); ``` Before this PR: ``` <0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2147483647, 0, 0, 0> ``` After this PR: ``` <-2147483648, -2147483648, -2147483648, -2147483648, -2147483648, -2147483648, -2147483648, -2147483648, -2147483648, -2147483648, -2147483648, -2147483648, -2147483648, -2147483648, -2147483648, -2147483648> ``` ## Root cause ```cpp GenTree* op1Clone1 = fgMakeMultiUse(&op1); // op1 := COMMA(STORE temp = orig, LCL_VAR temp); op1Clone1 := fresh LCL_VAR temp GenTree* mask1 = gtNewSimdIsNaNNode(type, op1, ...); // uses op1 (the COMMA) fixupVal = gtNewSimdBinOpNode(GT_AND_NOT, op1Clone1, mask1, …); // AND_NOT(LCL_VAR temp, IsNaN(COMMA(STORE temp, ...))) ``` `AND_NOT(a, b)` decomposes into `AND(a, NOT(b))`, so the left operand `a` evaluates first. The `LCL_VAR temp` read happens **before** the `STORE temp` that lives inside the COMMA on the right, so the AND consumes whatever was on the stack. Disasm on main with AVX-512 disabled (only the relevant block, V05 / V08 are the temps in question): ```asm vpcmpeqd ymm0, ymm0, ymm0 vandps ymm0, ymm0, ymmword ptr [rsp+0x20] ; reads V05 - never written vbroadcastss ymm1, dword ptr [reloc @RWD00] vcmpgeps ymm2, ymm0, ymm1 vbroadcastss ymm3, dword ptr [reloc @RWD04] vcvttps2dq ymm0, ymm0 vpblendvb ymm0, ymm0, ymm3, ymm2 vmovups ymm2, ymmword ptr [rsp] ; reads V08 - never written ... ``` The bug has existed since `gtNewSimdCvtNode` was first introduced; it stayed latent because pre-#127124 / #127402 the inner `IsNaN(op1)` expanded into per-element compares that kept enough materialization around to mask the bad ordering. With SIMD32/64 constant propagation, `CompareNotEqual(temp, temp)` value-numbers as AllBitsSet and the whole right subtree collapses to constants, leaving only the broken left-side read - which is exactly what `Vector512Tests.ConvertToInt32Test` started catching on non-AVX-512 hosts. ## Fix Two-line swap: pass `op1` (the COMMA, evaluated first) as `AND_NOT`'s left arg; use the clone for the IsNaN check. ```cpp GenTree* op1Clone1 = fgMakeMultiUse(&op1); GenTree* mask1 = gtNewSimdIsNaNNode(type, op1Clone1, simdSourceBaseType, simdSize); fixupVal = gtNewSimdBinOpNode(GT_AND_NOT, type, op1, mask1, simdSourceBaseType, simdSize); ``` Now the COMMA evaluates first, the STORE happens, both subsequent reads of the temp get the correct value. ## Validation - Repro produces correct output on AVX-512 disabled and AVX-512 enabled. - `Vector512Tests.ConvertToInt32Test` / `ConvertToInt32NativeTest` pass with `DOTNET_EnableAVX512=0`. - SuperPMI replay against `benchmarks.run` clean (38409 contexts, 0 failures, 0 asserts). ## Note on #127499 That PR adds an AVX-512 gate in `impSpecialIntrinsic`'s `NI_Vector512_ConvertToInt32` case. As @tannergooding pointed out in https://github.com/dotnet/runtime/pull/127499#discussion_r3154379943 / https://github.com/dotnet/runtime/pull/127499#discussion_r3154400992, that case is already unreachable on non-AVX-512 hosts because `Compiler::lookupId` returns `NI_Illegal` for `Vector512` ISA when AVX-512 is not opportunistically supported. I verified that applying #127499 alone leaves the test failing - the gate it adds is dead code. This PR addresses the actual bug; #127499 can be closed. Fixes #127440. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/gentree.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coreclr/jit/gentree.cpp b/src/coreclr/jit/gentree.cpp index fd92a2ece09cbf..e9d04fa4ecd156 100644 --- a/src/coreclr/jit/gentree.cpp +++ b/src/coreclr/jit/gentree.cpp @@ -23205,8 +23205,8 @@ GenTree* Compiler::gtNewSimdCvtNode( // mask1 contains the output either 0xFFFFFFFF or 0. // FixupVal zeros out any NaN values in the input by ANDing input with mask1. GenTree* op1Clone1 = fgMakeMultiUse(&op1); - GenTree* mask1 = gtNewSimdIsNaNNode(type, op1, simdSourceBaseType, simdSize); - fixupVal = gtNewSimdBinOpNode(GT_AND_NOT, type, op1Clone1, mask1, simdSourceBaseType, simdSize); + GenTree* mask1 = gtNewSimdIsNaNNode(type, op1Clone1, simdSourceBaseType, simdSize); + fixupVal = gtNewSimdBinOpNode(GT_AND_NOT, type, op1, mask1, simdSourceBaseType, simdSize); } if (varTypeIsSigned(simdTargetBaseType)) From 977f4125aa7cf8fd550f1bb91e966f20a1df6e54 Mon Sep 17 00:00:00 2001 From: Aaron R Robinson Date: Tue, 28 Apr 2026 22:45:57 -0700 Subject: [PATCH 003/115] Add `Type.GetNullableUnderlyingType()` virtual API (#126905) Closes #125388 Fixes #124216 Breaking change documentation: [dotnet/docs#53407](https://github.com/dotnet/docs/issues/53407) ## Summary Adds a new public virtual `Type.GetNullableUnderlyingType()` method so that `Type` subclasses (e.g. `MetadataLoadContext`'s `RoType`) can correctly identify `Nullable` types. `Nullable.GetUnderlyingType()` now forwards to this virtual. This follows the same pattern as `Enum.GetUnderlyingType()` forwarding to `Type.GetEnumUnderlyingType()`. ## Contract - `Type.GetNullableUnderlyingType()` returns a non-`null` result for both: - a constructed `Nullable` (returns `T`), **and** - the generic type definition `typeof(Nullable<>)` (returns the generic type parameter `T`). - `Nullable.GetUnderlyingType(Type)` continues to return `null` for the generic type definition for COMPAT. - The base `Type.GetNullableUnderlyingType()` throws `NotSupportedException`. Custom `Type` subclasses outside the BCL must override it; this is a behavioral breaking change documented in dotnet/docs#53407. ## Changes ### Public API - **`Type.cs`**: New `public virtual Type? GetNullableUnderlyingType()` that throws `NotSupportedException(SR.NotSupported_SubclassOverride)` (matches `IsByRefLike` pattern per @MichalStrehovsky's feedback). XML doc documents that the open generic `Nullable<>` is treated as nullable and yields the generic type parameter. - **`System.Runtime.cs`** / **`System.Reflection.Emit.cs`** (ref assemblies): New API + `TypeDelegator`/`TypeBuilder`/`EnumBuilder`/`GenericTypeParameterBuilder` overrides. ### `Nullable.GetUnderlyingType` rewire - **`Nullable.cs`**: Now delegates to the new virtual after preserving the `IsGenericTypeDefinition` COMPAT short-circuit. ### Runtime overrides (all three runtimes) - **`RuntimeType.CoreCLR.cs`** / **`RuntimeType.Mono.cs`**: Override that handles both constructed and open-generic cases. Open `Nullable<>` returns `GetGenericArguments()[0]` since the native fast-path can't yield a `MethodTable` for the formal type parameter `T`. - **`RuntimeType.NativeAot.cs`**: Same handling for constructed `Nullable` via the EEType fast-path. - **`RuntimeTypeInfo.cs`** (NativeAOT): Added `public virtual` returning `null` (per @jkotas's feedback). - **`NativeFormatRuntimeNamedTypeInfo.cs`** (NativeAOT): Sealed override that returns the generic parameter only when the type is `typeof(Nullable<>)`. - **`RuntimeConstructedGenericTypeInfo.cs`** (NativeAOT): override for constructed generics. ### Reflection subclasses - **`TypeDelegator.cs`**: Override forwarding to `typeImpl.GetNullableUnderlyingType()`. - **`SignatureType.cs` / `SignatureConstructedGenericType.cs` / `SignatureModifiedType.cs`**: Overrides that delegate through the generic definition. - **`ModifiedType.cs`**: Override delegating through the unmodified type. ### Reflection.Emit - **`TypeBuilder.cs` / `EnumBuilder.cs` / `GenericTypeParameterBuilder.cs` / `TypeBuilderInstantiation.cs`**: Overrides returning `null` (or appropriate result for instantiations). - **`SymbolType.cs`**: Override returning `null` so `Nullable.GetUnderlyingType` doesn't throw on `MakeArrayType`/`MakePointerType`/`MakeByRefType` results from `TypeBuilder`. ### MetadataLoadContext - **`RoType.cs`**: Override using `CoreType.NullableT` identity comparison; uses `GetGenericArguments()[0]` so the open `Nullable<>` returns the MLC-projected generic parameter rather than indexing empty `GenericTypeArguments`. - **`RoModifiedType.cs`**: Override delegating through the unmodified type (required because `RoModifiedType.GetGenericTypeDefinition()` throws). ### Tests - **`NullableTests.cs`**: Coverage for `RuntimeType` (constructed + open-generic) and `TypeDelegator`. - **`SignatureTypes.cs`**: Coverage for `SignatureConstructedGenericType` and `SignatureModifiedType`. - **`ModifiedTypeTests.cs`**: New `NullableModifiedTypeHolder` (uses `volatile delegate*` to obtain a `ModifiedType` wrapping `Nullable`) and tests for modified Nullable / non-Nullable. - **`TypeBuilderGetNullableUnderlyingType.cs`** (new): Coverage for `TypeBuilder`, `EnumBuilder`, `GenericTypeParameterBuilder`, `TypeBuilderInstantiation`, and `SymbolType` (Array / multi-dim Array / Pointer / ByRef). - **`TypeTests.Nullable.cs`** (MetadataLoadContext): Coverage for `RoType` (constructed + open-generic). --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jan Kotas Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/System/RuntimeType.CoreCLR.cs | 25 ++++++ .../NativeFormatRuntimeNamedTypeInfo.cs | 5 ++ .../RuntimeConstructedGenericTypeInfo.cs | 5 ++ .../Runtime/TypeInfos/RuntimeTypeInfo.cs | 2 + .../src/System/RuntimeType.NativeAot.cs | 13 +++ .../Common/tests/System/ModifiedTypeTests.cs | 31 +++++++ .../src/System/Nullable.cs | 15 ++-- .../src/System/Reflection/Emit/EnumBuilder.cs | 3 + .../Emit/GenericTypeParameterBuilder.cs | 3 + .../src/System/Reflection/Emit/SymbolType.cs | 2 + .../src/System/Reflection/Emit/TypeBuilder.cs | 2 + .../Emit/TypeBuilderInstantiation.cs | 1 + .../src/System/Reflection/ModifiedType.cs | 1 + .../SignatureConstructedGenericType.cs | 1 + .../Reflection/SignatureModifiedType.cs | 1 + .../src/System/Reflection/SignatureType.cs | 3 + .../src/System/Reflection/TypeDelegator.cs | 1 + .../System.Private.CoreLib/src/System/Type.cs | 12 +++ .../ref/System.Reflection.Emit.cs | 3 + .../tests/System.Reflection.Emit.Tests.csproj | 1 + .../TypeBuilderGetNullableUnderlyingType.cs | 80 +++++++++++++++++++ .../TypeLoading/Types/RoModifiedType.cs | 4 + .../Reflection/TypeLoading/Types/RoType.cs | 20 +++++ ...eflection.MetadataLoadContext.Tests.csproj | 1 + .../src/Tests/Type/TypeTests.Nullable.cs | 75 +++++++++++++++++ .../System.Runtime/ref/System.Runtime.cs | 2 + .../System/NullableTests.cs | 29 +++++++ .../System/Reflection/SignatureTypes.cs | 32 ++++++++ .../src/System/RuntimeType.Mono.cs | 14 +++- 29 files changed, 376 insertions(+), 11 deletions(-) create mode 100644 src/libraries/System.Reflection.Emit/tests/TypeBuilder/TypeBuilderGetNullableUnderlyingType.cs create mode 100644 src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.cs diff --git a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs index 9b8f2d0268c7a0..088587f84ed69d 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs @@ -3591,6 +3591,31 @@ public override GenericParameterAttributes GenericParameterAttributes #endregion #region Generics + + public override unsafe Type? GetNullableUnderlyingType() + { + TypeHandle th = GetNativeTypeHandle(); + if (!th.IsTypeDesc) + { + MethodTable* pMT = th.AsMethodTable(); + if (pMT->IsNullable) + { + // The open generic Nullable<> is also classified as Nullable, and a constructed + // Nullable instantiated over a generic variable holds a TypeDesc (not a + // MethodTable*) in InstantiationArg0(). Fall back to managed reflection in + // those cases. + if (pMT->ContainsGenericVariables) + { + return GetGenericArguments()[0]; + } + RuntimeType result = RuntimeTypeHandle.GetRuntimeTypeFromHandle((IntPtr)pMT->InstantiationArg0()); + GC.KeepAlive(this); + return result; + } + } + return null; + } + internal RuntimeType[] GetGenericArgumentsInternal() { return GetRootElementType().TypeHandle.GetInstantiationInternal(); diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/NativeFormat/NativeFormatRuntimeNamedTypeInfo.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/NativeFormat/NativeFormatRuntimeNamedTypeInfo.cs index 2a0fc6f176cfbf..7284481f7bc4f9 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/NativeFormat/NativeFormatRuntimeNamedTypeInfo.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/NativeFormat/NativeFormatRuntimeNamedTypeInfo.cs @@ -199,6 +199,11 @@ public sealed override string Name protected sealed override IEnumerable TrueCustomAttributes => RuntimeCustomAttributeData.GetCustomAttributes(_reader, _typeDefinition.CustomAttributes); + public sealed override Type? GetNullableUnderlyingType() + { + return (this.ToType() == typeof(Nullable<>)) ? RuntimeGenericTypeParameters[0].ToType() : null; + } + internal sealed override RuntimeTypeInfo[] RuntimeGenericTypeParameters { get diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeConstructedGenericTypeInfo.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeConstructedGenericTypeInfo.cs index 4eaf089573fb1e..8794d70d9d223a 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeConstructedGenericTypeInfo.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeConstructedGenericTypeInfo.cs @@ -85,6 +85,11 @@ public sealed override Type GetGenericTypeDefinition() return GenericTypeDefinitionTypeInfo.ToType(); } + public sealed override Type? GetNullableUnderlyingType() => + GenericTypeDefinitionTypeInfo.ToType() == typeof(Nullable<>) + ? _key.GenericTypeArguments[0].ToType() + : null; + public sealed override Guid GUID { get diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeTypeInfo.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeTypeInfo.cs index 7d99042d8467e2..46b620c9cfc26b 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeTypeInfo.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeTypeInfo.cs @@ -399,6 +399,8 @@ public virtual Type GetGenericTypeDefinition() throw new InvalidOperationException(SR.InvalidOperation_NotGenericType); } + public virtual Type? GetNullableUnderlyingType() => null; + public Type MakeArrayType() { // Do not implement this as a call to MakeArrayType(1) - they are not interchangeable. MakeArrayType() returns a diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs index a0285f166c0eca..97a534a6d567d1 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs @@ -122,6 +122,19 @@ public override Type GetEnumUnderlyingType() return Enum.InternalGetUnderlyingType(this); } + public override Type? GetNullableUnderlyingType() + { + MethodTable* pEEType = _pUnderlyingEEType; + if (pEEType != null) + { + if (!pEEType->IsNullable) + return null; + if (!pEEType->IsGenericTypeDefinition) + return GetTypeFromMethodTable(pEEType->NullableType); + } + return GetRuntimeTypeInfo().GetNullableUnderlyingType(); + } + public override bool IsEnumDefined(object value) { ArgumentNullException.ThrowIfNull(value); diff --git a/src/libraries/Common/tests/System/ModifiedTypeTests.cs b/src/libraries/Common/tests/System/ModifiedTypeTests.cs index 0175ddc892fa7e..2e13c3b8242f60 100644 --- a/src/libraries/Common/tests/System/ModifiedTypeTests.cs +++ b/src/libraries/Common/tests/System/ModifiedTypeTests.cs @@ -867,5 +867,36 @@ public static delegate* delegate* // ret > _fcnPtr_complex; } + [Fact] + public static unsafe void GetNullableUnderlyingType_ModifiedType() + { + FieldInfo fi = typeof(NullableModifiedTypeHolder).Project().GetField(nameof(NullableModifiedTypeHolder._fcnPtr_NullableReturn), Bindings); + + // The function pointer's return type is Nullable. Pulling the modified return type + // produces a ModifiedType wrapping Nullable, which exercises the override. + Type modifiedNullable = fi.GetModifiedFieldType().GetFunctionPointerReturnType(); + Assert.True(IsModifiedType(modifiedNullable)); + Assert.Equal(typeof(int?).Project(), modifiedNullable.UnderlyingSystemType); + + Type modifiedUnderlying = modifiedNullable.GetNullableUnderlyingType(); + Assert.NotNull(modifiedUnderlying); + Assert.True(IsModifiedType(modifiedUnderlying)); + Assert.Equal(typeof(int).Project(), modifiedUnderlying.UnderlyingSystemType); + Assert.Same(modifiedNullable.GetGenericArguments()[0], modifiedUnderlying); + } + + [Fact] + public static unsafe void GetNullableUnderlyingType_ModifiedType_NonNullable_ReturnsNull() + { + FieldInfo fi = typeof(ModifiedTypeHolder).Project().GetField(nameof(ModifiedTypeHolder._volatileInt), Bindings); + Type modifiedInt = fi.GetModifiedFieldType(); + Assert.True(IsModifiedType(modifiedInt)); + Assert.Null(modifiedInt.GetNullableUnderlyingType()); + } + + public unsafe class NullableModifiedTypeHolder + { + public static volatile delegate* _fcnPtr_NullableReturn; + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Nullable.cs b/src/libraries/System.Private.CoreLib/src/System/Nullable.cs index 2eb6e5002968a2..758af84f896c29 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Nullable.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Nullable.cs @@ -104,16 +104,11 @@ public static bool Equals(T? n1, T? n2) where T : struct { ArgumentNullException.ThrowIfNull(nullableType); - if (nullableType.IsGenericType && !nullableType.IsGenericTypeDefinition) - { - // Instantiated generic type only - Type genericType = nullableType.GetGenericTypeDefinition(); - if (ReferenceEquals(genericType, typeof(Nullable<>))) - { - return nullableType.GetGenericArguments()[0]; - } - } - return null; + // COMPAT: Returns null for generic type definition + if (nullableType.IsGenericTypeDefinition) + return null; + + return nullableType.GetNullableUnderlyingType(); } /// diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/EnumBuilder.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/EnumBuilder.cs index b8b250fb8507b3..d25baebf74282b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/EnumBuilder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/EnumBuilder.cs @@ -11,6 +11,9 @@ protected EnumBuilder() { } + // An EnumBuilder represents an enum being built; it cannot itself be a Nullable. + public override Type? GetNullableUnderlyingType() => null; + public FieldBuilder UnderlyingField => UnderlyingFieldCore; diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/GenericTypeParameterBuilder.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/GenericTypeParameterBuilder.cs index dfc38ba67ab088..a82f2a10dfdcfc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/GenericTypeParameterBuilder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/GenericTypeParameterBuilder.cs @@ -11,6 +11,9 @@ protected GenericTypeParameterBuilder() { } + // A generic type parameter is not a Nullable instantiation. + public override Type? GetNullableUnderlyingType() => null; + public void SetCustomAttribute(ConstructorInfo con, byte[] binaryAttribute) => SetCustomAttributeCore(con, binaryAttribute); protected abstract void SetCustomAttributeCore(ConstructorInfo con, ReadOnlySpan binaryAttribute); diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/SymbolType.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/SymbolType.cs index a17e82b464c725..b154dac0d48b1d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/SymbolType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/SymbolType.cs @@ -261,6 +261,8 @@ internal void SetFormat(string format, int curIndex, int length) public override bool IsSZArray => _rank <= 1 && _isSzArray; + public override Type? GetNullableUnderlyingType() => null; + public override Type MakePointerType() { return FormCompoundType(_format + "*", _baseType, 0)!; diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilder.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilder.cs index b1a0c16f804bd4..a782db50ffbcba 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilder.cs @@ -12,6 +12,8 @@ protected TypeBuilder() { } + public override Type? GetNullableUnderlyingType() => null; + public const int UnspecifiedTypeSize = 0; public PackingSize PackingSize diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs index 6bdbc966825e93..b7167fde85e323 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs @@ -249,6 +249,7 @@ public override bool ContainsGenericParameters } public override MethodBase? DeclaringMethod => null; public override Type GetGenericTypeDefinition() { return _genericType; } + public override Type? GetNullableUnderlyingType() => _genericType.GetNullableUnderlyingType() is not null ? _typeArguments[0] : null; [RequiresUnreferencedCode("If some of the generic arguments are annotated (either with DynamicallyAccessedMembersAttribute, or generic constraints), trimming can't validate that the requirements of those annotations are met.")] public override Type MakeGenericType(params Type[] inst) { throw new InvalidOperationException(SR.Format(SR.Arg_NotGenericTypeDefinition, this)); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.cs index 89d5401a50b1bc..f4ed19e487a555 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.cs @@ -78,6 +78,7 @@ public override Type[] GetOptionalCustomModifiers() public override bool ContainsGenericParameters => _unmodifiedType.ContainsGenericParameters; public override Type GetGenericTypeDefinition() => _unmodifiedType.GetGenericTypeDefinition(); public override bool IsGenericType => _unmodifiedType.IsGenericType; + public override Type? GetNullableUnderlyingType() => _unmodifiedType.GetNullableUnderlyingType() is not null ? GetGenericArguments()[0] : null; [DynamicallyAccessedMembers(InvokeMemberMembers)] public override object? InvokeMember(string name, BindingFlags invokeAttr, Binder? binder, object? target, diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs index 152a4b6cef8867..67be6b60a67cb8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs @@ -57,6 +57,7 @@ public sealed override bool ContainsGenericParameters internal sealed override SignatureType? ElementType => null; public sealed override int GetArrayRank() => throw new ArgumentException(SR.Argument_HasToBeArrayClass); public sealed override Type GetGenericTypeDefinition() => _genericTypeDefinition; + public sealed override Type? GetNullableUnderlyingType() => _genericTypeDefinition.GetNullableUnderlyingType() is not null ? _genericTypeArguments[0] : null; public sealed override Type[] GetGenericArguments() => GenericTypeArguments; public sealed override Type[] GenericTypeArguments => (Type[])(_genericTypeArguments.Clone()); public sealed override int GenericParameterPosition => throw new InvalidOperationException(SR.Arg_NotGenericParameter); diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.cs index 40d5a9ab977b02..c61466732cf47c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.cs @@ -42,6 +42,7 @@ internal SignatureModifiedType(Type baseType, Type[] requiredCustomModifiers, Ty public override Type[] GenericTypeArguments => _unmodifiedType.GenericTypeArguments; public override int GenericParameterPosition => _unmodifiedType.GenericParameterPosition; internal override SignatureType? ElementType => HasElementType ? new SignatureModifiedType(_unmodifiedType.GetElementType()!, [], []) : null; + public override Type? GetNullableUnderlyingType() => _unmodifiedType.GetNullableUnderlyingType(); public override string Name => _unmodifiedType.Name; public override string? Namespace => _unmodifiedType.Namespace; public override bool IsEnum => _unmodifiedType.IsEnum; diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs index a29ac0441f48d7..8995f99c2dbb86 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs @@ -18,6 +18,9 @@ internal abstract class SignatureType : Type { public sealed override bool IsSignatureType => true; + // The base implementation does not expose Nullable behavior; subclasses override when appropriate. + public override Type? GetNullableUnderlyingType() => null; + // Type flavor predicates public abstract override bool IsTypeDefinition { get; } protected abstract override bool HasElementTypeImpl(); diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.cs index d0ab8d07203f38..f657eaa93d3361 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.cs @@ -152,6 +152,7 @@ public TypeDelegator([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes. protected override bool IsValueTypeImpl() => typeImpl.IsValueType; protected override bool IsCOMObjectImpl() => typeImpl.IsCOMObject; public override bool IsByRefLike => typeImpl.IsByRefLike; + public override Type? GetNullableUnderlyingType() => typeImpl.GetNullableUnderlyingType(); public override bool IsConstructedGenericType => typeImpl.IsConstructedGenericType; public override bool IsCollectible => typeImpl.IsCollectible; diff --git a/src/libraries/System.Private.CoreLib/src/System/Type.cs b/src/libraries/System.Private.CoreLib/src/System/Type.cs index 5a80ac4a18553a..60bf2923b2b6f0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Type.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Type.cs @@ -602,6 +602,18 @@ protected virtual TypeCode GetTypeCodeImpl() public virtual bool IsInstanceOfType([NotNullWhen(true)] object? o) => o != null && IsAssignableFrom(o.GetType()); public virtual bool IsEquivalentTo([NotNullWhen(true)] Type? other) => this == other; + /// + /// Returns the underlying type argument of a type. + /// + /// + /// The type argument of the type if the current type represents + /// the generic type definition or a constructed ; + /// otherwise, . When the current type is the generic type definition + /// (for example, typeof(Nullable<>)), the returned type is the generic type + /// parameter T. + /// + public virtual Type? GetNullableUnderlyingType() => throw new NotSupportedException(SR.NotSupported_SubclassOverride); + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2085:UnrecognizedReflectionPattern", Justification = "The single instance field on enum types is never trimmed")] [Intrinsic] diff --git a/src/libraries/System.Reflection.Emit/ref/System.Reflection.Emit.cs b/src/libraries/System.Reflection.Emit/ref/System.Reflection.Emit.cs index bf5807a9cef73d..afef1b519c5ba1 100644 --- a/src/libraries/System.Reflection.Emit/ref/System.Reflection.Emit.cs +++ b/src/libraries/System.Reflection.Emit/ref/System.Reflection.Emit.cs @@ -162,6 +162,7 @@ protected EnumBuilder() { } public override System.Type? GetNestedType(string name, System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicNestedTypes | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicNestedTypes)] public override System.Type[] GetNestedTypes(System.Reflection.BindingFlags bindingAttr) { throw null; } + public override System.Type? GetNullableUnderlyingType() { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] public override System.Reflection.PropertyInfo[] GetProperties(System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] @@ -293,6 +294,7 @@ protected GenericTypeParameterBuilder() { } public override System.Type GetNestedType(string name, System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicNestedTypes | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicNestedTypes)] public override System.Type[] GetNestedTypes(System.Reflection.BindingFlags bindingAttr) { throw null; } + public override System.Type? GetNullableUnderlyingType() { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] public override System.Reflection.PropertyInfo[] GetProperties(System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] @@ -657,6 +659,7 @@ public void DefineMethodOverride(System.Reflection.MethodInfo methodInfoBody, Sy public override System.Type? GetNestedType(string name, System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicNestedTypes | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicNestedTypes)] public override System.Type[] GetNestedTypes(System.Reflection.BindingFlags bindingAttr) { throw null; } + public override System.Type? GetNullableUnderlyingType() { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] public override System.Reflection.PropertyInfo[] GetProperties(System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] diff --git a/src/libraries/System.Reflection.Emit/tests/System.Reflection.Emit.Tests.csproj b/src/libraries/System.Reflection.Emit/tests/System.Reflection.Emit.Tests.csproj index 64f0b179faaebb..f944d2e9e00cc2 100644 --- a/src/libraries/System.Reflection.Emit/tests/System.Reflection.Emit.Tests.csproj +++ b/src/libraries/System.Reflection.Emit/tests/System.Reflection.Emit.Tests.csproj @@ -117,6 +117,7 @@ + diff --git a/src/libraries/System.Reflection.Emit/tests/TypeBuilder/TypeBuilderGetNullableUnderlyingType.cs b/src/libraries/System.Reflection.Emit/tests/TypeBuilder/TypeBuilderGetNullableUnderlyingType.cs new file mode 100644 index 00000000000000..bed425a32e1ce9 --- /dev/null +++ b/src/libraries/System.Reflection.Emit/tests/TypeBuilder/TypeBuilderGetNullableUnderlyingType.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Reflection.Emit.Tests +{ + public class TypeBuilderGetNullableUnderlyingType + { + [Fact] + public void TypeBuilder_ReturnsNull() + { + TypeBuilder tb = Helpers.DynamicType(TypeAttributes.Public); + Assert.Null(tb.GetNullableUnderlyingType()); + } + + [Fact] + public void EnumBuilder_ReturnsNull() + { + EnumBuilder eb = Helpers.DynamicEnum(TypeAttributes.Public, typeof(int)); + Assert.Null(eb.GetNullableUnderlyingType()); + } + + [Fact] + public void GenericTypeParameterBuilder_ReturnsNull() + { + TypeBuilder tb = Helpers.DynamicType(TypeAttributes.Public); + GenericTypeParameterBuilder[] gps = tb.DefineGenericParameters("T"); + Assert.Null(gps[0].GetNullableUnderlyingType()); + } + + [Fact] + public void TypeBuilderInstantiation_ReturnsNull() + { + // A TypeBuilderInstantiation is produced when MakeGenericType is called on a + // generic TypeBuilder. The open generic definition is a TypeBuilder (never + // typeof(Nullable<>)), so the override always returns null in practice. + TypeBuilder tb = Helpers.DynamicType(TypeAttributes.Public); + tb.DefineGenericParameters("T"); + Type instantiation = tb.MakeGenericType(typeof(int)); + Assert.Null(instantiation.GetNullableUnderlyingType()); + } + + [Fact] + public void SymbolType_Array_ReturnsNull() + { + TypeBuilder tb = Helpers.DynamicType(TypeAttributes.Public); + Type arrayType = tb.MakeArrayType(); + Assert.Null(arrayType.GetNullableUnderlyingType()); + Assert.Null(Nullable.GetUnderlyingType(arrayType)); + } + + [Fact] + public void SymbolType_MultiDimArray_ReturnsNull() + { + TypeBuilder tb = Helpers.DynamicType(TypeAttributes.Public); + Type arrayType = tb.MakeArrayType(2); + Assert.Null(arrayType.GetNullableUnderlyingType()); + Assert.Null(Nullable.GetUnderlyingType(arrayType)); + } + + [Fact] + public void SymbolType_Pointer_ReturnsNull() + { + TypeBuilder tb = Helpers.DynamicType(TypeAttributes.Public); + Type pointerType = tb.MakePointerType(); + Assert.Null(pointerType.GetNullableUnderlyingType()); + Assert.Null(Nullable.GetUnderlyingType(pointerType)); + } + + [Fact] + public void SymbolType_ByRef_ReturnsNull() + { + TypeBuilder tb = Helpers.DynamicType(TypeAttributes.Public); + Type byRefType = tb.MakeByRefType(); + Assert.Null(byRefType.GetNullableUnderlyingType()); + Assert.Null(Nullable.GetUnderlyingType(byRefType)); + } + } +} diff --git a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoModifiedType.cs b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoModifiedType.cs index 1ec881fd27a9a3..0cf1eaf4f7f2ef 100644 --- a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoModifiedType.cs +++ b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoModifiedType.cs @@ -168,6 +168,10 @@ public override IEnumerable DeclaredNestedTypes public override Type GetGenericTypeDefinition() => throw new NotSupportedException(SR.NotSupported_ModifiedType); +#if NET11_0_OR_GREATER + public override Type? GetNullableUnderlyingType() => _unmodifiedType.GetNullableUnderlyingType() is not null ? GetGenericArguments()[0] : null; +#endif + // Generic parameters are supported. internal override RoType[] GetGenericTypeParametersNoCopy() => _unmodifiedType.GetGenericTypeParametersNoCopy(); internal override RoType[] GetGenericTypeArgumentsNoCopy() => _unmodifiedType.GetGenericTypeArgumentsNoCopy(); diff --git a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs index a5b0738d246f10..3ee62bfd62d42c 100644 --- a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs +++ b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs @@ -327,6 +327,26 @@ public sealed override Type MakeArrayType(int rank) private RoType? _lazyUnderlyingEnumType; public sealed override Array GetEnumValues() => throw new InvalidOperationException(SR.Arg_InvalidOperation_Reflection); + // Nullable methods +#if NET11_0_OR_GREATER + public override Type? GetNullableUnderlyingType() + { + if (IsGenericType) + { + RoType? nullableOfT = Loader.TryGetCoreType(CoreType.NullableT); + if (nullableOfT is not null && GetGenericTypeDefinition() == nullableOfT) + { + // Use GetGenericArguments() to cover both constructed Nullable + // (returns T) and the generic type definition Nullable<> + // (returns the generic type parameter). + return GetGenericArguments()[0]; + } + } + + return null; + } +#endif + #if NET [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2085:UnrecognizedReflectionPattern", Justification = "Enum Types are not trimmed.")] diff --git a/src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csproj b/src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csproj index 64669968feebf2..33e1e4ce93627d 100644 --- a/src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csproj +++ b/src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csproj @@ -62,6 +62,7 @@ + diff --git a/src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.cs b/src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.cs new file mode 100644 index 00000000000000..72b0bdf87f562a --- /dev/null +++ b/src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET + +using Xunit; + +namespace System.Reflection.Tests +{ + public static partial class TypeTests + { + [Fact] + public static void GetNullableUnderlyingType_MetadataLoadContext_NullableInt_ReturnsUnderlyingType() + { + string coreAssemblyPath = TestUtils.GetPathToCoreAssembly(); + var resolver = new PathAssemblyResolver([coreAssemblyPath]); + using var mlc = new MetadataLoadContext(resolver, TestUtils.GetNameOfCoreAssembly()); + + Assembly coreAssembly = mlc.LoadFromAssemblyPath(coreAssemblyPath); + Type intType = coreAssembly.GetType("System.Int32", throwOnError: true)!; + Type nullableIntType = coreAssembly.GetType("System.Nullable`1", throwOnError: true)!.MakeGenericType(intType); + + Type? underlying = Nullable.GetUnderlyingType(nullableIntType); + Assert.NotNull(underlying); + Assert.Equal("System.Int32", underlying.FullName); + Assert.Same(intType, underlying); + Assert.NotSame(typeof(int), underlying); + + Type? underlyingDirect = nullableIntType.GetNullableUnderlyingType(); + Assert.NotNull(underlyingDirect); + Assert.Equal("System.Int32", underlyingDirect.FullName); + Assert.Same(intType, underlyingDirect); + Assert.NotSame(typeof(int), underlyingDirect); + } + + [Fact] + public static void GetNullableUnderlyingType_MetadataLoadContext_NonNullableTypes_ReturnsNull() + { + string coreAssemblyPath = TestUtils.GetPathToCoreAssembly(); + var resolver = new PathAssemblyResolver([coreAssemblyPath]); + using var mlc = new MetadataLoadContext(resolver, TestUtils.GetNameOfCoreAssembly()); + + Assembly coreAssembly = mlc.LoadFromAssemblyPath(coreAssemblyPath); + Type intType = coreAssembly.GetType("System.Int32", throwOnError: true)!; + Type stringType = coreAssembly.GetType("System.String", throwOnError: true)!; + + Assert.Null(Nullable.GetUnderlyingType(intType)); + Assert.Null(Nullable.GetUnderlyingType(stringType)); + + Assert.Null(intType.GetNullableUnderlyingType()); + Assert.Null(stringType.GetNullableUnderlyingType()); + } + + [Fact] + public static void GetNullableUnderlyingType_MetadataLoadContext_OpenNullable() + { + string coreAssemblyPath = TestUtils.GetPathToCoreAssembly(); + var resolver = new PathAssemblyResolver([coreAssemblyPath]); + using var mlc = new MetadataLoadContext(resolver, TestUtils.GetNameOfCoreAssembly()); + + Assembly coreAssembly = mlc.LoadFromAssemblyPath(coreAssemblyPath); + Type openNullableType = coreAssembly.GetType("System.Nullable`1", throwOnError: true)!; + + // Nullable.GetUnderlyingType returns null for generic type definitions (COMPAT). + Assert.Null(Nullable.GetUnderlyingType(openNullableType)); + + // Type.GetNullableUnderlyingType returns the generic type parameter T for Nullable<>. + Type? underlying = openNullableType.GetNullableUnderlyingType(); + Assert.NotNull(underlying); + Assert.Same(openNullableType.GetGenericArguments()[0], underlying); + } + } +} + +#endif // NET diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 1e205d855b5d0a..8c517b088c4d76 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -6623,6 +6623,7 @@ protected Type() { } public virtual string? GetEnumName(object value) { throw null; } public virtual string[] GetEnumNames() { throw null; } public virtual System.Type GetEnumUnderlyingType() { throw null; } + public virtual System.Type? GetNullableUnderlyingType() { throw null; } [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("It might not be possible to create an array of the enum type at runtime. Use Enum.GetValues or the GetEnumValuesAsUnderlyingType method instead.")] public virtual System.Array GetEnumValues() { throw null; } public virtual System.Array GetEnumValuesAsUnderlyingType() { throw null; } @@ -13277,6 +13278,7 @@ public TypeDelegator([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers public override System.Type? GetNestedType(string name, System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicNestedTypes | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicNestedTypes)] public override System.Type[] GetNestedTypes(System.Reflection.BindingFlags bindingAttr) { throw null; } + public override System.Type? GetNullableUnderlyingType() { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] public override System.Reflection.PropertyInfo[] GetProperties(System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs index 4f3efc786e4623..94cebad747f2f4 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs @@ -88,6 +88,33 @@ public static void GetUnderlyingType(Type nullableType, Type? expected) Assert.Equal(expected, Nullable.GetUnderlyingType(nullableType)); } + public static IEnumerable GetNullableUnderlyingType_RuntimeType_TestData() + { + yield return new object[] { typeof(int?), typeof(int) }; + yield return new object[] { typeof(int), null }; + yield return new object[] { typeof(G), null }; + // Nullable<> (generic type definition) is nullable; returns the generic type parameter T. + yield return new object[] { typeof(Nullable<>), typeof(Nullable<>).GetGenericArguments()[0] }; + } + + [Theory] + [MemberData(nameof(GetNullableUnderlyingType_RuntimeType_TestData))] + public static void GetNullableUnderlyingType_RuntimeType(Type type, Type? expected) + { + Assert.Equal(expected, type.GetNullableUnderlyingType()); + } + + [Fact] + public static void GetNullableUnderlyingType_NullableOverForeignGenericParameter() + { + // Nullable instantiated over the generic parameter of another type. + Type genericParam = typeof(GStruct<>).GetGenericArguments()[0]; + Type nullableOverParam = typeof(Nullable<>).MakeGenericType(genericParam); + + Assert.Same(genericParam, nullableOverParam.GetNullableUnderlyingType()); + Assert.Same(genericParam, Nullable.GetUnderlyingType(nullableOverParam)); + } + [Fact] public static void GetUnderlyingType_NullType_ThrowsArgumentNullException() { @@ -222,5 +249,7 @@ private struct MutatingStruct } public class G { } + + public struct GStruct where T : struct { } } } diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Reflection/SignatureTypes.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Reflection/SignatureTypes.cs index 870982a7949d05..ba8f8d5584eef7 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Reflection/SignatureTypes.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Reflection/SignatureTypes.cs @@ -934,5 +934,37 @@ private static void TestSignatureTypeInvariants(Type type) Assert.Throws(() => type.GenericParameterPosition); } } + + [Fact] + public static void GetNullableUnderlyingType_SignatureConstructedGenericType_Nullable_ReturnsTypeArgument() + { + Type sig = Type.MakeGenericSignatureType(typeof(Nullable<>), typeof(int)); + Assert.True(sig.IsSignatureType); + Assert.Equal(typeof(int), sig.GetNullableUnderlyingType()); + } + + [Fact] + public static void GetNullableUnderlyingType_SignatureConstructedGenericType_NonNullable_ReturnsNull() + { + Type sig = Type.MakeGenericSignatureType(typeof(List<>), typeof(int)); + Assert.True(sig.IsSignatureType); + Assert.Null(sig.GetNullableUnderlyingType()); + } + + [Fact] + public static void GetNullableUnderlyingType_SignatureModifiedType_Nullable_DelegatesToUnmodifiedType() + { + Type sig = Type.MakeModifiedSignatureType(typeof(int?), null, null); + Assert.True(sig.IsSignatureType); + Assert.Equal(typeof(int), sig.GetNullableUnderlyingType()); + } + + [Fact] + public static void GetNullableUnderlyingType_SignatureModifiedType_NonNullable_ReturnsNull() + { + Type sig = Type.MakeModifiedSignatureType(typeof(int), null, null); + Assert.True(sig.IsSignatureType); + Assert.Null(sig.GetNullableUnderlyingType()); + } } } diff --git a/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs b/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs index f442f500bcd5b4..5ab601dceb9809 100644 --- a/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs @@ -1396,6 +1396,18 @@ public override GenericParameterAttributes GenericParameterAttributes #region Generics + public override Type? GetNullableUnderlyingType() + { + if (IsGenericType) + { + Type genericType = GetGenericTypeDefinition(); + if (ReferenceEquals(genericType, typeof(Nullable<>))) + return GetGenericArguments()[0]; + } + + return null; + } + internal RuntimeType[] GetGenericArgumentsInternal() { RuntimeType[]? res = null; @@ -2466,7 +2478,7 @@ public override string? FullName public sealed override bool HasSameMetadataDefinitionAs(MemberInfo other) => HasSameMetadataDefinitionAsCore(other); - internal bool IsNullableOfT => Nullable.GetUnderlyingType(this) != null; + internal bool IsNullableOfT => GetNullableUnderlyingType() is not null; public override bool IsSZArray { From a534a1c68e656b07d16015e2f46fa55b25bfd0b9 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Wed, 29 Apr 2026 08:09:38 +0200 Subject: [PATCH 004/115] [browser][coreCLR] Support interpreter code resolution on portable entrypoint platforms (#127370) --- src/coreclr/debug/daccess/daccess.cpp | 10 ++++---- src/coreclr/debug/daccess/dacdbiimpl.cpp | 2 +- src/coreclr/debug/daccess/request.cpp | 6 ++--- src/coreclr/debug/ee/functioninfo.cpp | 2 +- src/coreclr/vm/eventtrace.cpp | 2 +- src/coreclr/vm/method.hpp | 2 +- src/coreclr/vm/precode.cpp | 32 +++++++++++++++++------- src/coreclr/vm/precode.h | 2 +- src/coreclr/vm/prestub.cpp | 10 ++++++-- 9 files changed, 44 insertions(+), 24 deletions(-) diff --git a/src/coreclr/debug/daccess/daccess.cpp b/src/coreclr/debug/daccess/daccess.cpp index 222c8cf9eba00f..b41f07619a3646 100644 --- a/src/coreclr/debug/daccess/daccess.cpp +++ b/src/coreclr/debug/daccess/daccess.cpp @@ -5425,7 +5425,7 @@ ClrDataAccess::RawGetMethodName( MethodDesc* methodDesc = NULL; { - EECodeInfo codeInfo(GetInterpreterCodeFromInterpreterPrecodeIfPresent(TO_TADDR(address))); + EECodeInfo codeInfo(GetInterpreterCodeFromEntryPointIfPresent(TO_TADDR(address))); if (codeInfo.IsValid()) { if (displacement) @@ -5605,11 +5605,11 @@ ClrDataAccess::GetMethodVarInfo(MethodDesc* methodDesc, { return E_INVALIDARG; } - nativeCodeStartAddr = PCODEToPINSTR(GetInterpreterCodeFromInterpreterPrecodeIfPresent(requestedNativeCodeVersion.GetNativeCode())); + nativeCodeStartAddr = PCODEToPINSTR(GetInterpreterCodeFromEntryPointIfPresent(requestedNativeCodeVersion.GetNativeCode())); } else { - nativeCodeStartAddr = PCODEToPINSTR(GetInterpreterCodeFromInterpreterPrecodeIfPresent(methodDesc->GetNativeCode())); + nativeCodeStartAddr = PCODEToPINSTR(GetInterpreterCodeFromEntryPointIfPresent(methodDesc->GetNativeCode())); } DebugInfoRequest request; @@ -5664,11 +5664,11 @@ ClrDataAccess::GetMethodNativeMap(MethodDesc* methodDesc, { return E_INVALIDARG; } - nativeCodeStartAddr = PCODEToPINSTR(GetInterpreterCodeFromInterpreterPrecodeIfPresent(requestedNativeCodeVersion.GetNativeCode())); + nativeCodeStartAddr = PCODEToPINSTR(GetInterpreterCodeFromEntryPointIfPresent(requestedNativeCodeVersion.GetNativeCode())); } else { - nativeCodeStartAddr = PCODEToPINSTR(GetInterpreterCodeFromInterpreterPrecodeIfPresent(methodDesc->GetNativeCode())); + nativeCodeStartAddr = PCODEToPINSTR(GetInterpreterCodeFromEntryPointIfPresent(methodDesc->GetNativeCode())); } DebugInfoRequest request; diff --git a/src/coreclr/debug/daccess/dacdbiimpl.cpp b/src/coreclr/debug/daccess/dacdbiimpl.cpp index fcd5363107575e..bd31eee7c233bf 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.cpp +++ b/src/coreclr/debug/daccess/dacdbiimpl.cpp @@ -1316,7 +1316,7 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetNativeCodeInfoForAddr(CORDB_AD EX_TRY_ALLOW_DATATARGET_MISSING_MEMORY { - codeAddr = GetInterpreterCodeFromInterpreterPrecodeIfPresent(codeAddr); + codeAddr = GetInterpreterCodeFromEntryPointIfPresent(codeAddr); } EX_END_CATCH_ALLOW_DATATARGET_MISSING_MEMORY; diff --git a/src/coreclr/debug/daccess/request.cpp b/src/coreclr/debug/daccess/request.cpp index 60043181c1de6a..10b78d156aa84a 100644 --- a/src/coreclr/debug/daccess/request.cpp +++ b/src/coreclr/debug/daccess/request.cpp @@ -911,7 +911,7 @@ HRESULT ClrDataAccess::GetThreadData(CLRDATA_ADDRESS threadAddr, struct DacpThre void CopyNativeCodeVersionToReJitData(NativeCodeVersion nativeCodeVersion, NativeCodeVersion activeCodeVersion, DacpReJitData * pReJitData) { pReJitData->rejitID = nativeCodeVersion.GetILCodeVersion().GetVersionId(); - pReJitData->NativeCodeAddr = GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCodeVersion.GetNativeCode()); + pReJitData->NativeCodeAddr = GetInterpreterCodeFromEntryPointIfPresent(nativeCodeVersion.GetNativeCode()); if (nativeCodeVersion != activeCodeVersion) { @@ -1021,7 +1021,7 @@ HRESULT ClrDataAccess::GetMethodDescData( if (!requestedNativeCodeVersion.IsNull() && requestedNativeCodeVersion.GetNativeCode() != (PCODE)NULL) { methodDescData->bHasNativeCode = TRUE; - methodDescData->NativeCodeAddr = TO_CDADDR(PCODEToPINSTR(GetInterpreterCodeFromInterpreterPrecodeIfPresent(requestedNativeCodeVersion.GetNativeCode()))); + methodDescData->NativeCodeAddr = TO_CDADDR(PCODEToPINSTR(GetInterpreterCodeFromEntryPointIfPresent(requestedNativeCodeVersion.GetNativeCode()))); } else { @@ -1245,7 +1245,7 @@ HRESULT ClrDataAccess::GetTieredVersions( int count = 0; for (NativeCodeVersionIterator iter = nativeCodeVersions.Begin(); iter != nativeCodeVersions.End(); iter++) { - TADDR pNativeCode = PCODEToPINSTR(GetInterpreterCodeFromInterpreterPrecodeIfPresent((*iter).GetNativeCode())); + TADDR pNativeCode = PCODEToPINSTR(GetInterpreterCodeFromEntryPointIfPresent((*iter).GetNativeCode())); nativeCodeAddrs[count].NativeCodeAddr = pNativeCode; PTR_NativeCodeVersionNode pNode = (*iter).AsNode(); nativeCodeAddrs[count].NativeCodeVersionNodePtr = PTR_CDADDR(pNode); diff --git a/src/coreclr/debug/ee/functioninfo.cpp b/src/coreclr/debug/ee/functioninfo.cpp index 6ad138388ee0a5..3ee9d46810e911 100644 --- a/src/coreclr/debug/ee/functioninfo.cpp +++ b/src/coreclr/debug/ee/functioninfo.cpp @@ -2060,7 +2060,7 @@ void DebuggerMethodInfo::CreateDJIsForMethodDesc(MethodDesc * pMethodDesc) { // Some versions may not be compiled yet - skip those for now // if they compile later the JitCompiled callback will add a DJI to our cache at that time - PCODE codeAddr = GetInterpreterCodeFromInterpreterPrecodeIfPresent(itr->GetNativeCode()); + PCODE codeAddr = GetInterpreterCodeFromEntryPointIfPresent(itr->GetNativeCode()); LOG((LF_CORDB, LL_INFO10000, "DMI::CDJIFMD (%d) Native code for DJI - %p\n", ++count, codeAddr)); if (codeAddr) { diff --git a/src/coreclr/vm/eventtrace.cpp b/src/coreclr/vm/eventtrace.cpp index 2488a54040f945..cd4cff43f3c88d 100644 --- a/src/coreclr/vm/eventtrace.cpp +++ b/src/coreclr/vm/eventtrace.cpp @@ -4417,7 +4417,7 @@ TADDR MethodAndStartAddressToEECodeInfoPointer(MethodDesc *pMethodDesc, PCODE pN return 0; } - return GetInterpreterCodeFromInterpreterPrecodeIfPresent(start); + return GetInterpreterCodeFromEntryPointIfPresent(start); } /****************************************************************************/ diff --git a/src/coreclr/vm/method.hpp b/src/coreclr/vm/method.hpp index 29b76fd0fede96..43368727b1fa47 100644 --- a/src/coreclr/vm/method.hpp +++ b/src/coreclr/vm/method.hpp @@ -1631,7 +1631,7 @@ class MethodDesc PCODE GetCodeForInterpreterOrJitted() { WRAPPER_NO_CONTRACT; - return GetInterpreterCodeFromInterpreterPrecodeIfPresent(GetNativeCode()); + return GetInterpreterCodeFromEntryPointIfPresent(GetNativeCode()); } // Returns GetNativeCode() if it exists, but also checks to see if there diff --git a/src/coreclr/vm/precode.cpp b/src/coreclr/vm/precode.cpp index 5b1422aa1c8273..6b72dee83957bc 100644 --- a/src/coreclr/vm/precode.cpp +++ b/src/coreclr/vm/precode.cpp @@ -959,32 +959,46 @@ BOOL StubPrecode::IsStubPrecodeByASM(PCODE addr) #endif // !FEATURE_PORTABLE_ENTRYPOINTS -TADDR GetInterpreterCodeFromInterpreterPrecodeIfPresent(TADDR codePointerMaybeInterpreterStub) +TADDR GetInterpreterCodeFromEntryPointIfPresent(TADDR entryPoint) { CONTRACTL { NOTHROW; GC_NOTRIGGER; SUPPORTS_DAC; } CONTRACTL_END; - -#if defined(FEATURE_INTERPRETER) && !defined(FEATURE_PORTABLE_ENTRYPOINTS) - if (codePointerMaybeInterpreterStub == (TADDR)NULL) + +#ifdef FEATURE_INTERPRETER + if (entryPoint == (TADDR)NULL) { return (TADDR)NULL; } - RangeSection * pRS = ExecutionManager::FindCodeRange(codePointerMaybeInterpreterStub, ExecutionManager::GetScanFlags()); + RangeSection * pRS = ExecutionManager::FindCodeRange(entryPoint, ExecutionManager::GetScanFlags()); + +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + if (pRS == NULL) + { + // Address not in any code range - this is a portable entry point. + MethodDesc* pMD = PortableEntryPoint::GetMethodDesc((PCODE)entryPoint); + PTR_InterpByteCodeStart pInterpCode = pMD->GetInterpreterCode(); + if (pInterpCode != NULL) + { + entryPoint = dac_cast(pInterpCode); + } + } +#else // !FEATURE_PORTABLE_ENTRYPOINTS if (pRS != NULL && pRS->_flags & RangeSection::RANGE_SECTION_RANGELIST) { if (pRS->_pRangeList->GetCodeBlockKind() == STUB_CODE_BLOCK_STUBPRECODE) { - if (dac_cast(PCODEToPINSTR(codePointerMaybeInterpreterStub))->GetType() == PRECODE_INTERPRETER) + if (dac_cast(PCODEToPINSTR(entryPoint))->GetType() == PRECODE_INTERPRETER) { - codePointerMaybeInterpreterStub = (dac_cast(PCODEToPINSTR(codePointerMaybeInterpreterStub)))->GetData()->ByteCodeAddr; + entryPoint = (dac_cast(PCODEToPINSTR(entryPoint)))->GetData()->ByteCodeAddr; } } } -#endif +#endif // FEATURE_PORTABLE_ENTRYPOINTS +#endif // FEATURE_INTERPRETER - return codePointerMaybeInterpreterStub; + return entryPoint; } diff --git a/src/coreclr/vm/precode.h b/src/coreclr/vm/precode.h index 3f8958ecea9b83..2b6aef0db9251a 100644 --- a/src/coreclr/vm/precode.h +++ b/src/coreclr/vm/precode.h @@ -864,6 +864,6 @@ extern InterleavedLoaderHeapConfig s_fixupStubPrecodeHeapConfig; #endif // FEATURE_PORTABLE_ENTRYPOINTS -TADDR GetInterpreterCodeFromInterpreterPrecodeIfPresent(TADDR codePointerMaybeInterpreterStub); +TADDR GetInterpreterCodeFromEntryPointIfPresent(TADDR entryPoint); #endif // __PRECODE_H__ diff --git a/src/coreclr/vm/prestub.cpp b/src/coreclr/vm/prestub.cpp index 532c285d4fab46..c5b01c62128233 100644 --- a/src/coreclr/vm/prestub.cpp +++ b/src/coreclr/vm/prestub.cpp @@ -809,9 +809,15 @@ PCODE MethodDesc::JitCompileCodeLockedEventWrapper(PrepareCodeConfig* pConfig, J #ifdef FEATURE_INTERPRETER if (isInterpreterCode) { - // If this is interpreter code, we need to get the native code start address from the interpreter Precode + // If this is interpreter code, get the native code start address from the + // interpreter entrypoint data: PortableEntryPoint interpreter data when + // FEATURE_PORTABLE_ENTRYPOINTS is enabled, otherwise the interpreter Precode. +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + InterpByteCodeStart* interpreterCode = (InterpByteCodeStart*)PortableEntryPoint::GetInterpreterData(pCode); +#else // !FEATURE_PORTABLE_ENTRYPOINTS InterpreterPrecode* pPrecode = InterpreterPrecode::FromEntryPoint(pCode); InterpByteCodeStart* interpreterCode = dac_cast(pPrecode->GetData()->ByteCodeAddr); +#endif // FEATURE_PORTABLE_ENTRYPOINTS pNativeCodeStartAddress = PINSTRToPCODE(dac_cast(interpreterCode)); } #endif // FEATURE_INTERPRETER @@ -2528,7 +2534,7 @@ PCODE MethodDesc::DoPrestub(MethodTable *pDispatchingMT, CallerGCMode callerGCMo // Check to see if the entrypoint is into the interpreter. If so, grab the interpreter codes from the stub and put that directly // into the MethodDesc TADDR functionAddress = GetOrCreatePrecode()->GetTarget(); - TADDR byteCodeStartOrFunctionAddress = GetInterpreterCodeFromInterpreterPrecodeIfPresent(functionAddress); + TADDR byteCodeStartOrFunctionAddress = GetInterpreterCodeFromEntryPointIfPresent(functionAddress); if (byteCodeStartOrFunctionAddress != functionAddress) { // Then we must have an InterpByteCodeStart From 4e8ab2dbfb1e533f8a7bc475ade1af82f6618374 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Wed, 29 Apr 2026 09:15:11 +0200 Subject: [PATCH 005/115] Add configurable EventPipe CPU sampling rate via `DOTNET_EventPipeThreadSamplingRate` (#127292) --- src/coreclr/inc/clrconfigvalues.h | 1 + .../nativeaot/Runtime/eventpipe/ep-rt-aot.h | 17 +++++++++++++++++ .../vm/eventing/eventpipe/ep-rt-coreclr.h | 18 +++++++++++++----- src/mono/browser/build/WasmApp.InTree.props | 6 +++++- .../eventpipe/ep-rt-mono-runtime-provider.c | 2 -- src/mono/mono/eventpipe/ep-rt-mono.h | 19 +++++++++++++++++++ ...rosoft.NET.Sdk.WebAssembly.Browser.targets | 15 ++++++++++++++- src/native/eventpipe/ds-ipc-pal-websocket.h | 8 +++++++- src/native/eventpipe/ep-rt.h | 5 +++++ src/native/eventpipe/ep.c | 8 +++++++- 10 files changed, 88 insertions(+), 11 deletions(-) diff --git a/src/coreclr/inc/clrconfigvalues.h b/src/coreclr/inc/clrconfigvalues.h index 7e280e3e059be6..7767eb65a9b9c4 100644 --- a/src/coreclr/inc/clrconfigvalues.h +++ b/src/coreclr/inc/clrconfigvalues.h @@ -610,6 +610,7 @@ RETAIL_CONFIG_DWORD_INFO(INTERNAL_EventPipeCircularMB, W("EventPipeCircularMB"), RETAIL_CONFIG_DWORD_INFO(INTERNAL_EventPipeProcNumbers, W("EventPipeProcNumbers"), 0, "Enable/disable capturing processor numbers in EventPipe event headers") RETAIL_CONFIG_DWORD_INFO(INTERNAL_EventPipeOutputStreaming, W("EventPipeOutputStreaming"), 1, "Enable/disable streaming for trace file set in DOTNET_EventPipeOutputPath. Non-zero values enable streaming.") RETAIL_CONFIG_DWORD_INFO(INTERNAL_EventPipeEnableStackwalk, W("EventPipeEnableStackwalk"), 1, "Set to 0 to disable collecting stacks for EventPipe events.") +RETAIL_CONFIG_DWORD_INFO(INTERNAL_EventPipeThreadSamplingRate, W("EventPipeThreadSamplingRate"), 0, "Desired sample interval in milliseconds for EventPipe thread time sampling profiler. 0 means use the default.") #ifdef FEATURE_AUTO_TRACE RETAIL_CONFIG_DWORD_INFO_EX(INTERNAL_AutoTrace_N_Tracers, W("AutoTrace_N_Tracers"), 0, "", CLRConfig::LookupOptions::ParseIntegerAsBase10) diff --git a/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h b/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h index 78d6578dce29c8..c9ae8dcbd80c8e 100644 --- a/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h +++ b/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h @@ -507,6 +507,23 @@ ep_rt_config_value_get_enable_stackwalk (void) return false; } +static +inline +uint32_t +ep_rt_config_value_get_sampling_rate (void) +{ + STATIC_CONTRACT_NOTHROW; + + uint64_t value; + if (RhConfig::Environment::TryGetIntegerValue("EventPipeThreadSamplingRate", &value, true)) + { + EP_ASSERT(value <= UINT32_MAX); + return static_cast(value); + } + + return 0; +} + /* * EventPipeSampleProfiler. */ diff --git a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h index 97991823eaff97..ebb46b8eb5396c 100644 --- a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h +++ b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h @@ -444,9 +444,9 @@ ep_rt_provider_config_init (EventPipeProviderConfiguration *provider_config) // This function is auto-generated from /src/scripts/genEventPipe.py #ifdef TARGET_UNIX extern "C" void InitProvidersAndEvents (); -#else +#else // TARGET_UNIX extern void InitProvidersAndEvents (); -#endif +#endif // TARGET_UNIX static void @@ -572,6 +572,15 @@ ep_rt_config_value_get_enable_stackwalk (void) return CLRConfig::GetConfigValue(CLRConfig::INTERNAL_EventPipeEnableStackwalk) != 0; } +static +inline +uint32_t +ep_rt_config_value_get_sampling_rate (void) +{ + STATIC_CONTRACT_NOTHROW; + return CLRConfig::GetConfigValue(CLRConfig::INTERNAL_EventPipeThreadSamplingRate); +} + /* * EventPipeSampleProfiler. */ @@ -622,13 +631,12 @@ void ep_rt_notify_profiler_provider_created (EventPipeProvider *provider) { STATIC_CONTRACT_NOTHROW; - -#ifndef DACCESS_COMPILE +#if !defined(DACCESS_COMPILE) && defined(PROFILING_SUPPORTED) // Let the profiler know the provider has been created so it can register if it wants to BEGIN_PROFILER_CALLBACK (CORProfilerTrackEventPipe ()); (&g_profControlBlock)->EventPipeProviderCreated (provider); END_PROFILER_CALLBACK (); -#endif // DACCESS_COMPILE +#endif // !DACCESS_COMPILE && PROFILING_SUPPORTED } /* diff --git a/src/mono/browser/build/WasmApp.InTree.props b/src/mono/browser/build/WasmApp.InTree.props index d097d45119197d..9031c6f6d0cc25 100644 --- a/src/mono/browser/build/WasmApp.InTree.props +++ b/src/mono/browser/build/WasmApp.InTree.props @@ -14,10 +14,14 @@ - + false + false + + true + library diff --git a/src/mono/mono/eventpipe/ep-rt-mono-runtime-provider.c b/src/mono/mono/eventpipe/ep-rt-mono-runtime-provider.c index 2fff49357e844c..5503c2c0b9eebf 100644 --- a/src/mono/mono/eventpipe/ep-rt-mono-runtime-provider.c +++ b/src/mono/mono/eventpipe/ep-rt-mono-runtime-provider.c @@ -1507,8 +1507,6 @@ void ep_rt_mono_sample_profiler_enabled (EventPipeEvent *sampling_event) { desired_sample_interval_ms = ((double)ep_sample_profiler_get_sampling_rate ()) / 1000000.0; - EP_ASSERT (desired_sample_interval_ms >= 0.0); - EP_ASSERT (desired_sample_interval_ms < 1000.0); current_sampling_event = sampling_event; current_sampling_thread = ep_rt_thread_get_handle (); diff --git a/src/mono/mono/eventpipe/ep-rt-mono.h b/src/mono/mono/eventpipe/ep-rt-mono.h index ce5c7f7e95af96..a5f3c2626509ed 100644 --- a/src/mono/mono/eventpipe/ep-rt-mono.h +++ b/src/mono/mono/eventpipe/ep-rt-mono.h @@ -637,6 +637,25 @@ ep_rt_config_value_get_enable_stackwalk (void) return value_uint32_t != 0; } +static +inline +uint32_t +ep_rt_config_value_get_sampling_rate (void) +{ + uint32_t value_uint32_t = 0; + gchar *value = g_getenv ("DOTNET_EventPipeThreadSamplingRate"); + if (!value) + value = g_getenv ("COMPlus_EventPipeThreadSamplingRate"); + if (value) { + gchar *endptr = NULL; + guint64 parsed = strtoull (value, &endptr, 10); + if (endptr != value && *endptr == '\0' && value [0] != '-' && parsed <= G_MAXUINT32) + value_uint32_t = (uint32_t)parsed; + } + g_free (value); + return value_uint32_t; +} + /* * EventPipeSampleProfiler. */ diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets index 1ef84bdfc7b757..6442c23e87d1ac 100644 --- a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets +++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets @@ -482,9 +482,22 @@ Copyright (c) .NET Foundation. All rights reserved. - + + + <_WasmPerfInstFilter>$(WasmPerformanceInstrumentation) + <_WasmPerfInstInterval> + <_WasmPerfInstDescriptor> + <_WasmPerfInstInterval Condition="$(WasmPerformanceInstrumentation.Contains(',interval='))">$(WasmPerformanceInstrumentation.Substring($([MSBuild]::Add($(WasmPerformanceInstrumentation.IndexOf(',interval=')), 10)))) + <_WasmPerfInstFilter Condition="$(WasmPerformanceInstrumentation.Contains(',interval='))">$(WasmPerformanceInstrumentation.Substring(0, $(WasmPerformanceInstrumentation.IndexOf(',interval=')))) + <_WasmPerfInstInterval Condition="'$(_WasmPerfInstInterval)' != '' and $(_WasmPerfInstInterval.Contains(','))">$(_WasmPerfInstInterval.Substring(0, $(_WasmPerfInstInterval.IndexOf(',')))) + <_WasmPerfInstDescriptor Condition="'$(_WasmPerfInstFilter)' != '' and '$(UseMonoRuntime)' != 'false'">eventpipe,callspec=$(_WasmPerfInstFilter) + <_WasmPerfInstFilter Condition="'$(_WasmPerfInstFilter)' == 'all' and '$(UseMonoRuntime)' == 'false'">* + <_WasmPerfInstDescriptor Condition="'$(_WasmPerfInstFilter)' != '' and '$(UseMonoRuntime)' == 'false'">$(_WasmPerfInstFilter) + + + diff --git a/src/native/eventpipe/ds-ipc-pal-websocket.h b/src/native/eventpipe/ds-ipc-pal-websocket.h index 1273d9367c9d41..9b948ab89ed4db 100644 --- a/src/native/eventpipe/ds-ipc-pal-websocket.h +++ b/src/native/eventpipe/ds-ipc-pal-websocket.h @@ -53,11 +53,17 @@ struct _DiagnosticsIpcStream { }; #endif +#ifdef __cplusplus +extern "C" { +#endif extern int ds_rt_websocket_poll (int client_socket); extern int ds_rt_websocket_create (const char* url); -extern int ds_rt_websocket_recv (int client_socket, const uint8_t* buffer, uint32_t bytes_to_read); +extern int ds_rt_websocket_recv (int client_socket, uint8_t* buffer, uint32_t bytes_to_read); extern int ds_rt_websocket_send (int client_socket, const uint8_t* buffer, uint32_t bytes_to_write); extern int ds_rt_websocket_close(int client_socket); +#ifdef __cplusplus +} +#endif #endif /* ENABLE_PERFTRACING */ #endif /* __DIAGNOSTICS_IPC_PAL_WEB_SOCKET_H__ */ diff --git a/src/native/eventpipe/ep-rt.h b/src/native/eventpipe/ep-rt.h index ef28cb5147240c..d3cc68db3ffded 100644 --- a/src/native/eventpipe/ep-rt.h +++ b/src/native/eventpipe/ep-rt.h @@ -200,6 +200,11 @@ inline bool ep_rt_config_value_get_enable_stackwalk (void); +static +inline +uint32_t +ep_rt_config_value_get_sampling_rate (void); + /* * EventPipeSampleProfiler. */ diff --git a/src/native/eventpipe/ep.c b/src/native/eventpipe/ep.c index 0b45cf280d3fdc..9cf4acff6bdfed 100644 --- a/src/native/eventpipe/ep.c +++ b/src/native/eventpipe/ep.c @@ -1496,7 +1496,13 @@ ep_init (void) #else // PERFTRACING_DISABLE_THREADS const uint32_t default_profiler_sample_rate_in_nanoseconds = 5000000; // 5 msec. #endif // PERFTRACING_DISABLE_THREADS - ep_sample_profiler_set_sampling_rate (default_profiler_sample_rate_in_nanoseconds); + + // Allow overriding the sampling rate via DOTNET_EventPipeThreadSamplingRate (in milliseconds). + uint32_t configured_rate_ms = ep_rt_config_value_get_sampling_rate (); + if (configured_rate_ms > 0) + ep_sample_profiler_set_sampling_rate ((uint64_t)configured_rate_ms * 1000000); + else + ep_sample_profiler_set_sampling_rate (default_profiler_sample_rate_in_nanoseconds); _ep_deferred_enable_session_ids = dn_vector_alloc_t (EventPipeSessionID); _ep_deferred_disable_session_ids = dn_vector_alloc_t (EventPipeSessionID); From 25c2c303efc252a6661c463fc7662d86321e4e58 Mon Sep 17 00:00:00 2001 From: Matous Kozak <55735845+matouskozak@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:36:06 +0100 Subject: [PATCH 006/115] [clr-interp] Add initial CoreCLR interpreter func-eval support (#126576) ## Description This adds initial func-eval support for threads stopped in CoreCLR interpreter code. - Queue interpreter func-eval requests in the existing pending evals hash table (ProcessAnyPendingEvals) instead of using native-context hijacking - Execute pending evals from InterpBreakpoint after the debugger callback returns, reusing the shared dispatch path for both interpreter and exception-time evals - Skip native-only setup for interpreter evals: executable breakpoint segment allocation, SP alignment validation, and register/context updates that rely on real native frames - Rename m_evalDuringException to m_evalUsesHijack (inverted logic) to accurately describe the flag's purpose now that it covers both exception and interpreter paths - Move Init() skip logic inside DebuggerEval so callers don't need interpreter-specific conditionals - Return CORDBG_E_FUNC_EVAL_BAD_START_POINT for func-eval requests on interpreter threads not stopped at a breakpoint - Reuse the direct completion path for interpreter evals, where completion is signaled without the native breakpoint trap mechanism ## Testing - Locally on iOS simulator - DiagnosticTests: Func-eval tests passing under --clrinterpreter (osx-arm64, validated locally) - FuncEval.NestedFuncEvalTest - FuncEval.EscapeSecondPass FuncEval.InfiniteLoopASyncAbortTest is Windows-only and FuncEval.AssemblyLoadFuncEval is a Mono configuration. Fixes https://github.com/dotnet/runtime/issues/125959 --------- Co-authored-by: Milos Kotlar Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/ee/debugger.cpp | 122 ++++++++++++------ src/coreclr/debug/ee/debugger.h | 7 +- src/coreclr/debug/ee/funceval.cpp | 8 +- .../vm/datadescriptor/datadescriptor.inc | 2 +- src/coreclr/vm/dbginterface.h | 2 + src/coreclr/vm/interpexec.cpp | 32 ++++- .../FrameHandling/BaseFrameHandler.cs | 4 +- .../FrameHandling/X86FrameHandler.cs | 4 +- .../Data/Frames/DebuggerEval.cs | 4 +- 9 files changed, 133 insertions(+), 52 deletions(-) diff --git a/src/coreclr/debug/ee/debugger.cpp b/src/coreclr/debug/ee/debugger.cpp index d48fc9e68ee83b..b0641ac5daa5bd 100644 --- a/src/coreclr/debug/ee/debugger.cpp +++ b/src/coreclr/debug/ee/debugger.cpp @@ -1213,31 +1213,39 @@ ULONG DebuggerMethodInfoTable::CheckDmiTable(void) // Arguments: // pContext - The context to return to when done with this eval. // pEvalInfo - Contains all the important information, such as parameters, type args, method. -// fInException - TRUE if the thread for the eval is currently in an exception notification. -// bpInfoSegmentRX - bpInfoSegmentRX is an InteropSafe allocation allocated by the caller. -// (Caller allocated as there is no way to fail the allocation without -// throwing, and this function is called in a NOTHROW region) +// bpInfoSegmentRX - Non-NULL only when the eval hijacks the native CPU context through +// FuncEvalHijack. NULL for non-hijack evals (exception-time or interpreter), +// which complete via the pending-eval queue instead of a native breakpoint +// trap. Caller-allocated because this function is NOTHROW. // -DebuggerEval::DebuggerEval(CONTEXT * pContext, DebuggerIPCE_FuncEvalInfo * pEvalInfo, bool fInException, DebuggerEvalBreakpointInfoSegment* bpInfoSegmentRX) +DebuggerEval::DebuggerEval(CONTEXT * pContext, DebuggerIPCE_FuncEvalInfo * pEvalInfo, DebuggerEvalBreakpointInfoSegment* bpInfoSegmentRX) { WRAPPER_NO_CONTRACT; + if (bpInfoSegmentRX != NULL) + { #if !defined(DBI_COMPILE) && !defined(DACCESS_COMPILE) && defined(HOST_OSX) && defined(HOST_ARM64) - ExecutableWriterHolder bpInfoSegmentWriterHolder(bpInfoSegmentRX, sizeof(DebuggerEvalBreakpointInfoSegment)); - DebuggerEvalBreakpointInfoSegment *bpInfoSegmentRW = bpInfoSegmentWriterHolder.GetRW(); + ExecutableWriterHolder bpInfoSegmentWriterHolder(bpInfoSegmentRX, sizeof(DebuggerEvalBreakpointInfoSegment)); + DebuggerEvalBreakpointInfoSegment *bpInfoSegmentRW = bpInfoSegmentWriterHolder.GetRW(); #else // !DBI_COMPILE && !DACCESS_COMPILE && HOST_OSX && HOST_ARM64 - DebuggerEvalBreakpointInfoSegment *bpInfoSegmentRW = bpInfoSegmentRX; + DebuggerEvalBreakpointInfoSegment *bpInfoSegmentRW = bpInfoSegmentRX; #endif // !DBI_COMPILE && !DACCESS_COMPILE && HOST_OSX && HOST_ARM64 - new (bpInfoSegmentRW) DebuggerEvalBreakpointInfoSegment(this); - m_bpInfoSegment = bpInfoSegmentRX; + new (bpInfoSegmentRW) DebuggerEvalBreakpointInfoSegment(this); + m_bpInfoSegment = bpInfoSegmentRX; - // This must be non-zero so that the saved opcode is non-zero, and on IA64 we want it to be 0x16 - // so that we can have a breakpoint instruction in any slot in the bundle. - bpInfoSegmentRW->m_breakpointInstruction[0] = 0x16; + // This must be non-zero so that the saved opcode is non-zero, and on IA64 we want it to be 0x16 + // so that we can have a breakpoint instruction in any slot in the bundle. + bpInfoSegmentRW->m_breakpointInstruction[0] = 0x16; #if defined(TARGET_ARM) - USHORT *bp = (USHORT*)&m_bpInfoSegment->m_breakpointInstruction; - *bp = CORDbg_BREAK_INSTRUCTION; + USHORT *bp = (USHORT*)&m_bpInfoSegment->m_breakpointInstruction; + *bp = CORDbg_BREAK_INSTRUCTION; #endif // TARGET_ARM + } + else + { + m_bpInfoSegment = NULL; + } + m_thread = pEvalInfo->vmThreadToken.GetRawPtr(); m_evalType = pEvalInfo->funcEvalType; m_methodToken = pEvalInfo->funcMetadataToken; @@ -1263,7 +1271,10 @@ DebuggerEval::DebuggerEval(CONTEXT * pContext, DebuggerIPCE_FuncEvalInfo * pEval m_aborting = FE_ABORT_NONE; m_aborted = false; m_completed = false; - m_evalDuringException = fInException; + // Hijacked evals redirect the native CPU context through FuncEvalHijack; non-hijack + // evals (exception-time and interpreter) complete via the pending-eval queue. The + // presence of the breakpoint info segment is the single source of truth. + m_evalUsesHijack = (bpInfoSegmentRX != NULL); m_retValueBoxing = Debugger::NoValueTypeBoxing; m_vmObjectHandle = VMPTR_OBJECTHANDLE::NullPtr(); @@ -7556,7 +7567,7 @@ void Debugger::ProcessAnyPendingEvals(Thread *pThread) { DebuggerEval *pDE = pfe->pDE; - _ASSERTE(pDE->m_evalDuringException); + _ASSERTE(!pDE->m_evalUsesHijack); _ASSERTE(pDE->m_thread == GetThreadNULLOk()); // Remove the pending eval from the hash. This ensures that if we take a first chance exception during the eval @@ -14198,29 +14209,57 @@ HRESULT Debugger::FuncEvalSetup(DebuggerIPCE_FuncEvalInfo *pEvalInfo, return CORDBG_E_ILLEGAL_AT_GC_UNSAFE_POINT; } - if (filterContext != NULL && ::GetSP(filterContext) != ALIGN_DOWN(::GetSP(filterContext), STACK_ALIGN_SIZE)) + // A func eval uses a CONTEXT hijack (redirects the native CPU context through FuncEvalHijack) + // only when the thread is stopped at a breakpoint or single-step in JIT-compiled code. For + // exception-time evals and interpreter evals we cannot hijack the native context — those paths + // queue the DebuggerEval into the pending-eval table and let the suspend-resume logic dispatch + // it: for exceptions via Debugger::ProcessAnyPendingEvals on continue, for the interpreter via + // INTOP_BREAKPOINT after the debugger callback returns. + bool funcEvalUsesHijack = !fInException; +#ifdef FEATURE_INTERPRETER + if (funcEvalUsesHijack && filterContext != NULL) { - // SP is not aligned, we cannot do a FuncEval here - LOG((LF_CORDB, LL_INFO1000, "D::FES SP is unaligned")); - return CORDBG_E_FUNC_EVAL_BAD_START_POINT; + EECodeInfo codeInfo((PCODE)GetIP(filterContext)); + if (codeInfo.IsInterpretedCode()) + funcEvalUsesHijack = false; } +#endif // FEATURE_INTERPRETER - // Allocate the breakpoint instruction info for the debugger info in executable memory. - DebuggerHeap *pHeap = g_pDebugger->GetInteropSafeExecutableHeap_NoThrow(); - if (pHeap == NULL) + if (funcEvalUsesHijack) { - return E_OUTOFMEMORY; + _ASSERTE(filterContext != NULL); + if (::GetSP(filterContext) != ALIGN_DOWN(::GetSP(filterContext), STACK_ALIGN_SIZE)) + { + // SP is not aligned, we cannot do a FuncEval here + LOG((LF_CORDB, LL_INFO1000, "D::FES SP is unaligned")); + return CORDBG_E_FUNC_EVAL_BAD_START_POINT; + } } - DebuggerEvalBreakpointInfoSegment *bpInfoSegmentRX = (DebuggerEvalBreakpointInfoSegment*)pHeap->Alloc(sizeof(DebuggerEvalBreakpointInfoSegment)); - if (bpInfoSegmentRX == NULL) + // Allocate the breakpoint instruction info only for hijacked evals. Non-hijack paths + // (exception-time and interpreter) signal completion via FuncEvalComplete on the pending-eval + // queue, not via a native breakpoint trap, so the segment would never be used. Avoiding the + // allocation also means we don't require executable memory on platforms where it's unavailable + // (e.g. iOS). + DebuggerEvalBreakpointInfoSegment *bpInfoSegmentRX = NULL; + if (funcEvalUsesHijack) { - return E_OUTOFMEMORY; + DebuggerHeap *pHeap = g_pDebugger->GetInteropSafeExecutableHeap_NoThrow(); + if (pHeap == NULL) + { + return E_OUTOFMEMORY; + } + + bpInfoSegmentRX = (DebuggerEvalBreakpointInfoSegment*)pHeap->Alloc(sizeof(DebuggerEvalBreakpointInfoSegment)); + if (bpInfoSegmentRX == NULL) + { + return E_OUTOFMEMORY; + } } // Create a DebuggerEval to hold info about this eval while its in progress. Constructor copies the thread's // CONTEXT. - DebuggerEval *pDE = new (interopsafe, nothrow) DebuggerEval(filterContext, pEvalInfo, fInException, bpInfoSegmentRX); + DebuggerEval *pDE = new (interopsafe, nothrow) DebuggerEval(filterContext, pEvalInfo, bpInfoSegmentRX); if (pDE == NULL) { @@ -14259,9 +14298,9 @@ HRESULT Debugger::FuncEvalSetup(DebuggerIPCE_FuncEvalInfo *pEvalInfo, *argDataArea = pDE->m_argData; } - // Set the thread's IP (in the filter context) to our hijack function if we're stopped due to a breakpoint or single - // step. - if (!fInException) + // Hijacked evals rewrite the thread's native context to enter FuncEvalHijack when execution resumes. + // Non-hijack evals are queued in the pending-eval table and dispatched from the resume path. + if (funcEvalUsesHijack) { _ASSERTE(filterContext != NULL); @@ -14309,9 +14348,15 @@ HRESULT Debugger::FuncEvalSetup(DebuggerIPCE_FuncEvalInfo *pEvalInfo, DeleteInteropSafeExecutable(pDE); // Note this runs the destructor for DebuggerEval, which releases its internal buffers return (hr); } - // If we're in an exception, then add a pending eval for this thread. This will cause us to perform the func - // eval when the user continues the process after the current exception event. + + // Queue the eval. Exception-time evals run from Debugger::ProcessAnyPendingEvals when + // the process continues. Interpreter evals run from the INTOP_BREAKPOINT handler after + // the debugger callback returns — no context modification and no IncThreadsAtUnsafePlaces + // needed because the stack remains walkable. GetPendingEvals()->AddPendingEval(pDE->m_thread, pDE); + + LOG((LF_CORDB, LL_INFO1000, "D::FES: Non-hijack func eval setup for pDE:%p on thread %p (fInException=%d)\n", + pDE, pThread, fInException)); } @@ -15963,7 +16008,7 @@ unsigned FuncEvalFrame::GetFrameAttribs_Impl(void) { LIMITED_METHOD_DAC_CONTRACT; - if (GetDebuggerEval()->m_evalDuringException) + if (!GetDebuggerEval()->m_evalUsesHijack) { return FRAME_ATTR_NONE; } @@ -15977,7 +16022,7 @@ TADDR FuncEvalFrame::GetReturnAddressPtr_Impl() { LIMITED_METHOD_DAC_CONTRACT; - if (GetDebuggerEval()->m_evalDuringException) + if (!GetDebuggerEval()->m_evalUsesHijack) { return (TADDR)NULL; } @@ -15995,8 +16040,9 @@ void FuncEvalFrame::UpdateRegDisplay_Impl(const PREGDISPLAY pRD, bool updateFloa SUPPORTS_DAC; DebuggerEval * pDE = GetDebuggerEval(); - // No context to update if we're doing a func eval from within exception processing. - if (pDE->m_evalDuringException) + // No context to update if we're doing a func eval from within exception processing + // or from interpreter code (both skip the hijack path). + if (!pDE->m_evalUsesHijack) { return; } diff --git a/src/coreclr/debug/ee/debugger.h b/src/coreclr/debug/ee/debugger.h index 6a3fbe9bd2b65c..20139885d95316 100644 --- a/src/coreclr/debug/ee/debugger.h +++ b/src/coreclr/debug/ee/debugger.h @@ -3478,15 +3478,18 @@ class DebuggerEval FUNC_EVAL_ABORT_TYPE m_aborting; // Has an abort been requested, and what type. bool m_aborted; // Was this eval aborted bool m_completed; // Is the eval complete - successfully or by aborting - bool m_evalDuringException; + bool m_evalUsesHijack; VMPTR_OBJECTHANDLE m_vmObjectHandle; TypeHandle m_ownerTypeHandle; DebuggerEvalBreakpointInfoSegment* m_bpInfoSegment; - DebuggerEval(T_CONTEXT * pContext, DebuggerIPCE_FuncEvalInfo * pEvalInfo, bool fInException, DebuggerEvalBreakpointInfoSegment* bpInfoSegmentRX); + DebuggerEval(T_CONTEXT * pContext, DebuggerIPCE_FuncEvalInfo * pEvalInfo, DebuggerEvalBreakpointInfoSegment* bpInfoSegmentRX); bool Init() { + if (m_bpInfoSegment == NULL) + return true; + _ASSERTE(DbgIsExecutable(&m_bpInfoSegment->m_breakpointInstruction, sizeof(m_bpInfoSegment->m_breakpointInstruction))); return true; } diff --git a/src/coreclr/debug/ee/funceval.cpp b/src/coreclr/debug/ee/funceval.cpp index f251de3016ce34..d40c71c6d4daff 100644 --- a/src/coreclr/debug/ee/funceval.cpp +++ b/src/coreclr/debug/ee/funceval.cpp @@ -3822,7 +3822,7 @@ void * STDCALL FuncEvalHijackWorker(DebuggerEval *pDE) #endif #endif - if (!pDE->m_evalDuringException) + if (pDE->m_evalUsesHijack) { // // From this point forward we use FORBID regions to guard against GCs. @@ -3842,7 +3842,7 @@ void * STDCALL FuncEvalHijackWorker(DebuggerEval *pDE) if (filterContext) { - _ASSERTE(pDE->m_evalDuringException); + _ASSERTE(!pDE->m_evalUsesHijack); g_pEEInterface->SetThreadFilterContext(pDE->m_thread, NULL); } @@ -3901,7 +3901,7 @@ void * STDCALL FuncEvalHijackWorker(DebuggerEval *pDE) // Codepitching can hijack our frame's return address. That means that we'll need to update PC in our saved context // so that when its restored, its like we've returned to the codepitching hijack. At this point, the old value of // EIP is worthless anyway. - if (!pDE->m_evalDuringException) + if (pDE->m_evalUsesHijack) { SetIP(&pDE->m_context, (SIZE_T)FEFrame.GetReturnAddress()); } @@ -3913,7 +3913,7 @@ void * STDCALL FuncEvalHijackWorker(DebuggerEval *pDE) void *dest = NULL; - if (!pDE->m_evalDuringException) + if (pDE->m_evalUsesHijack) { // Signal to the helper thread that we're done with our func eval. Start by creating a DebuggerFuncEvalComplete // object. Give it an address at which to create the patch, which is a chunk of memory specified by our diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 64528180895016..6caeddf628fbd8 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -986,7 +986,7 @@ CDAC_TYPE_END(FuncEvalFrame) CDAC_TYPE_BEGIN(DebuggerEval) CDAC_TYPE_SIZE(sizeof(DebuggerEval)) CDAC_TYPE_FIELD(DebuggerEval, EXTERN_TYPE(Context), TargetContext, offsetof(DebuggerEval, m_context)) -CDAC_TYPE_FIELD(DebuggerEval, T_BOOL, EvalDuringException, offsetof(DebuggerEval, m_evalDuringException)) +CDAC_TYPE_FIELD(DebuggerEval, T_BOOL, EvalUsesHijack, offsetof(DebuggerEval, m_evalUsesHijack)) CDAC_TYPE_END(DebuggerEval) #endif // DEBUGGING_SUPPORTED diff --git a/src/coreclr/vm/dbginterface.h b/src/coreclr/vm/dbginterface.h index 3c58a5cc2d1977..90bf54abfee821 100644 --- a/src/coreclr/vm/dbginterface.h +++ b/src/coreclr/vm/dbginterface.h @@ -388,6 +388,8 @@ class DebugInterface virtual HRESULT IsMethodDeoptimized(Module *pModule, mdMethodDef methodDef, BOOL *pResult) = 0; virtual void MulticastTraceNextStep(DELEGATEREF pbDel, INT32 count) = 0; virtual void ExternalMethodFixupNextStep(PCODE address) = 0; + virtual void ProcessAnyPendingEvals(Thread* pThread) = 0; + #endif //DACCESS_COMPILE }; diff --git a/src/coreclr/vm/interpexec.cpp b/src/coreclr/vm/interpexec.cpp index d89ae68d123d6f..7fe0a4b3f00955 100644 --- a/src/coreclr/vm/interpexec.cpp +++ b/src/coreclr/vm/interpexec.cpp @@ -766,7 +766,6 @@ static void InterpBreakpoint(const int32_t *ip, const InterpMethodContextFrame * exceptionRecord.ExceptionCode = STATUS_BREAKPOINT; exceptionRecord.ExceptionAddress = (PVOID)ip; - // Construct a CONTEXT for the debugger CONTEXT ctx; memset(&ctx, 0, sizeof(CONTEXT)); @@ -789,10 +788,41 @@ static void InterpBreakpoint(const int32_t *ip, const InterpMethodContextFrame * STATUS_BREAKPOINT, pThread)) { + InterpThreadContext *pThreadContext = pThread->GetInterpThreadContext(); + + const int32_t *savedBypassAddress = pThreadContext->m_bypassAddress; + int32_t savedBypassOpcode = pThreadContext->m_bypassOpcode; + + // Clear the bypass before dispatching pending evals + pThreadContext->m_bypassAddress = NULL; + pThreadContext->m_bypassOpcode = 0; + + pThread->SetFilterContext(&ctx); + EX_TRY + { + g_pDebugInterface->ProcessAnyPendingEvals(pThread); + } + EX_CATCH + { + pThread->SetFilterContext(NULL); + pThreadContext->m_bypassAddress = savedBypassAddress; + pThreadContext->m_bypassOpcode = savedBypassOpcode; + EX_RETHROW; + } + EX_END_CATCH + pThread->SetFilterContext(NULL); + + // The debugger may have moved execution via SetIP. If so, drop the bypass + // (it was set up for the original IP) and resume at the new context via + // ResumeAfterCatchException. if ((GetIP(&ctx) != (PCODE)ip) || (GetSP(&ctx) != (DWORD64)pFrame)) { ThrowResumeAfterCatchException(GetSP(&ctx), GetIP(&ctx)); } + + // No SetIP change — restore the bypass so the original opcode runs once. + pThreadContext->m_bypassAddress = savedBypassAddress; + pThreadContext->m_bypassOpcode = savedBypassOpcode; } } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/BaseFrameHandler.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/BaseFrameHandler.cs index f27cd11ecc36b9..3d56a63532613e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/BaseFrameHandler.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/BaseFrameHandler.cs @@ -59,8 +59,8 @@ public virtual void HandleFuncEvalFrame(FuncEvalFrame funcEvalFrame) { Data.DebuggerEval debuggerEval = _target.ProcessedData.GetOrAdd(funcEvalFrame.DebuggerEvalPtr); - // No context to update if we're doing a func eval from within exception processing. - if (debuggerEval.EvalDuringException) + // No context to update if the eval doesn't use a hijack (exception or interpreter path). + if (!debuggerEval.EvalUsesHijack) { return; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/X86FrameHandler.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/X86FrameHandler.cs index af2d60cd31c156..8b06193322f99f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/X86FrameHandler.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/X86FrameHandler.cs @@ -50,8 +50,8 @@ public override void HandleFuncEvalFrame(FuncEvalFrame funcEvalFrame) { Data.DebuggerEval debuggerEval = _target.ProcessedData.GetOrAdd(funcEvalFrame.DebuggerEvalPtr); - // No context to update if we're doing a func eval from within exception processing. - if (debuggerEval.EvalDuringException) + // No context to update if the eval doesn't use a hijack (exception or interpreter path). + if (!debuggerEval.EvalUsesHijack) { return; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DebuggerEval.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DebuggerEval.cs index e162ce98ae3771..a68afc4f53d3a0 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DebuggerEval.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DebuggerEval.cs @@ -12,11 +12,11 @@ public DebuggerEval(Target target, TargetPointer address) { Target.TypeInfo type = target.GetTypeInfo(DataType.DebuggerEval); TargetContext = address + (ulong)type.Fields[nameof(TargetContext)].Offset; - EvalDuringException = target.ReadField(address, type, nameof(EvalDuringException)) != 0; + EvalUsesHijack = target.ReadField(address, type, nameof(EvalUsesHijack)) != 0; Address = address; } public TargetPointer Address { get; } public TargetPointer TargetContext { get; } - public bool EvalDuringException { get; } + public bool EvalUsesHijack { get; } } From d101f7649ff08f7ff87ca90d5021de106b6c4aa4 Mon Sep 17 00:00:00 2001 From: Milos Kotlar Date: Wed, 29 Apr 2026 09:54:11 +0200 Subject: [PATCH 007/115] [ios-clr] Enable ResourceAssemblyLoadContext fallback for Apple mobile CoreCLR (#127058) ## Description Part of splitting #125439 into smaller, self-contained PRs. On Apple mobile platforms, test assemblies are deployed as files inside the `.app` bundle rather than being embedded as managed resources in the host test assembly. As a result, `Assembly.GetManifestResourceStream` returns null in `ResourceAssemblyLoadContext` and any test that relies on it fails. This PR updates `ResourceAssemblyLoadContext` to fall back to loading the assembly from `AppContext.BaseDirectory` when the embedded resource is not present. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot --- .../DefaultContext/DefaultLoadContextTest.cs | 1 + ...Runtime.Loader.DefaultContext.Tests.csproj | 6 +++++ .../tests/ILLink.Descriptors.xml | 2 ++ ...ime.Loader.RefEmitLoadContext.Tests.csproj | 8 ++++++- .../tests/ResourceAssemblyLoadContext.cs | 22 +++++++++++++++++++ src/libraries/tests.proj | 1 - 6 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Runtime.Loader/tests/DefaultContext/DefaultLoadContextTest.cs b/src/libraries/System.Runtime.Loader/tests/DefaultContext/DefaultLoadContextTest.cs index ab0814f4e1e7be..4d506e46437f65 100644 --- a/src/libraries/System.Runtime.Loader/tests/DefaultContext/DefaultLoadContextTest.cs +++ b/src/libraries/System.Runtime.Loader/tests/DefaultContext/DefaultLoadContextTest.cs @@ -86,6 +86,7 @@ private Assembly ResolveNullAssembly(AssemblyLoadContext sender, AssemblyName as // Does not apply to Mono AOT scenarios as it is expected the name of the .aotdata file matches // the true name of the assembly and not the physical file name. [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoAOT), nameof(PlatformDetection.HasAssemblyFiles))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124344", typeof(PlatformDetection), nameof(PlatformDetection.IsAppleMobile), nameof(PlatformDetection.IsCoreCLR))] public void LoadInDefaultContext() { // This will attempt to load an assembly, by path, in the Default Load context via the Resolving event diff --git a/src/libraries/System.Runtime.Loader/tests/DefaultContext/System.Runtime.Loader.DefaultContext.Tests.csproj b/src/libraries/System.Runtime.Loader/tests/DefaultContext/System.Runtime.Loader.DefaultContext.Tests.csproj index 39ecb948c19636..3b55c05d9f9e3b 100644 --- a/src/libraries/System.Runtime.Loader/tests/DefaultContext/System.Runtime.Loader.DefaultContext.Tests.csproj +++ b/src/libraries/System.Runtime.Loader/tests/DefaultContext/System.Runtime.Loader.DefaultContext.Tests.csproj @@ -15,4 +15,10 @@ CopyToOutputDirectory="PreserveNewest" TargetPath="System.Runtime.Loader.Noop.Assembly_test.dll" /> + + + + + + diff --git a/src/libraries/System.Runtime.Loader/tests/ILLink.Descriptors.xml b/src/libraries/System.Runtime.Loader/tests/ILLink.Descriptors.xml index 7fa0c9a0982542..991945c09a3ad2 100644 --- a/src/libraries/System.Runtime.Loader/tests/ILLink.Descriptors.xml +++ b/src/libraries/System.Runtime.Loader/tests/ILLink.Descriptors.xml @@ -2,4 +2,6 @@ + + diff --git a/src/libraries/System.Runtime.Loader/tests/RefEmitLoadContext/System.Runtime.Loader.RefEmitLoadContext.Tests.csproj b/src/libraries/System.Runtime.Loader/tests/RefEmitLoadContext/System.Runtime.Loader.RefEmitLoadContext.Tests.csproj index 8be7fcae782447..f203c97e099cf6 100644 --- a/src/libraries/System.Runtime.Loader/tests/RefEmitLoadContext/System.Runtime.Loader.RefEmitLoadContext.Tests.csproj +++ b/src/libraries/System.Runtime.Loader/tests/RefEmitLoadContext/System.Runtime.Loader.RefEmitLoadContext.Tests.csproj @@ -14,4 +14,10 @@ OutputItemType="content" CopyToOutputDirectory="PreserveNewest" /> - \ No newline at end of file + + + + + + + diff --git a/src/libraries/System.Runtime.Loader/tests/ResourceAssemblyLoadContext.cs b/src/libraries/System.Runtime.Loader/tests/ResourceAssemblyLoadContext.cs index 84ad66d3433455..8b95fb944e6338 100644 --- a/src/libraries/System.Runtime.Loader/tests/ResourceAssemblyLoadContext.cs +++ b/src/libraries/System.Runtime.Loader/tests/ResourceAssemblyLoadContext.cs @@ -31,6 +31,28 @@ protected override Assembly Load(AssemblyName assemblyName) if (asmStream == null) { + // On platforms where assemblies are deployed as files alongside the app + // (e.g., Apple mobile CoreCLR) rather than embedded resources, fall back + // to loading from the app directory. + string basePath = Path.Combine(AppContext.BaseDirectory, assembly); + if (File.Exists(basePath)) + { + if (LoadBy == LoadBy.Path) + { + var tempPath = Directory.CreateTempSubdirectory().FullName; + string path = Path.Combine(tempPath, assembly); + File.Copy(basePath, path); + return LoadFromAssemblyPath(path); + } + else if (LoadBy == LoadBy.Stream) + { + using (FileStream stream = File.OpenRead(basePath)) + { + return LoadFromStream(stream); + } + } + } + return null; } diff --git a/src/libraries/tests.proj b/src/libraries/tests.proj index e9007937c35bcc..169c05b0b06517 100644 --- a/src/libraries/tests.proj +++ b/src/libraries/tests.proj @@ -656,7 +656,6 @@ - From 126ec7b3d7143d4ee05ede93e5012d046005768f Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:04:39 +0200 Subject: [PATCH 008/115] JIT: Rename JitDasmWithAddress to JitDisasmWithAddress (#127267) --- src/coreclr/jit/compiler.cpp | 2 +- src/coreclr/jit/jitconfigvalues.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coreclr/jit/compiler.cpp b/src/coreclr/jit/compiler.cpp index a7e591163e109c..daf91aaf30e983 100644 --- a/src/coreclr/jit/compiler.cpp +++ b/src/coreclr/jit/compiler.cpp @@ -2687,7 +2687,7 @@ void Compiler::compInitOptions(JitFlags* jitFlags) } #endif // LATE_DISASM - if (JitConfig.JitDasmWithAddress() != 0) + if (JitConfig.JitDisasmWithAddress() != 0) { opts.disAddr = true; } diff --git a/src/coreclr/jit/jitconfigvalues.h b/src/coreclr/jit/jitconfigvalues.h index e747bb8223c03d..41f52190955dab 100644 --- a/src/coreclr/jit/jitconfigvalues.h +++ b/src/coreclr/jit/jitconfigvalues.h @@ -347,7 +347,7 @@ CONFIG_INTEGER(JitDisasmWithDebugInfo, "JitDisasmWithDebugInfo", 0) CONFIG_INTEGER(JitDisasmSpilled, "JitDisasmSpilled", 0) // Print the process address next to each instruction of the disassembly -CONFIG_INTEGER(JitDasmWithAddress, "JitDasmWithAddress", 0) +CONFIG_INTEGER(JitDisasmWithAddress, "JitDisasmWithAddress", 0) RELEASE_CONFIG_STRING(JitStdOutFile, "JitStdOutFile") // If set, sends JIT's stdout output to this file. From 03df3922283c2fe198085e027113d13a0cd9a053 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marie=20P=C3=ADchov=C3=A1?= <11718369+ManickaP@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:22:52 +0200 Subject: [PATCH 009/115] [QUIC] Do not allocate exception if it might not be used (#127526) --- .../System.Net.Quic/src/System/Net/Quic/QuicStream.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicStream.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicStream.cs index b9a9b8df071951..133cd1aa7030a6 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicStream.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicStream.cs @@ -672,7 +672,10 @@ private int HandleEventShutdownComplete(ref SHUTDOWN_COMPLETE_DATA data) _receiveTcs.TrySetException(exception); _sendTcs.TrySetException(exception); } - _startedTcs.TrySetException(ThrowHelper.GetOperationAbortedException()); + if (!_startedTcs.IsCompleted) + { + _startedTcs.TrySetException(ThrowHelper.GetOperationAbortedException()); + } _shutdownTcs.TrySetResult(); return QUIC_STATUS_SUCCESS; } From 920b3106febb287f7486a2a6614ff4ad13f13d78 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:36:49 +0200 Subject: [PATCH 010/115] JIT: Replace PERFSCORE_MEMORY_* macros with PerfScoreMemoryAccessKind enum class (#127148) --- src/coreclr/jit/emit.cpp | 4 +-- src/coreclr/jit/emit.h | 23 +++++++++-------- src/coreclr/jit/emitarm64.cpp | 34 ++++++++++++------------ src/coreclr/jit/emitloongarch64.cpp | 16 +++++++----- src/coreclr/jit/emitriscv64.cpp | 6 ++--- src/coreclr/jit/emitxarch.cpp | 40 ++++++++++++++--------------- 6 files changed, 63 insertions(+), 60 deletions(-) diff --git a/src/coreclr/jit/emit.cpp b/src/coreclr/jit/emit.cpp index 98f74b755e1408..8f36252fb769bc 100644 --- a/src/coreclr/jit/emit.cpp +++ b/src/coreclr/jit/emit.cpp @@ -1393,7 +1393,7 @@ float emitter::insEvaluateExecutionCost(instrDesc* id) insExecutionCharacteristics result = getInsExecutionCharacteristics(id); float throughput = result.insThroughput; float latency = result.insLatency; - unsigned memAccessKind = result.insMemoryAccessKind; + PerfScoreMemoryAccessKind memAccessKind = result.insMemoryAccessKind; // Check for PERFSCORE_THROUGHPUT_ILLEGAL and PERFSCORE_LATENCY_ILLEGAL. // Note that 0.0 throughput is allowed for pseudo-instructions in the instrDesc list that won't actually @@ -1401,7 +1401,7 @@ float emitter::insEvaluateExecutionCost(instrDesc* id) assert(throughput >= 0.0); assert(latency >= 0.0); - if (memAccessKind == PERFSCORE_MEMORY_WRITE || memAccessKind == PERFSCORE_MEMORY_READ_WRITE) + if ((memAccessKind == PerfScoreMemoryAccessKind::Write) || (memAccessKind == PerfScoreMemoryAccessKind::ReadWrite)) { // We assume that we won't read back from memory for the next WR_GENERAL cycles // Thus we normally won't pay latency costs for writes. diff --git a/src/coreclr/jit/emit.h b/src/coreclr/jit/emit.h index 039e2ccaad169e..28e61029a4c06e 100644 --- a/src/coreclr/jit/emit.h +++ b/src/coreclr/jit/emit.h @@ -2026,11 +2026,19 @@ class emitter } }; // End of struct instrDesc + enum class PerfScoreMemoryAccessKind : unsigned + { + None = 0, + Read = 1, + Write = 2, + ReadWrite = 3, + }; + #if defined(TARGET_XARCH) insFormat getMemoryOperation(instrDesc* id) const; insFormat ExtractMemoryFormat(insFormat insFmt) const; #elif defined(TARGET_ARM64) - void getMemoryOperation(instrDesc* id, unsigned* pMemAccessKind, bool* pIsLocalAccess); + void getMemoryOperation(instrDesc* id, PerfScoreMemoryAccessKind* pMemAccessKind, bool* pIsLocalAccess); #endif #if defined(DEBUG) || defined(LATE_DISASM) @@ -2212,18 +2220,11 @@ class emitter #error Unknown TARGET #endif -// Make this an enum: -// -#define PERFSCORE_MEMORY_NONE 0 -#define PERFSCORE_MEMORY_READ 1 -#define PERFSCORE_MEMORY_WRITE 2 -#define PERFSCORE_MEMORY_READ_WRITE 3 - struct insExecutionCharacteristics { - float insThroughput; - float insLatency; - unsigned insMemoryAccessKind; + float insThroughput = 0; + float insLatency = 0; + PerfScoreMemoryAccessKind insMemoryAccessKind = PerfScoreMemoryAccessKind::None; }; float insEvaluateExecutionCost(instrDesc* id); diff --git a/src/coreclr/jit/emitarm64.cpp b/src/coreclr/jit/emitarm64.cpp index 2056d8dabee674..254a81fe57ec22 100644 --- a/src/coreclr/jit/emitarm64.cpp +++ b/src/coreclr/jit/emitarm64.cpp @@ -15257,11 +15257,11 @@ regNumber emitter::emitInsTernary(instruction ins, emitAttr attr, GenTree* dst, #if defined(DEBUG) || defined(LATE_DISASM) -void emitter::getMemoryOperation(instrDesc* id, unsigned* pMemAccessKind, bool* pIsLocalAccess) +void emitter::getMemoryOperation(instrDesc* id, PerfScoreMemoryAccessKind* pMemAccessKind, bool* pIsLocalAccess) { - unsigned memAccessKind = PERFSCORE_MEMORY_NONE; - bool isLocalAccess = false; - instruction ins = id->idIns(); + PerfScoreMemoryAccessKind memAccessKind = PerfScoreMemoryAccessKind::None; + bool isLocalAccess = false; + instruction ins = id->idIns(); if (emitInsIsLoadOrStore(ins)) { @@ -15269,17 +15269,17 @@ void emitter::getMemoryOperation(instrDesc* id, unsigned* pMemAccessKind, bool* { if (emitInsIsStore(ins)) { - memAccessKind = PERFSCORE_MEMORY_READ_WRITE; + memAccessKind = PerfScoreMemoryAccessKind::ReadWrite; } else { - memAccessKind = PERFSCORE_MEMORY_READ; + memAccessKind = PerfScoreMemoryAccessKind::Read; } } else { assert(emitInsIsStore(ins)); - memAccessKind = PERFSCORE_MEMORY_WRITE; + memAccessKind = PerfScoreMemoryAccessKind::Write; } insFormat insFmt = id->idInsFmt(); @@ -15412,7 +15412,7 @@ void emitter::getMemoryOperation(instrDesc* id, unsigned* pMemAccessKind, bool* default: assert(!"Logic Error"); - memAccessKind = PERFSCORE_MEMORY_NONE; + memAccessKind = PerfScoreMemoryAccessKind::None; break; } } @@ -15444,8 +15444,8 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins instruction ins = id->idIns(); insFormat insFmt = id->idInsFmt(); - unsigned memAccessKind; - bool isLocalAccess; + PerfScoreMemoryAccessKind memAccessKind; + bool isLocalAccess; getMemoryOperation(id, &memAccessKind, &isLocalAccess); result.insThroughput = PERFSCORE_THROUGHPUT_ILLEGAL; @@ -15453,15 +15453,15 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins // Initialize insLatency based upon the instruction's memAccessKind and local access values // - if (memAccessKind == PERFSCORE_MEMORY_READ) + if (memAccessKind == PerfScoreMemoryAccessKind::Read) { result.insLatency = isLocalAccess ? PERFSCORE_LATENCY_RD_STACK : PERFSCORE_LATENCY_RD_GENERAL; } - else if (memAccessKind == PERFSCORE_MEMORY_WRITE) + else if (memAccessKind == PerfScoreMemoryAccessKind::Write) { result.insLatency = isLocalAccess ? PERFSCORE_LATENCY_WR_STACK : PERFSCORE_LATENCY_WR_GENERAL; } - else if (memAccessKind == PERFSCORE_MEMORY_READ_WRITE) + else if (memAccessKind == PerfScoreMemoryAccessKind::ReadWrite) { result.insLatency = isLocalAccess ? PERFSCORE_LATENCY_RD_WR_STACK : PERFSCORE_LATENCY_RD_WR_GENERAL; } @@ -15779,7 +15779,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins case IF_LS_3B: // ldp, ldpsw, ldnp, stp, stnp (load/store pair zero offset) case IF_LS_3C: // load/store pair with offset pre/post inc - if (memAccessKind == PERFSCORE_MEMORY_READ) + if (memAccessKind == PerfScoreMemoryAccessKind::Read) { // ldp, ldpsw, ldnp result.insThroughput = PERFSCORE_THROUGHPUT_1C; @@ -15800,7 +15800,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins else // store instructions { // stp, stnp - assert(memAccessKind == PERFSCORE_MEMORY_WRITE); + assert(memAccessKind == PerfScoreMemoryAccessKind::Write); result.insThroughput = PERFSCORE_THROUGHPUT_1C; } break; @@ -15814,7 +15814,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins break; case IF_LS_3E: // ARMv8.1 LSE Atomics - if (memAccessKind == PERFSCORE_MEMORY_WRITE) + if (memAccessKind == PerfScoreMemoryAccessKind::Write) { // staddb, staddlb, staddh, staddlh, stadd. staddl result.insThroughput = PERFSCORE_THROUGHPUT_2C; @@ -15822,7 +15822,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins } else { - assert(memAccessKind == PERFSCORE_MEMORY_READ_WRITE); + assert(memAccessKind == PerfScoreMemoryAccessKind::ReadWrite); result.insThroughput = PERFSCORE_THROUGHPUT_3C; result.insLatency = max(PERFSCORE_LATENCY_3C, result.insLatency); } diff --git a/src/coreclr/jit/emitloongarch64.cpp b/src/coreclr/jit/emitloongarch64.cpp index 9ec429118953eb..d7b878bad395d2 100644 --- a/src/coreclr/jit/emitloongarch64.cpp +++ b/src/coreclr/jit/emitloongarch64.cpp @@ -5256,7 +5256,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins result.insThroughput = PERFSCORE_THROUGHPUT_ILLEGAL; result.insLatency = PERFSCORE_LATENCY_ILLEGAL; - result.insMemoryAccessKind = PERFSCORE_MEMORY_NONE; + result.insMemoryAccessKind = PerfScoreMemoryAccessKind::None; // Calculate merge emit instructions cost. unsigned CombinedInsCnt = id->idCodeSize() / sizeof(code_t); @@ -5273,7 +5273,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins } else // ins == load { // pcaddu12i + load or lu12i.w + lu32i.d + load - result.insMemoryAccessKind = PERFSCORE_MEMORY_READ; + result.insMemoryAccessKind = PerfScoreMemoryAccessKind::Read; result.insThroughput = (CombinedInsCnt == 2) ? PERFSCORE_THROUGHPUT_4C : PERFSCORE_THROUGHPUT_7C; if ((INS_ld_b <= ins) && (ins <= INS_ld_wu)) { @@ -5322,9 +5322,10 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins } else if (id->idInsOpt() == INS_OPTS_RELOC) { // pcalau12i + (addi.d or ld.d) - result.insLatency = id->idIsCnsReloc() ? PERFSCORE_LATENCY_2C : PERFSCORE_LATENCY_5C; - result.insThroughput = id->idIsCnsReloc() ? PERFSCORE_THROUGHPUT_6C : PERFSCORE_THROUGHPUT_4C; - result.insMemoryAccessKind = id->idIsCnsReloc() ? PERFSCORE_MEMORY_NONE : PERFSCORE_MEMORY_READ; + result.insLatency = id->idIsCnsReloc() ? PERFSCORE_LATENCY_2C : PERFSCORE_LATENCY_5C; + result.insThroughput = id->idIsCnsReloc() ? PERFSCORE_THROUGHPUT_6C : PERFSCORE_THROUGHPUT_4C; + result.insMemoryAccessKind = + id->idIsCnsReloc() ? PerfScoreMemoryAccessKind::None : PerfScoreMemoryAccessKind::Read; } else { @@ -5340,12 +5341,13 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins { if (emitInsIsLoad(ins)) { - result.insMemoryAccessKind = emitInsIsStore(ins) ? PERFSCORE_MEMORY_READ_WRITE : PERFSCORE_MEMORY_READ; + result.insMemoryAccessKind = + emitInsIsStore(ins) ? PerfScoreMemoryAccessKind::ReadWrite : PerfScoreMemoryAccessKind::Read; } else { assert(emitInsIsStore(ins)); - result.insMemoryAccessKind = PERFSCORE_MEMORY_WRITE; + result.insMemoryAccessKind = PerfScoreMemoryAccessKind::Write; } } diff --git a/src/coreclr/jit/emitriscv64.cpp b/src/coreclr/jit/emitriscv64.cpp index e25e252ecd07fb..70b17f905ca915 100644 --- a/src/coreclr/jit/emitriscv64.cpp +++ b/src/coreclr/jit/emitriscv64.cpp @@ -5605,7 +5605,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins insExecutionCharacteristics result; result.insThroughput = PERFSCORE_LATENCY_1C; result.insLatency = PERFSCORE_THROUGHPUT_1C; - result.insMemoryAccessKind = PERFSCORE_MEMORY_NONE; + result.insMemoryAccessKind = PerfScoreMemoryAccessKind::None; unsigned codeSize = id->idCodeSize(); assert((codeSize >= 2) && ((codeSize % 2) == 0)); @@ -5713,7 +5713,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins case MajorOpcode::Amo: result.insLatency = result.insThroughput = PERFSCORE_LATENCY_5C; - result.insMemoryAccessKind = PERFSCORE_MEMORY_READ_WRITE; + result.insMemoryAccessKind = PerfScoreMemoryAccessKind::ReadWrite; break; case MajorOpcode::Branch: @@ -5765,7 +5765,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins result.insLatency += PERFSCORE_LATENCY_1C; // assume non-stack load/stores are more likely to cache-miss result.insThroughput += immediateBuildingCost; - result.insMemoryAccessKind = isLoad ? PERFSCORE_MEMORY_READ : PERFSCORE_MEMORY_WRITE; + result.insMemoryAccessKind = isLoad ? PerfScoreMemoryAccessKind::Read : PerfScoreMemoryAccessKind::Write; break; } diff --git a/src/coreclr/jit/emitxarch.cpp b/src/coreclr/jit/emitxarch.cpp index c4ddbfd26bc6d9..06edda78c24fbe 100644 --- a/src/coreclr/jit/emitxarch.cpp +++ b/src/coreclr/jit/emitxarch.cpp @@ -20302,9 +20302,9 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins // Model the memory throughput, latency, kind - float memThroughput = PERFSCORE_THROUGHPUT_ILLEGAL; - float memLatency = PERFSCORE_LATENCY_ILLEGAL; - unsigned memAccessKind = 0; + float memThroughput = PERFSCORE_THROUGHPUT_ILLEGAL; + float memLatency = PERFSCORE_LATENCY_ILLEGAL; + PerfScoreMemoryAccessKind memAccessKind = PerfScoreMemoryAccessKind::None; switch (memFmt) { @@ -20314,7 +20314,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins { memThroughput = PERFSCORE_THROUGHPUT_RD; memLatency = PERFSCORE_LATENCY_RD_STACK; - memAccessKind = PERFSCORE_MEMORY_READ; + memAccessKind = PerfScoreMemoryAccessKind::Read; break; } @@ -20322,7 +20322,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins { memThroughput = PERFSCORE_THROUGHPUT_WR; memLatency = PERFSCORE_LATENCY_WR_STACK; - memAccessKind = PERFSCORE_MEMORY_WRITE; + memAccessKind = PerfScoreMemoryAccessKind::Write; break; } @@ -20330,7 +20330,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins { memThroughput = PERFSCORE_THROUGHPUT_RW; memLatency = PERFSCORE_LATENCY_RD_WR_STACK; - memAccessKind = PERFSCORE_MEMORY_READ_WRITE; + memAccessKind = PerfScoreMemoryAccessKind::ReadWrite; break; } @@ -20340,7 +20340,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins { memThroughput = PERFSCORE_THROUGHPUT_RD; memLatency = PERFSCORE_LATENCY_RD_CONST_ADDR; - memAccessKind = PERFSCORE_MEMORY_READ; + memAccessKind = PerfScoreMemoryAccessKind::Read; break; } @@ -20348,7 +20348,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins { memThroughput = PERFSCORE_THROUGHPUT_WR; memLatency = PERFSCORE_LATENCY_WR_CONST_ADDR; - memAccessKind = PERFSCORE_MEMORY_WRITE; + memAccessKind = PerfScoreMemoryAccessKind::Write; break; } @@ -20356,7 +20356,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins { memThroughput = PERFSCORE_THROUGHPUT_RW; memLatency = PERFSCORE_LATENCY_RD_WR_CONST_ADDR; - memAccessKind = PERFSCORE_MEMORY_READ_WRITE; + memAccessKind = PerfScoreMemoryAccessKind::ReadWrite; break; } @@ -20366,7 +20366,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins { memThroughput = PERFSCORE_THROUGHPUT_RD; memLatency = PERFSCORE_LATENCY_RD_GENERAL; - memAccessKind = PERFSCORE_MEMORY_READ; + memAccessKind = PerfScoreMemoryAccessKind::Read; break; } @@ -20374,7 +20374,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins { memThroughput = PERFSCORE_THROUGHPUT_WR; memLatency = PERFSCORE_LATENCY_WR_GENERAL; - memAccessKind = PERFSCORE_MEMORY_WRITE; + memAccessKind = PerfScoreMemoryAccessKind::Write; break; } @@ -20382,7 +20382,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins { memThroughput = PERFSCORE_THROUGHPUT_RW; memLatency = PERFSCORE_LATENCY_RD_WR_GENERAL; - memAccessKind = PERFSCORE_MEMORY_READ_WRITE; + memAccessKind = PerfScoreMemoryAccessKind::ReadWrite; break; } @@ -20390,7 +20390,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins { memThroughput = PERFSCORE_THROUGHPUT_ZERO; memLatency = PERFSCORE_LATENCY_ZERO; - memAccessKind = PERFSCORE_MEMORY_NONE; + memAccessKind = PerfScoreMemoryAccessKind::None; break; } @@ -20399,7 +20399,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins assert(!"Unhandled insFmt for switch (memFmt)"); memThroughput = PERFSCORE_THROUGHPUT_ZERO; memLatency = PERFSCORE_LATENCY_ZERO; - memAccessKind = PERFSCORE_MEMORY_NONE; + memAccessKind = PerfScoreMemoryAccessKind::None; break; } } @@ -20522,7 +20522,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins } else { - assert(memAccessKind == PERFSCORE_MEMORY_WRITE); // _SHF form never emitted + assert(memAccessKind == PerfScoreMemoryAccessKind::Write); // _SHF form never emitted insThroughput = PERFSCORE_THROUGHPUT_2C; } break; @@ -20617,7 +20617,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins insThroughput = PERFSCORE_THROUGHPUT_1C; insLatency = PERFSCORE_LATENCY_3C; - if (memAccessKind == PERFSCORE_MEMORY_READ) + if (memAccessKind == PerfScoreMemoryAccessKind::Read) { // The reads have twice the throughput of the register to register variants insThroughput = PERFSCORE_THROUGHPUT_2X; @@ -20639,7 +20639,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins insThroughput = PERFSCORE_THROUGHPUT_1C; insLatency = PERFSCORE_LATENCY_1C; - if (memAccessKind == PERFSCORE_MEMORY_READ) + if (memAccessKind == PerfScoreMemoryAccessKind::Read) { // The reads have twice the throughput of the register to register variants insThroughput = PERFSCORE_THROUGHPUT_2X; @@ -20909,7 +20909,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins { insLatency = (opSize >= EA_32BYTE) ? PERFSCORE_LATENCY_3C : PERFSCORE_LATENCY_1C; - if (memAccessKind == PERFSCORE_MEMORY_NONE) + if (memAccessKind == PerfScoreMemoryAccessKind::None) { insThroughput = PERFSCORE_THROUGHPUT_1C; } @@ -20925,7 +20925,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins case INS_pinsrd: case INS_pinsrq: { - if (memAccessKind == PERFSCORE_MEMORY_NONE) + if (memAccessKind == PerfScoreMemoryAccessKind::None) { insThroughput = PERFSCORE_THROUGHPUT_2C; insLatency = PERFSCORE_LATENCY_4C; @@ -21320,7 +21320,7 @@ emitter::insExecutionCharacteristics emitter::getInsExecutionCharacteristics(ins } else { - if (memAccessKind != PERFSCORE_MEMORY_NONE) + if (memAccessKind != PerfScoreMemoryAccessKind::None) { if (IsSimdInstruction(ins)) { From 40365cd876dec6ab7686bf951c659ac52fc05226 Mon Sep 17 00:00:00 2001 From: Milos Kotlar Date: Wed, 29 Apr 2026 11:09:34 +0200 Subject: [PATCH 011/115] [interp] Fix doubled interpreter frames in DAC/DBI stack walks (#126953) ## Description When DAC/DBI seeds StackFrameIterator from a CONTEXT pointing into interpreted code, m_crawl.pFrame is initialized to the thread's top explicit Frame, which is the InterpreterFrame that owns the executing InterpMethodContextFrame chain. ResetRegDisp reads the owning InterpreterFrame* from the CONTEXT's first-arg register and advances m_crawl.pFrame past it before ProcessCurrentFrame runs. ## Tests Fixes the following interpreter debugger test failures: - `StackWalking.NestedException` - `StackWalking.ChildParentTest` --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jan Vorlicek --- src/coreclr/vm/stackwalk.cpp | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/coreclr/vm/stackwalk.cpp b/src/coreclr/vm/stackwalk.cpp index 1aae7049d026b7..00f2123f005fe0 100644 --- a/src/coreclr/vm/stackwalk.cpp +++ b/src/coreclr/vm/stackwalk.cpp @@ -70,7 +70,7 @@ PTR_VOID ConvertStackMarkToPointerOnOSStack(PTR_Thread pThread, PTR_VOID stackMa } pCurrent = pCurrent->pParent; } while (pCurrent != NULL); - + } pFrame = pFrame->PtrNextFrame(); @@ -1087,6 +1087,9 @@ BOOL StackFrameIterator::Init(Thread * pThread, // process the REGDISPLAY and stop at the first frame ProcessIp(GetControlPC(m_crawl.pRD)); +#ifdef FEATURE_INTERPRETER + _ASSERTE(!m_crawl.codeInfo.IsInterpretedCode()); +#endif // FEATURE_INTERPRETER if (m_crawl.isFrameless && !!(m_crawl.pRD->pCurrentContext->ContextFlags & CONTEXT_EXCEPTION_ACTIVE)) { m_crawl.hasFaulted = true; @@ -1160,6 +1163,21 @@ BOOL StackFrameIterator::ResetRegDisp(PREGDISPLAY pRegDisp, PCODE curPc = GetControlPC(pRegDisp); ProcessIp(curPc); +#ifdef FEATURE_INTERPRETER + if (m_crawl.codeInfo.IsInterpretedCode()) + { + // The CONTEXT carries the owning InterpreterFrame in the first-arg register + // (set by InterpreterFrame::SetContextToInterpMethodContextFrame). Advance + // m_crawl.pFrame past it so the iterator does not re-enter the same + // InterpMethodContextFrame chain via the explicit frame link. + PTR_InterpreterFrame pOwningInterpFrame = + dac_cast((TADDR)GetFirstArgReg(m_crawl.pRD->pCurrentContext)); + _ASSERTE(pOwningInterpFrame != NULL); + _ASSERTE(pOwningInterpFrame->GetFrameIdentifier() == FrameIdentifier::InterpreterFrame); + m_crawl.pFrame = pOwningInterpFrame->PtrNextFrame(); + } + else +#endif // FEATURE_INTERPRETER // loop the frame chain to find the closet explicit frame which is lower than the specified REGDISPLAY // (stack grows up towards lower address) if (m_crawl.pFrame != FRAME_TOP) From b01c08cba96ccf093952774a21a1f34f08852370 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:18:00 +0100 Subject: [PATCH 012/115] Don't get last write time of empty list of files (#127513) Without this condition, fresh test runs fail because there are no 'done' files indicating r2r compilation already completed --- src/tests/Common/tests.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/Common/tests.targets b/src/tests/Common/tests.targets index 335728c416ecb5..0b45d2048d1d78 100644 --- a/src/tests/Common/tests.targets +++ b/src/tests/Common/tests.targets @@ -220,7 +220,7 @@ <_Crossgen2InCoreRootTime>$([System.IO.File]::GetLastWriteTime('$(_Crossgen2DstPath)').Ticks) - + <_StaleIlCg2DoneFile Include="@(_IlCg2DoneFiles)" Condition="$([System.IO.File]::GetLastWriteTime('%(Identity)').Ticks) < $(_Crossgen2InCoreRootTime)" /> From 6df7b63c607b6ac9e100831c07349d907e78bbd5 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 29 Apr 2026 13:04:03 +0300 Subject: [PATCH 013/115] Reinstate LINQ join convenience overloads and fix the build break (#126649) > [!NOTE] > This PR description was generated with Copilot. Reinstates the LINQ convenience overloads from #121998 and #121999 that were reverted in #126624, while also fixing the build break that caused the revert. ## Summary - preserves the original API/implementation commits by cherry-picking them onto this branch - fixes the `System.Linq.AsyncEnumerable` test build break by making the affected async selectorless overload calls explicit where inference was insufficient during the multi-target build ## Validation - `build.cmd clr+libs -rc release` - `.\dotnet.cmd build .\src\libraries\System.Linq.AsyncEnumerable\tests\System.Linq.AsyncEnumerable.Tests.csproj /t:Test --no-restore` - `.\dotnet.cmd build .\src\libraries\System.Linq\tests\System.Linq.Tests.csproj /t:Test --no-restore` - `.\dotnet.cmd build .\src\libraries\System.Linq.Queryable\tests\System.Linq.Queryable.Tests.csproj /t:Test --no-restore` --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Shay Rojansky Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: roji <1862641+roji@users.noreply.github.com> --- .../ref/System.Linq.AsyncEnumerable.cs | 8 + .../src/System/Linq/GroupJoin.cs | 133 +++++++++++++ .../src/System/Linq/Join.cs | 135 ++++++++++++++ .../src/System/Linq/LeftJoin.cs | 131 +++++++++++++ .../src/System/Linq/RightJoin.cs | 132 +++++++++++++ .../tests/GroupJoinTests.cs | 79 ++++++++ .../tests/JoinTests.cs | 78 ++++++++ .../tests/LeftJoinTests.cs | 80 ++++++++ .../tests/RightJoinTests.cs | 80 ++++++++ .../ref/System.Linq.Queryable.cs | 4 + .../src/System/Linq/Queryable.cs | 116 ++++++++++++ .../tests/GroupJoinTests.cs | 62 +++++++ .../System.Linq.Queryable/tests/JoinTests.cs | 90 +++++++++ .../tests/LeftJoinTests.cs | 90 +++++++++ .../tests/RightJoinTests.cs | 90 +++++++++ src/libraries/System.Linq/ref/System.Linq.cs | 4 + .../System.Linq/src/System/Linq/GroupJoin.cs | 81 ++++++++ .../System.Linq/src/System/Linq/Join.cs | 78 ++++++++ .../System.Linq/src/System/Linq/LeftJoin.cs | 80 ++++++++ .../System.Linq/src/System/Linq/RightJoin.cs | 80 ++++++++ .../System.Linq/tests/GroupJoinTests.cs | 174 ++++++++++++++++++ src/libraries/System.Linq/tests/JoinTests.cs | 104 +++++++++++ .../System.Linq/tests/LeftJoinTests.cs | 94 ++++++++++ .../System.Linq/tests/RightJoinTests.cs | 95 ++++++++++ 24 files changed, 2098 insertions(+) diff --git a/src/libraries/System.Linq.AsyncEnumerable/ref/System.Linq.AsyncEnumerable.cs b/src/libraries/System.Linq.AsyncEnumerable/ref/System.Linq.AsyncEnumerable.cs index 881b679a19f3ae..b13c0504a02459 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/ref/System.Linq.AsyncEnumerable.cs +++ b/src/libraries/System.Linq.AsyncEnumerable/ref/System.Linq.AsyncEnumerable.cs @@ -73,6 +73,8 @@ public static partial class AsyncEnumerable public static System.Collections.Generic.IAsyncEnumerable GroupBy(this System.Collections.Generic.IAsyncEnumerable source, System.Func keySelector, System.Func, TResult> resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable GroupBy(this System.Collections.Generic.IAsyncEnumerable source, System.Func> keySelector, System.Func> elementSelector, System.Func, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask> resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable GroupBy(this System.Collections.Generic.IAsyncEnumerable source, System.Func keySelector, System.Func elementSelector, System.Func, TResult> resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } + public static System.Collections.Generic.IAsyncEnumerable> GroupJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func> outerKeySelector, System.Func> innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } + public static System.Collections.Generic.IAsyncEnumerable> GroupJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable GroupJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func> outerKeySelector, System.Func> innerKeySelector, System.Func, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask> resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable GroupJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func, TResult> resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable<(int Index, TSource Item)> Index(this System.Collections.Generic.IAsyncEnumerable source) { throw null; } @@ -81,6 +83,8 @@ public static partial class AsyncEnumerable public static System.Collections.Generic.IAsyncEnumerable Intersect(this System.Collections.Generic.IAsyncEnumerable first, System.Collections.Generic.IAsyncEnumerable second, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable Join(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func> outerKeySelector, System.Func> innerKeySelector, System.Func> resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable Join(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } + public static System.Collections.Generic.IAsyncEnumerable<(TOuter Outer, TInner Inner)> Join(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func> outerKeySelector, System.Func> innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } + public static System.Collections.Generic.IAsyncEnumerable<(TOuter Outer, TInner Inner)> Join(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Threading.Tasks.ValueTask LastAsync(this System.Collections.Generic.IAsyncEnumerable source, System.Func predicate, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Threading.Tasks.ValueTask LastAsync(this System.Collections.Generic.IAsyncEnumerable source, System.Func> predicate, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Threading.Tasks.ValueTask LastAsync(this System.Collections.Generic.IAsyncEnumerable source, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } @@ -92,6 +96,8 @@ public static partial class AsyncEnumerable public static System.Threading.Tasks.ValueTask LastOrDefaultAsync(this System.Collections.Generic.IAsyncEnumerable source, TSource defaultValue, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Collections.Generic.IAsyncEnumerable LeftJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func> outerKeySelector, System.Func> innerKeySelector, System.Func> resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable LeftJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } + public static System.Collections.Generic.IAsyncEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func> outerKeySelector, System.Func> innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } + public static System.Collections.Generic.IAsyncEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Threading.Tasks.ValueTask LongCountAsync(this System.Collections.Generic.IAsyncEnumerable source, System.Func predicate, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Threading.Tasks.ValueTask LongCountAsync(this System.Collections.Generic.IAsyncEnumerable source, System.Func> predicate, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Threading.Tasks.ValueTask LongCountAsync(this System.Collections.Generic.IAsyncEnumerable source, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } @@ -114,6 +120,8 @@ public static partial class AsyncEnumerable public static System.Collections.Generic.IAsyncEnumerable Reverse(this System.Collections.Generic.IAsyncEnumerable source) { throw null; } public static System.Collections.Generic.IAsyncEnumerable RightJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func> outerKeySelector, System.Func> innerKeySelector, System.Func> resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable RightJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } + public static System.Collections.Generic.IAsyncEnumerable<(TOuter? Outer, TInner Inner)> RightJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func> outerKeySelector, System.Func> innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } + public static System.Collections.Generic.IAsyncEnumerable<(TOuter? Outer, TInner Inner)> RightJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable SelectMany(this System.Collections.Generic.IAsyncEnumerable source, System.Func> selector) { throw null; } public static System.Collections.Generic.IAsyncEnumerable SelectMany(this System.Collections.Generic.IAsyncEnumerable source, System.Func> selector) { throw null; } public static System.Collections.Generic.IAsyncEnumerable SelectMany(this System.Collections.Generic.IAsyncEnumerable source, System.Func> selector) { throw null; } diff --git a/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/GroupJoin.cs b/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/GroupJoin.cs index 3673aa85956f52..f00866f850bfbe 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/GroupJoin.cs +++ b/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/GroupJoin.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; @@ -10,6 +11,120 @@ namespace System.Linq { public static partial class AsyncEnumerable { + /// Correlates the elements of two sequences based on key equality and groups the results. If is or omitted, the default equality comparer is used to compare keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to hash and compare keys, or to use . + /// + /// An that contains elements of type + /// where each grouping contains the outer element as the key and the matching inner elements. + /// + /// is . + /// is . + /// is . + /// is . + public static IAsyncEnumerable> GroupJoin( + this IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func outerKeySelector, + Func innerKeySelector, + IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return + outer.IsKnownEmpty() ? Empty>() : + Impl(outer, inner, outerKeySelector, innerKeySelector, comparer, default); + + static async IAsyncEnumerable> Impl( + IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func outerKeySelector, + Func innerKeySelector, + IEqualityComparer? comparer, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await using IAsyncEnumerator e = outer.GetAsyncEnumerator(cancellationToken); + + if (await e.MoveNextAsync()) + { + AsyncLookup lookup = await AsyncLookup.CreateForJoinAsync(inner, innerKeySelector, comparer, cancellationToken); + do + { + TOuter item = e.Current; + yield return new AsyncGroupJoinGrouping(item, lookup[outerKeySelector(item)]); + } + while (await e.MoveNextAsync()); + } + } + } + + /// Correlates the elements of two sequences based on key equality and groups the results. If is or omitted, the default equality comparer is used to compare keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to hash and compare keys, or to use . + /// + /// An that contains elements of type + /// where each grouping contains the outer element as the key and the matching inner elements. + /// + /// is . + /// is . + /// is . + /// is . + public static IAsyncEnumerable> GroupJoin( + this IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func> outerKeySelector, + Func> innerKeySelector, + IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return + outer.IsKnownEmpty() ? Empty>() : + Impl(outer, inner, outerKeySelector, innerKeySelector, comparer, default); + + static async IAsyncEnumerable> Impl( + IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func> outerKeySelector, + Func> innerKeySelector, + IEqualityComparer? comparer, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await using IAsyncEnumerator e = outer.GetAsyncEnumerator(cancellationToken); + + if (await e.MoveNextAsync()) + { + AsyncLookup lookup = await AsyncLookup.CreateForJoinAsync(inner, innerKeySelector, comparer, cancellationToken); + do + { + TOuter item = e.Current; + yield return new AsyncGroupJoinGrouping( + item, + lookup[await outerKeySelector(item, cancellationToken)]); + } + while (await e.MoveNextAsync()); + } + } + } + /// Correlates the elements of two sequences based on key equality and groups the results. /// /// @@ -143,4 +258,22 @@ lookup[await outerKeySelector(item, cancellationToken)], } } } + + internal sealed class AsyncGroupJoinGrouping : IGrouping + { + private readonly TKey _key; + private readonly IEnumerable _elements; + + public AsyncGroupJoinGrouping(TKey key, IEnumerable elements) + { + _key = key; + _elements = elements; + } + + public TKey Key => _key; + + public IEnumerator GetEnumerator() => _elements.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } } diff --git a/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/Join.cs b/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/Join.cs index eb086126386a30..ba67fe31b7392c 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/Join.cs +++ b/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/Join.cs @@ -156,5 +156,140 @@ static async IAsyncEnumerable Impl( } } } + + /// Correlates the elements of two sequences based on matching keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to use to hash and compare keys. + /// + /// An that has elements of type (TOuter Outer, TInner Inner) + /// that are obtained by performing an inner join on two sequences. + /// + /// is . + /// is . + /// is . + /// is . + public static IAsyncEnumerable<(TOuter Outer, TInner Inner)> Join( + this IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func outerKeySelector, + Func innerKeySelector, + IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return + outer.IsKnownEmpty() || inner.IsKnownEmpty() ? Empty<(TOuter Outer, TInner Inner)>() : + Impl(outer, inner, outerKeySelector, innerKeySelector, comparer, default); + + static async IAsyncEnumerable<(TOuter Outer, TInner Inner)> Impl( + IAsyncEnumerable outer, IAsyncEnumerable inner, + Func outerKeySelector, + Func innerKeySelector, + IEqualityComparer? comparer, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await using IAsyncEnumerator e = outer.GetAsyncEnumerator(cancellationToken); + + if (await e.MoveNextAsync()) + { + AsyncLookup lookup = await AsyncLookup.CreateForJoinAsync(inner, innerKeySelector, comparer, cancellationToken); + if (lookup.Count != 0) + { + do + { + TOuter item = e.Current; + Grouping? g = lookup.GetGrouping(outerKeySelector(item), create: false); + if (g is not null) + { + int count = g._count; + TInner[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return (item, elements[i]); + } + } + } + while (await e.MoveNextAsync()); + } + } + } + } + + /// Correlates the elements of two sequences based on matching keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to use to hash and compare keys. + /// + /// An that has elements of type (TOuter Outer, TInner Inner) + /// that are obtained by performing an inner join on two sequences. + /// + /// is . + /// is . + /// is . + /// is . + public static IAsyncEnumerable<(TOuter Outer, TInner Inner)> Join( + this IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func> outerKeySelector, + Func> innerKeySelector, + IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return + outer.IsKnownEmpty() || inner.IsKnownEmpty() ? Empty<(TOuter Outer, TInner Inner)>() : + Impl(outer, inner, outerKeySelector, innerKeySelector, comparer, default); + + static async IAsyncEnumerable<(TOuter Outer, TInner Inner)> Impl( + IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func> outerKeySelector, + Func> innerKeySelector, + IEqualityComparer? comparer, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await using IAsyncEnumerator e = outer.GetAsyncEnumerator(cancellationToken); + + if (await e.MoveNextAsync()) + { + AsyncLookup lookup = await AsyncLookup.CreateForJoinAsync(inner, innerKeySelector, comparer, cancellationToken); + if (lookup.Count != 0) + { + do + { + TOuter item = e.Current; + Grouping? g = lookup.GetGrouping(await outerKeySelector(item, cancellationToken), create: false); + if (g is not null) + { + int count = g._count; + TInner[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return (item, elements[i]); + } + } + } + while (await e.MoveNextAsync()); + } + } + } + } } } diff --git a/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/LeftJoin.cs b/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/LeftJoin.cs index a86645ece88d69..8438bdaac6963f 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/LeftJoin.cs +++ b/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/LeftJoin.cs @@ -152,5 +152,136 @@ static async IAsyncEnumerable Impl( } } } + + /// Correlates the elements of two sequences based on matching keys. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to use to hash and compare keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter Outer, TInner? Inner) that are obtained by performing a left outer join on two sequences. + /// is . + /// is . + /// is . + /// is . + public static IAsyncEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin( + this IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func outerKeySelector, + Func innerKeySelector, + IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return + outer.IsKnownEmpty() ? Empty<(TOuter Outer, TInner? Inner)>() : + Impl(outer, inner, outerKeySelector, innerKeySelector, comparer, default); + + static async IAsyncEnumerable<(TOuter Outer, TInner? Inner)> Impl( + IAsyncEnumerable outer, IAsyncEnumerable inner, + Func outerKeySelector, + Func innerKeySelector, + IEqualityComparer? comparer, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await using IAsyncEnumerator e = outer.GetAsyncEnumerator(cancellationToken); + + if (await e.MoveNextAsync()) + { + AsyncLookup innerLookup = await AsyncLookup.CreateForJoinAsync(inner, innerKeySelector, comparer, cancellationToken); + do + { + TOuter item = e.Current; + Grouping? g = innerLookup.GetGrouping(outerKeySelector(item), create: false); + if (g is null) + { + yield return (item, default); + } + else + { + int count = g._count; + TInner[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return (item, elements[i]); + } + } + } + while (await e.MoveNextAsync()); + } + } + } + + /// Correlates the elements of two sequences based on matching keys. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to use to hash and compare keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter Outer, TInner? Inner) that are obtained by performing a left outer join on two sequences. + /// is . + /// is . + /// is . + /// is . + public static IAsyncEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin( + this IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func> outerKeySelector, + Func> innerKeySelector, + IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return + outer.IsKnownEmpty() ? Empty<(TOuter Outer, TInner? Inner)>() : + Impl(outer, inner, outerKeySelector, innerKeySelector, comparer, default); + + static async IAsyncEnumerable<(TOuter Outer, TInner? Inner)> Impl( + IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func> outerKeySelector, + Func> innerKeySelector, + IEqualityComparer? comparer, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await using IAsyncEnumerator e = outer.GetAsyncEnumerator(cancellationToken); + + if (await e.MoveNextAsync()) + { + AsyncLookup innerLookup = await AsyncLookup.CreateForJoinAsync(inner, innerKeySelector, comparer, cancellationToken); + do + { + TOuter item = e.Current; + Grouping? g = innerLookup.GetGrouping(await outerKeySelector(item, cancellationToken), create: false); + if (g is null) + { + yield return (item, default); + } + else + { + int count = g._count; + TInner[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return (item, elements[i]); + } + } + } + while (await e.MoveNextAsync()); + } + } + } } } diff --git a/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/RightJoin.cs b/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/RightJoin.cs index 9328acb4597abc..6e83ddccad533c 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/RightJoin.cs +++ b/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/RightJoin.cs @@ -153,5 +153,137 @@ static async IAsyncEnumerable Impl( } } } + + /// Correlates the elements of two sequences based on matching keys. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to use to hash and compare keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter? Outer, TInner Inner) that are obtained by performing a right outer join on two sequences. + /// is . + /// is . + /// is . + /// is . + public static IAsyncEnumerable<(TOuter? Outer, TInner Inner)> RightJoin( + this IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func outerKeySelector, + Func innerKeySelector, + IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return + inner.IsKnownEmpty() ? Empty<(TOuter? Outer, TInner Inner)>() : + Impl(outer, inner, outerKeySelector, innerKeySelector, comparer, default); + + static async IAsyncEnumerable<(TOuter? Outer, TInner Inner)> Impl( + IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func outerKeySelector, + Func innerKeySelector, + IEqualityComparer? comparer, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await using IAsyncEnumerator e = inner.GetAsyncEnumerator(cancellationToken); + + if (await e.MoveNextAsync()) + { + AsyncLookup outerLookup = await AsyncLookup.CreateForJoinAsync(outer, outerKeySelector, comparer, cancellationToken); + do + { + TInner item = e.Current; + Grouping? g = outerLookup.GetGrouping(innerKeySelector(item), create: false); + if (g is null) + { + yield return (default, item); + } + else + { + int count = g._count; + TOuter[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return (elements[i], item); + } + } + } + while (await e.MoveNextAsync()); + } + } + } + + /// Correlates the elements of two sequences based on matching keys. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to use to hash and compare keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter? Outer, TInner Inner) that are obtained by performing a right outer join on two sequences. + /// is . + /// is . + /// is . + /// is . + public static IAsyncEnumerable<(TOuter? Outer, TInner Inner)> RightJoin( + this IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func> outerKeySelector, + Func> innerKeySelector, + IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return + inner.IsKnownEmpty() ? Empty<(TOuter? Outer, TInner Inner)>() : + Impl(outer, inner, outerKeySelector, innerKeySelector, comparer, default); + + static async IAsyncEnumerable<(TOuter? Outer, TInner Inner)> Impl( + IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func> outerKeySelector, + Func> innerKeySelector, + IEqualityComparer? comparer, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await using IAsyncEnumerator e = inner.GetAsyncEnumerator(cancellationToken); + + if (await e.MoveNextAsync()) + { + AsyncLookup outerLookup = await AsyncLookup.CreateForJoinAsync(outer, outerKeySelector, comparer, cancellationToken); + do + { + TInner item = e.Current; + Grouping? g = outerLookup.GetGrouping(await innerKeySelector(item, cancellationToken), create: false); + if (g is null) + { + yield return (default, item); + } + else + { + int count = g._count; + TOuter[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return (elements[i], item); + } + } + } + while (await e.MoveNextAsync()); + } + } + } } } diff --git a/src/libraries/System.Linq.AsyncEnumerable/tests/GroupJoinTests.cs b/src/libraries/System.Linq.AsyncEnumerable/tests/GroupJoinTests.cs index 305faf3c77681f..c09a68c9482822 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/tests/GroupJoinTests.cs +++ b/src/libraries/System.Linq.AsyncEnumerable/tests/GroupJoinTests.cs @@ -26,6 +26,20 @@ public void InvalidInputs_Throws() AssertExtensions.Throws("resultSelector", () => AsyncEnumerable.GroupJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), async (outer, ct) => outer, async (inner, ct) => inner, (Func, CancellationToken, ValueTask>)null)); } + [Fact] + public void InvalidInputs_WithoutResultSelector_Throws() + { + AssertExtensions.Throws("outer", () => AsyncEnumerable.GroupJoin((IAsyncEnumerable)null, AsyncEnumerable.Empty(), outer => outer, inner => inner)); + AssertExtensions.Throws("inner", () => AsyncEnumerable.GroupJoin(AsyncEnumerable.Empty(), (IAsyncEnumerable)null, outer => outer, inner => inner)); + AssertExtensions.Throws("outerKeySelector", () => AsyncEnumerable.GroupJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), (Func)null, inner => inner)); + AssertExtensions.Throws("innerKeySelector", () => AsyncEnumerable.GroupJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), outer => outer, (Func)null)); + + AssertExtensions.Throws("outer", () => AsyncEnumerable.GroupJoin((IAsyncEnumerable)null, AsyncEnumerable.Empty(), async (outer, ct) => outer, async (inner, ct) => inner)); + AssertExtensions.Throws("inner", () => AsyncEnumerable.GroupJoin(AsyncEnumerable.Empty(), (IAsyncEnumerable)null, async (outer, ct) => outer, async (inner, ct) => inner)); + AssertExtensions.Throws("outerKeySelector", () => AsyncEnumerable.GroupJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), (Func>)null, async (inner, ct) => inner)); + AssertExtensions.Throws("innerKeySelector", () => AsyncEnumerable.GroupJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), async (outer, ct) => outer, (Func>)null)); + } + [Fact] public void Empty_ProducesEmpty() // validating an optimization / implementation detail { @@ -33,6 +47,13 @@ public void Empty_ProducesEmpty() // validating an optimization / implementation Assert.Same(AsyncEnumerable.Empty(), AsyncEnumerable.Empty().GroupJoin(CreateSource(1, 2, 3), async (s, ct) => s, async (i, ct) => i.ToString(), async (s, e, ct) => s)); } + [Fact] + public void Empty_WithoutResultSelector_ProducesEmpty() + { + Assert.Same(AsyncEnumerable.Empty>(), AsyncEnumerable.Empty().GroupJoin(CreateSource(1, 2, 3), s => s, i => i.ToString())); + Assert.Same(AsyncEnumerable.Empty>(), AsyncEnumerable.Empty().GroupJoin(CreateSource(1, 2, 3), async (s, ct) => s, async (i, ct) => i.ToString())); + } + [Fact] public async Task VariousValues_MatchesEnumerable_String() { @@ -55,6 +76,38 @@ await AssertEqual( } } + [Fact] + public async Task VariousValues_WithoutResultSelector_MatchesEnumerable() + { + int[] outer = [1, 2, 3]; + int[] inner = [1, 2, 2, 3, 3, 3]; + + foreach (IAsyncEnumerable outerSource in CreateSources(outer)) + foreach (IAsyncEnumerable innerSource in CreateSources(inner)) + { +#if NET + var expected = outer.GroupJoin(inner, o => o, i => i); + var result = await outerSource.GroupJoin(innerSource, o => o, i => i).ToListAsync(); + + Assert.Equal(expected.Count(), result.Count); + foreach (var (exp, act) in expected.Zip(result)) + { + Assert.Equal(exp.Key, act.Key); + Assert.Equal(exp.ToList(), act.ToList()); + } + + var resultAsync = await outerSource.GroupJoin(innerSource, async (o, ct) => o, async (i, ct) => i).ToListAsync(); + + Assert.Equal(expected.Count(), resultAsync.Count); + foreach (var (exp, act) in expected.Zip(resultAsync)) + { + Assert.Equal(exp.Key, act.Key); + Assert.Equal(exp.ToList(), act.ToList()); + } +#endif + } + } + [Fact] public async Task Cancellation_Cancels() { @@ -167,5 +220,31 @@ public async Task InterfaceCalls_ExpectedCounts() Assert.Equal(4, inner.CurrentCount); Assert.Equal(1, inner.DisposeAsyncCount); } + + [Fact] + public async Task InterfaceCalls_WithoutResultSelector_ExpectedCounts() + { + TrackingAsyncEnumerable outer, inner; + + outer = CreateSource(2, 4, 8, 16).Track(); + inner = CreateSource(1, 2, 3, 4).Track(); + await ConsumeAsync(outer.GroupJoin(inner, outer => outer, inner => inner)); + Assert.Equal(5, outer.MoveNextAsyncCount); + Assert.Equal(4, outer.CurrentCount); + Assert.Equal(1, outer.DisposeAsyncCount); + Assert.Equal(5, inner.MoveNextAsyncCount); + Assert.Equal(4, inner.CurrentCount); + Assert.Equal(1, inner.DisposeAsyncCount); + + outer = CreateSource(2, 4, 8, 16).Track(); + inner = CreateSource(1, 2, 3, 4).Track(); + await ConsumeAsync(outer.GroupJoin(inner, async (outer, ct) => outer, async (inner, ct) => inner)); + Assert.Equal(5, outer.MoveNextAsyncCount); + Assert.Equal(4, outer.CurrentCount); + Assert.Equal(1, outer.DisposeAsyncCount); + Assert.Equal(5, inner.MoveNextAsyncCount); + Assert.Equal(4, inner.CurrentCount); + Assert.Equal(1, inner.DisposeAsyncCount); + } } } diff --git a/src/libraries/System.Linq.AsyncEnumerable/tests/JoinTests.cs b/src/libraries/System.Linq.AsyncEnumerable/tests/JoinTests.cs index 258ea44c57ba49..a9f2598c6b9c3e 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/tests/JoinTests.cs +++ b/src/libraries/System.Linq.AsyncEnumerable/tests/JoinTests.cs @@ -176,5 +176,83 @@ public async Task InterfaceCalls_ExpectedCounts() Assert.Equal(4, inner.CurrentCount); Assert.Equal(1, inner.DisposeAsyncCount); } + + [Fact] + public void TupleJoin_InvalidInputs_Throws() + { + AssertExtensions.Throws("outer", () => AsyncEnumerable.Join((IAsyncEnumerable)null, AsyncEnumerable.Empty(), outer => outer, inner => inner)); + AssertExtensions.Throws("inner", () => AsyncEnumerable.Join(AsyncEnumerable.Empty(), (IAsyncEnumerable)null, outer => outer, inner => inner)); + AssertExtensions.Throws("outerKeySelector", () => AsyncEnumerable.Join(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), (Func)null, inner => inner)); + AssertExtensions.Throws("innerKeySelector", () => AsyncEnumerable.Join(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), outer => outer, (Func)null)); + + AssertExtensions.Throws("outer", () => AsyncEnumerable.Join((IAsyncEnumerable)null, AsyncEnumerable.Empty(), async (outer, ct) => outer, async (inner, ct) => inner)); + AssertExtensions.Throws("inner", () => AsyncEnumerable.Join(AsyncEnumerable.Empty(), (IAsyncEnumerable)null, async (outer, ct) => outer, async (inner, ct) => inner)); + AssertExtensions.Throws("outerKeySelector", () => AsyncEnumerable.Join(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), (Func>)null, async (inner, ct) => inner)); + AssertExtensions.Throws("innerKeySelector", () => AsyncEnumerable.Join(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), async (outer, ct) => outer, (Func>)null)); + } + + [Fact] + public void TupleJoin_Empty_ProducesEmpty() + { + IAsyncEnumerable empty = AsyncEnumerable.Empty(); + IAsyncEnumerable nonEmpty = CreateSource("1", "2", "3"); + + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), empty.Join(empty, s => s, s => s)); + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), empty.Join(empty, async (s, ct) => s, async (s, ct) => s)); + + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), nonEmpty.Join(empty, s => s, s => s)); + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), nonEmpty.Join(empty, async (s, ct) => s, async (s, ct) => s)); + + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), empty.Join(nonEmpty, s => s, s => s)); + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), empty.Join(nonEmpty, async (s, ct) => s, async (s, ct) => s)); + } + + [Fact] + public async Task TupleJoin_VariousValues_MatchesEnumerable_String() + { + Random rand = new(42); + foreach (int length in new[] { 0, 1, 2, 1000 }) + { + string[] values = new string[length]; + FillRandom(rand, values); + + foreach (IAsyncEnumerable source in CreateSources(values)) + { + await AssertEqual( + values.Join(values, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ', resultSelector: (outer, inner) => (Outer: outer, Inner: inner)).Select(t => t.Outer + t.Inner), + source.Join(source, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner)); + + await AssertEqual( + values.Join(values, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ', resultSelector: (outer, inner) => (Outer: outer, Inner: inner)).Select(t => t.Outer + t.Inner), + source.Join(source, async (s, ct) => s.Length > 0 ? s[0] : ' ', async (s, ct) => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner)); + } + } + } + + [Fact] + public async Task TupleJoin_InterfaceCalls_ExpectedCounts() + { + TrackingAsyncEnumerable outer, inner; + + outer = CreateSource(2, 4, 8, 16).Track(); + inner = CreateSource(1, 2, 3, 4).Track(); + await ConsumeAsync(outer.Join(inner, outer => outer, inner => inner)); + Assert.Equal(5, outer.MoveNextAsyncCount); + Assert.Equal(4, outer.CurrentCount); + Assert.Equal(1, outer.DisposeAsyncCount); + Assert.Equal(5, inner.MoveNextAsyncCount); + Assert.Equal(4, inner.CurrentCount); + Assert.Equal(1, inner.DisposeAsyncCount); + + outer = CreateSource(2, 4, 8, 16).Track(); + inner = CreateSource(1, 2, 3, 4).Track(); + await ConsumeAsync(outer.Join(inner, async (outer, ct) => outer, async (inner, ct) => inner)); + Assert.Equal(5, outer.MoveNextAsyncCount); + Assert.Equal(4, outer.CurrentCount); + Assert.Equal(1, outer.DisposeAsyncCount); + Assert.Equal(5, inner.MoveNextAsyncCount); + Assert.Equal(4, inner.CurrentCount); + Assert.Equal(1, inner.DisposeAsyncCount); + } } } diff --git a/src/libraries/System.Linq.AsyncEnumerable/tests/LeftJoinTests.cs b/src/libraries/System.Linq.AsyncEnumerable/tests/LeftJoinTests.cs index 759ae3ab1ed1d8..917371e9ebe958 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/tests/LeftJoinTests.cs +++ b/src/libraries/System.Linq.AsyncEnumerable/tests/LeftJoinTests.cs @@ -178,5 +178,85 @@ public async Task InterfaceCalls_ExpectedCounts() Assert.Equal(4, inner.CurrentCount); Assert.Equal(1, inner.DisposeAsyncCount); } + + [Fact] + public void TupleLeftJoin_InvalidInputs_Throws() + { + AssertExtensions.Throws("outer", () => AsyncEnumerable.LeftJoin((IAsyncEnumerable)null, AsyncEnumerable.Empty(), outer => outer, inner => inner)); + AssertExtensions.Throws("inner", () => AsyncEnumerable.LeftJoin(AsyncEnumerable.Empty(), (IAsyncEnumerable)null, outer => outer, inner => inner)); + AssertExtensions.Throws("outerKeySelector", () => AsyncEnumerable.LeftJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), (Func)null, inner => inner)); + AssertExtensions.Throws("innerKeySelector", () => AsyncEnumerable.LeftJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), outer => outer, (Func)null)); + + AssertExtensions.Throws("outer", () => AsyncEnumerable.LeftJoin((IAsyncEnumerable)null, AsyncEnumerable.Empty(), async (outer, ct) => outer, async (inner, ct) => inner)); + AssertExtensions.Throws("inner", () => AsyncEnumerable.LeftJoin(AsyncEnumerable.Empty(), (IAsyncEnumerable)null, async (outer, ct) => outer, async (inner, ct) => inner)); + AssertExtensions.Throws("outerKeySelector", () => AsyncEnumerable.LeftJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), (Func>)null, async (inner, ct) => inner)); + AssertExtensions.Throws("innerKeySelector", () => AsyncEnumerable.LeftJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), async (outer, ct) => outer, (Func>)null)); + } + + [Fact] + public void TupleLeftJoin_Empty_ProducesEmpty() + { + IAsyncEnumerable empty = AsyncEnumerable.Empty(); + IAsyncEnumerable nonEmpty = CreateSource("1", "2", "3"); + + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), empty.LeftJoin(empty, s => s, s => s)); + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), empty.LeftJoin(empty, async (s, ct) => s, async (s, ct) => s)); + + Assert.NotSame(AsyncEnumerable.Empty<(string, string)>(), nonEmpty.LeftJoin(empty, s => s, s => s)); + Assert.NotSame(AsyncEnumerable.Empty<(string, string)>(), nonEmpty.LeftJoin(empty, async (s, ct) => s, async (s, ct) => s)); + + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), empty.LeftJoin(nonEmpty, s => s, s => s)); + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), empty.LeftJoin(nonEmpty, async (s, ct) => s, async (s, ct) => s)); + } + +#if NET + [Fact] + public async Task TupleLeftJoin_VariousValues_MatchesEnumerable_String() + { + Random rand = new(42); + foreach (int length in new[] { 0, 1, 2, 1000 }) + { + string[] values = new string[length]; + FillRandom(rand, values); + + foreach (IAsyncEnumerable source in CreateSources(values)) + { + await AssertEqual( + values.LeftJoin(values, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner), + source.LeftJoin(source, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner)); + + await AssertEqual( + values.LeftJoin(values, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner), + source.LeftJoin(source, async (s, ct) => s.Length > 0 ? s[0] : ' ', async (s, ct) => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner)); + } + } + } +#endif + + [Fact] + public async Task TupleLeftJoin_InterfaceCalls_ExpectedCounts() + { + TrackingAsyncEnumerable outer, inner; + + outer = CreateSource(2, 4, 8, 16).Track(); + inner = CreateSource(1, 2, 3, 4).Track(); + await ConsumeAsync(outer.LeftJoin(inner, outer => outer, inner => inner)); + Assert.Equal(5, outer.MoveNextAsyncCount); + Assert.Equal(4, outer.CurrentCount); + Assert.Equal(1, outer.DisposeAsyncCount); + Assert.Equal(5, inner.MoveNextAsyncCount); + Assert.Equal(4, inner.CurrentCount); + Assert.Equal(1, inner.DisposeAsyncCount); + + outer = CreateSource(2, 4, 8, 16).Track(); + inner = CreateSource(1, 2, 3, 4).Track(); + await ConsumeAsync(outer.LeftJoin(inner, async (outer, ct) => outer, async (inner, ct) => inner)); + Assert.Equal(5, outer.MoveNextAsyncCount); + Assert.Equal(4, outer.CurrentCount); + Assert.Equal(1, outer.DisposeAsyncCount); + Assert.Equal(5, inner.MoveNextAsyncCount); + Assert.Equal(4, inner.CurrentCount); + Assert.Equal(1, inner.DisposeAsyncCount); + } } } diff --git a/src/libraries/System.Linq.AsyncEnumerable/tests/RightJoinTests.cs b/src/libraries/System.Linq.AsyncEnumerable/tests/RightJoinTests.cs index e7f123a464e463..de91b6201c4403 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/tests/RightJoinTests.cs +++ b/src/libraries/System.Linq.AsyncEnumerable/tests/RightJoinTests.cs @@ -178,5 +178,85 @@ public async Task InterfaceCalls_ExpectedCounts() Assert.Equal(4, inner.CurrentCount); Assert.Equal(1, inner.DisposeAsyncCount); } + + [Fact] + public void TupleRightJoin_InvalidInputs_Throws() + { + AssertExtensions.Throws("outer", () => AsyncEnumerable.RightJoin((IAsyncEnumerable)null, AsyncEnumerable.Empty(), outer => outer, inner => inner)); + AssertExtensions.Throws("inner", () => AsyncEnumerable.RightJoin(AsyncEnumerable.Empty(), (IAsyncEnumerable)null, outer => outer, inner => inner)); + AssertExtensions.Throws("outerKeySelector", () => AsyncEnumerable.RightJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), (Func)null, inner => inner)); + AssertExtensions.Throws("innerKeySelector", () => AsyncEnumerable.RightJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), outer => outer, (Func)null)); + + AssertExtensions.Throws("outer", () => AsyncEnumerable.RightJoin((IAsyncEnumerable)null, AsyncEnumerable.Empty(), async (outer, ct) => outer, async (inner, ct) => inner)); + AssertExtensions.Throws("inner", () => AsyncEnumerable.RightJoin(AsyncEnumerable.Empty(), (IAsyncEnumerable)null, async (outer, ct) => outer, async (inner, ct) => inner)); + AssertExtensions.Throws("outerKeySelector", () => AsyncEnumerable.RightJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), (Func>)null, async (inner, ct) => inner)); + AssertExtensions.Throws("innerKeySelector", () => AsyncEnumerable.RightJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), async (outer, ct) => outer, (Func>)null)); + } + + [Fact] + public void TupleRightJoin_Empty_ProducesEmpty() + { + IAsyncEnumerable empty = AsyncEnumerable.Empty(); + IAsyncEnumerable nonEmpty = CreateSource("1", "2", "3"); + + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), empty.RightJoin(empty, s => s, s => s)); + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), empty.RightJoin(empty, async (s, ct) => s, async (s, ct) => s)); + + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), nonEmpty.RightJoin(empty, s => s, s => s)); + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), nonEmpty.RightJoin(empty, async (s, ct) => s, async (s, ct) => s)); + + Assert.NotSame(AsyncEnumerable.Empty<(string, string)>(), empty.RightJoin(nonEmpty, s => s, s => s)); + Assert.NotSame(AsyncEnumerable.Empty<(string, string)>(), empty.RightJoin(nonEmpty, async (s, ct) => s, async (s, ct) => s)); + } + +#if NET + [Fact] + public async Task TupleRightJoin_VariousValues_MatchesEnumerable_String() + { + Random rand = new(42); + foreach (int length in new[] { 0, 1, 2, 1000 }) + { + string[] values = new string[length]; + FillRandom(rand, values); + + foreach (IAsyncEnumerable source in CreateSources(values)) + { + await AssertEqual( + values.RightJoin(values, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner), + source.RightJoin(source, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner)); + + await AssertEqual( + values.RightJoin(values, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner), + source.RightJoin(source, async (s, ct) => s.Length > 0 ? s[0] : ' ', async (s, ct) => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner)); + } + } + } +#endif + + [Fact] + public async Task TupleRightJoin_InterfaceCalls_ExpectedCounts() + { + TrackingAsyncEnumerable outer, inner; + + outer = CreateSource(2, 4, 8, 16).Track(); + inner = CreateSource(1, 2, 3, 4).Track(); + await ConsumeAsync(outer.RightJoin(inner, outer => outer, inner => inner)); + Assert.Equal(5, outer.MoveNextAsyncCount); + Assert.Equal(4, outer.CurrentCount); + Assert.Equal(1, outer.DisposeAsyncCount); + Assert.Equal(5, inner.MoveNextAsyncCount); + Assert.Equal(4, inner.CurrentCount); + Assert.Equal(1, inner.DisposeAsyncCount); + + outer = CreateSource(2, 4, 8, 16).Track(); + inner = CreateSource(1, 2, 3, 4).Track(); + await ConsumeAsync(outer.RightJoin(inner, async (outer, ct) => outer, async (inner, ct) => inner)); + Assert.Equal(5, outer.MoveNextAsyncCount); + Assert.Equal(4, outer.CurrentCount); + Assert.Equal(1, outer.DisposeAsyncCount); + Assert.Equal(5, inner.MoveNextAsyncCount); + Assert.Equal(4, inner.CurrentCount); + Assert.Equal(1, inner.DisposeAsyncCount); + } } } diff --git a/src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs b/src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs index 2d63b858f0bd26..82d85662557874 100644 --- a/src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs +++ b/src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs @@ -108,6 +108,7 @@ public static partial class Queryable public static System.Linq.IQueryable GroupBy(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> keySelector, System.Linq.Expressions.Expression, TResult>> resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } public static System.Linq.IQueryable GroupBy(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> keySelector, System.Linq.Expressions.Expression> elementSelector, System.Linq.Expressions.Expression, TResult>> resultSelector) { throw null; } public static System.Linq.IQueryable GroupBy(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> keySelector, System.Linq.Expressions.Expression> elementSelector, System.Linq.Expressions.Expression, TResult>> resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } + public static System.Linq.IQueryable> GroupJoin(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Linq.IQueryable GroupJoin(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Linq.Expressions.Expression, TResult>> resultSelector) { throw null; } public static System.Linq.IQueryable GroupJoin(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Linq.Expressions.Expression, TResult>> resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } public static System.Linq.IQueryable<(int Index, TSource Item)> Index(this System.Linq.IQueryable source) { throw null; } @@ -117,6 +118,7 @@ public static partial class Queryable public static System.Linq.IQueryable Intersect(this System.Linq.IQueryable source1, System.Collections.Generic.IEnumerable source2, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } public static System.Linq.IQueryable Join(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Linq.Expressions.Expression> resultSelector) { throw null; } public static System.Linq.IQueryable Join(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Linq.Expressions.Expression> resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } + public static System.Linq.IQueryable<(TOuter Outer, TInner Inner)> Join(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static TSource? LastOrDefault(this System.Linq.IQueryable source) { throw null; } public static TSource? LastOrDefault(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> predicate) { throw null; } public static TSource LastOrDefault(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> predicate, TSource defaultValue) { throw null; } @@ -125,6 +127,7 @@ public static partial class Queryable public static TSource Last(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> predicate) { throw null; } public static System.Linq.IQueryable LeftJoin(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Linq.Expressions.Expression> resultSelector) { throw null; } public static System.Linq.IQueryable LeftJoin(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Linq.Expressions.Expression> resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } + public static System.Linq.IQueryable<(TOuter Outer, TInner? Inner)> LeftJoin(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static long LongCount(this System.Linq.IQueryable source) { throw null; } public static long LongCount(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> predicate) { throw null; } public static TSource? MaxBy(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> keySelector) { throw null; } @@ -158,6 +161,7 @@ public static partial class Queryable public static System.Linq.IQueryable Reverse(this System.Linq.IQueryable source) { throw null; } public static System.Linq.IQueryable RightJoin(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Linq.Expressions.Expression> resultSelector) { throw null; } public static System.Linq.IQueryable RightJoin(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Linq.Expressions.Expression> resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } + public static System.Linq.IQueryable<(TOuter? Outer, TInner Inner)> RightJoin(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Linq.IQueryable SelectMany(this System.Linq.IQueryable source, System.Linq.Expressions.Expression>> selector) { throw null; } public static System.Linq.IQueryable SelectMany(this System.Linq.IQueryable source, System.Linq.Expressions.Expression>> selector) { throw null; } public static System.Linq.IQueryable SelectMany(this System.Linq.IQueryable source, System.Linq.Expressions.Expression>> collectionSelector, System.Linq.Expressions.Expression> resultSelector) { throw null; } diff --git a/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs b/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs index 1290b8f6819e23..09243d4c8431ac 100644 --- a/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs +++ b/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs @@ -408,6 +408,66 @@ public static IQueryable Join(this IQuer outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector), Expression.Quote(resultSelector), Expression.Constant(comparer, typeof(IEqualityComparer)))); } + /// Correlates the elements of two sequences based on key equality and groups the results. If is or omitted, the default equality comparer is used to compare keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to hash and compare keys, or to use . + /// + /// An that contains elements of type + /// where each grouping contains the outer element as the key and the matching inner elements. + /// + /// is . + /// is . + /// is . + /// is . + [DynamicDependency("GroupJoin`3", typeof(Enumerable))] + public static IQueryable> GroupJoin(this IQueryable outer, IEnumerable inner, Expression> outerKeySelector, Expression> innerKeySelector, IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return outer.Provider.CreateQuery>( + Expression.Call( + null, + new Func, IEnumerable, Expression>, Expression>, IEqualityComparer, IQueryable>>(GroupJoin).Method, + outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector), Expression.Constant(comparer, typeof(IEqualityComparer)))); + } + + /// + /// Correlates the elements of two sequences based on matching keys. If is or omitted, the default equality comparer is used to compare keys. + /// + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to hash and compare keys, or to use . + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter Outer, TInner Inner) that are obtained by performing an inner join on two sequences. + /// or or or is . + [DynamicDependency("Join`3", typeof(Enumerable))] + public static IQueryable<(TOuter Outer, TInner Inner)> Join(this IQueryable outer, IEnumerable inner, Expression> outerKeySelector, Expression> innerKeySelector, IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return outer.Provider.CreateQuery<(TOuter Outer, TInner Inner)>( + Expression.Call( + null, + new Func, IEnumerable, Expression>, Expression>, IEqualityComparer, IQueryable<(TOuter Outer, TInner Inner)>>(Join).Method, + outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector), Expression.Constant(comparer, typeof(IEqualityComparer)))); + } + [DynamicDependency("GroupJoin`4", typeof(Enumerable))] public static IQueryable GroupJoin(this IQueryable outer, IEnumerable inner, Expression> outerKeySelector, Expression> innerKeySelector, Expression, TResult>> resultSelector) { @@ -669,6 +729,34 @@ public static IQueryable LeftJoin(this I outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector), Expression.Quote(resultSelector), Expression.Constant(comparer, typeof(IEqualityComparer)))); } + /// + /// Correlates the elements of two sequences based on matching keys. If is or omitted, the default equality comparer is used to compare keys. + /// + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to hash and compare keys, or to use . + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter Outer, TInner? Inner) that are obtained by performing a left outer join on two sequences. + /// or or or is . + [DynamicDependency("LeftJoin`3", typeof(Enumerable))] + public static IQueryable<(TOuter Outer, TInner? Inner)> LeftJoin(this IQueryable outer, IEnumerable inner, Expression> outerKeySelector, Expression> innerKeySelector, IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return outer.Provider.CreateQuery<(TOuter Outer, TInner? Inner)>( + Expression.Call( + null, + new Func, IEnumerable, Expression>, Expression>, IEqualityComparer, IQueryable<(TOuter Outer, TInner? Inner)>>(LeftJoin).Method, + outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector), Expression.Constant(comparer, typeof(IEqualityComparer)))); + } + /// /// Sorts the elements of a sequence in ascending order. /// @@ -1094,6 +1182,34 @@ public static IQueryable RightJoin(this outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector), Expression.Quote(resultSelector), Expression.Constant(comparer, typeof(IEqualityComparer)))); } + /// + /// Correlates the elements of two sequences based on matching keys. If is or omitted, the default equality comparer is used to compare keys. + /// + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to hash and compare keys, or to use . + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter? Outer, TInner Inner) that are obtained by performing a right outer join on two sequences. + /// or or or is . + [DynamicDependency("RightJoin`3", typeof(Enumerable))] + public static IQueryable<(TOuter? Outer, TInner Inner)> RightJoin(this IQueryable outer, IEnumerable inner, Expression> outerKeySelector, Expression> innerKeySelector, IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return outer.Provider.CreateQuery<(TOuter? Outer, TInner Inner)>( + Expression.Call( + null, + new Func, IEnumerable, Expression>, Expression>, IEqualityComparer, IQueryable<(TOuter? Outer, TInner Inner)>>(RightJoin).Method, + outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector), Expression.Constant(comparer, typeof(IEqualityComparer)))); + } + [DynamicDependency("ThenBy`2", typeof(Enumerable))] public static IOrderedQueryable ThenBy(this IOrderedQueryable source, Expression> keySelector) { diff --git a/src/libraries/System.Linq.Queryable/tests/GroupJoinTests.cs b/src/libraries/System.Linq.Queryable/tests/GroupJoinTests.cs index 3db5cb8cad203e..8d79f7e1c17919 100644 --- a/src/libraries/System.Linq.Queryable/tests/GroupJoinTests.cs +++ b/src/libraries/System.Linq.Queryable/tests/GroupJoinTests.cs @@ -287,5 +287,67 @@ public void GroupJoin2() var count = new[] { 0, 1, 2 }.AsQueryable().GroupJoin(new[] { 1, 2, 3 }, n1 => n1, n2 => n2, (n1, n2) => n1, EqualityComparer.Default).Count(); Assert.Equal(3, count); } + + [Fact] + public void GroupJoinWithoutResultSelector() + { + var result = new[] { 0, 1, 2 }.AsQueryable().GroupJoin(new[] { 1, 2, 3 }, n1 => n1, n2 => n2).ToList(); + Assert.Equal(3, result.Count); + Assert.Equal(0, result[0].Key); + Assert.Empty(result[0]); + Assert.Equal(1, result[1].Key); + Assert.Single(result[1]); + Assert.Equal(2, result[2].Key); + Assert.Single(result[2]); + } + + [Fact] + public void GroupJoinWithoutResultSelector_OuterNull() + { + IQueryable outer = null; + int[] inner = { 1, 2, 3 }; + + AssertExtensions.Throws("outer", () => outer.GroupJoin(inner.AsQueryable(), n1 => n1, n2 => n2)); + } + + [Fact] + public void GroupJoinWithoutResultSelector_InnerNull() + { + int[] outer = { 0, 1, 2 }; + IQueryable inner = null; + + AssertExtensions.Throws("inner", () => outer.AsQueryable().GroupJoin(inner, n1 => n1, n2 => n2)); + } + + [Fact] + public void GroupJoinWithoutResultSelector_OuterKeySelectorNull() + { + int[] outer = { 0, 1, 2 }; + int[] inner = { 1, 2, 3 }; + + AssertExtensions.Throws("outerKeySelector", () => outer.AsQueryable().GroupJoin(inner.AsQueryable(), null, n2 => n2)); + } + + [Fact] + public void GroupJoinWithoutResultSelector_InnerKeySelectorNull() + { + int[] outer = { 0, 1, 2 }; + int[] inner = { 1, 2, 3 }; + + AssertExtensions.Throws("innerKeySelector", () => outer.AsQueryable().GroupJoin(inner.AsQueryable(), n1 => n1, null)); + } + + [Fact] + public void GroupJoinWithoutResultSelector_CustomComparer() + { + var result = new[] { "Tim", "Bob", "Robert" }.AsQueryable().GroupJoin(new[] { "miT", "Robert" }, n1 => n1, n2 => n2, new AnagramEqualityComparer()).ToList(); + Assert.Equal(3, result.Count); + Assert.Equal("Tim", result[0].Key); + Assert.Single(result[0]); + Assert.Equal("Bob", result[1].Key); + Assert.Empty(result[1]); + Assert.Equal("Robert", result[2].Key); + Assert.Single(result[2]); + } } } diff --git a/src/libraries/System.Linq.Queryable/tests/JoinTests.cs b/src/libraries/System.Linq.Queryable/tests/JoinTests.cs index 2677a404871d98..0b0b9c536c1ecc 100644 --- a/src/libraries/System.Linq.Queryable/tests/JoinTests.cs +++ b/src/libraries/System.Linq.Queryable/tests/JoinTests.cs @@ -262,5 +262,95 @@ public void Join2() var count = new[] { 0, 1, 2 }.AsQueryable().Join(new[] { 1, 2, 3 }, n1 => n1, n2 => n2, (n1, n2) => n1 + n2, EqualityComparer.Default).Count(); Assert.Equal(2, count); } + + [Fact] + public void TupleJoin_Basic() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + }; + OrderRec[] inner = { + new OrderRec{ orderID = 45321, custID = 99022, total = 50 }, + new OrderRec{ orderID = 43421, custID = 29022, total = 20 }, + new OrderRec{ orderID = 95421, custID = 98022, total = 9 } + }; + + var result = outer.AsQueryable().Join(inner.AsQueryable(), e => e.custID, e => e.custID).ToList(); + + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Outer.name == "Prakash" && r.Inner.orderID == 95421); + Assert.Contains(result, r => r.Outer.name == "Robert" && r.Inner.orderID == 45321); + } + + [Fact] + public void TupleJoin_WithComparer() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 } + }; + AnagramRec[] inner = { + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + }; + + var result = outer.AsQueryable().Join(inner.AsQueryable(), e => e.name, e => e.name, new AnagramEqualityComparer()).ToList(); + + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Outer.name == "Prakash" && r.Inner.name == "Prakash"); + Assert.Contains(result, r => r.Outer.name == "Tim" && r.Inner.name == "miT"); + } + + [Fact] + public void TupleJoin_OuterNull() + { + IQueryable outer = null; + OrderRec[] inner = { new OrderRec{ orderID = 45321, custID = 98022, total = 50 } }; + + AssertExtensions.Throws("outer", () => outer.Join(inner.AsQueryable(), e => e.custID, e => e.custID)); + } + + [Fact] + public void TupleJoin_InnerNull() + { + CustomerRec[] outer = { new CustomerRec{ name = "Prakash", custID = 98022 } }; + IEnumerable inner = null; + + AssertExtensions.Throws("inner", () => outer.AsQueryable().Join(inner, e => e.custID, e => e.custID)); + } + + [Fact] + public void TupleJoin_OuterKeySelectorNull() + { + CustomerRec[] outer = { new CustomerRec{ name = "Prakash", custID = 98022 } }; + OrderRec[] inner = { new OrderRec{ orderID = 45321, custID = 98022, total = 50 } }; + + AssertExtensions.Throws("outerKeySelector", () => outer.AsQueryable().Join(inner.AsQueryable(), (Expression>)null, e => e.custID)); + } + + [Fact] + public void TupleJoin_InnerKeySelectorNull() + { + CustomerRec[] outer = { new CustomerRec{ name = "Prakash", custID = 98022 } }; + OrderRec[] inner = { new OrderRec{ orderID = 45321, custID = 98022, total = 50 } }; + + AssertExtensions.Throws("innerKeySelector", () => outer.AsQueryable().Join(inner.AsQueryable(), e => e.custID, (Expression>)null)); + } + + [Fact] + public void TupleJoin1() + { + var count = new[] { 0, 1, 2 }.AsQueryable().Join(new[] { 1, 2, 3 }, n1 => n1, n2 => n2).Count(); + Assert.Equal(2, count); + } + + [Fact] + public void TupleJoin2() + { + var count = new[] { 0, 1, 2 }.AsQueryable().Join(new[] { 1, 2, 3 }, n1 => n1, n2 => n2, EqualityComparer.Default).Count(); + Assert.Equal(2, count); + } } } diff --git a/src/libraries/System.Linq.Queryable/tests/LeftJoinTests.cs b/src/libraries/System.Linq.Queryable/tests/LeftJoinTests.cs index 9666a1c7c8bb29..7d8b2d6282944e 100644 --- a/src/libraries/System.Linq.Queryable/tests/LeftJoinTests.cs +++ b/src/libraries/System.Linq.Queryable/tests/LeftJoinTests.cs @@ -270,5 +270,95 @@ public void Join2() var count = new[] { 0, 1, 2 }.AsQueryable().LeftJoin(new[] { 1, 2, 3 }, n1 => n1, n2 => n2, (n1, n2) => n1 + n2, EqualityComparer.Default).Count(); Assert.Equal(3, count); } + + [Fact] + public void TupleLeftJoin_Basic() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + }; + OrderRec[] inner = { + new OrderRec{ orderID = 45321, custID = 99022, total = 50 }, + new OrderRec{ orderID = 43421, custID = 29022, total = 20 }, + new OrderRec{ orderID = 95421, custID = 98022, total = 9 } + }; + + var result = outer.AsQueryable().LeftJoin(inner.AsQueryable(), e => e.custID, e => e.custID).ToList(); + + Assert.Equal(3, result.Count); + Assert.Contains(result, r => r.Outer.name == "Prakash" && r.Inner.orderID == 95421); + Assert.Contains(result, r => r.Outer.name == "Tim" && r.Inner.orderID == 0); + Assert.Contains(result, r => r.Outer.name == "Robert" && r.Inner.orderID == 45321); + } + + [Fact] + public void TupleLeftJoin_WithComparer() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 } + }; + AnagramRec[] inner = { + new AnagramRec{ name = "miT", orderID = 43455, total = 10 } + }; + + var result = outer.AsQueryable().LeftJoin(inner.AsQueryable(), e => e.name, e => e.name, new AnagramEqualityComparer()).ToList(); + + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Outer.name == "Tim" && r.Inner.name == "miT"); + Assert.Contains(result, r => r.Outer.name == "Prakash" && r.Inner.name == null); + } + + [Fact] + public void TupleLeftJoin_OuterNull() + { + IQueryable outer = null; + OrderRec[] inner = { new OrderRec{ orderID = 45321, custID = 98022, total = 50 } }; + + AssertExtensions.Throws("outer", () => outer.LeftJoin(inner.AsQueryable(), e => e.custID, e => e.custID)); + } + + [Fact] + public void TupleLeftJoin_InnerNull() + { + CustomerRec[] outer = { new CustomerRec{ name = "Prakash", custID = 98022 } }; + IEnumerable inner = null; + + AssertExtensions.Throws("inner", () => outer.AsQueryable().LeftJoin(inner, e => e.custID, e => e.custID)); + } + + [Fact] + public void TupleLeftJoin_OuterKeySelectorNull() + { + CustomerRec[] outer = { new CustomerRec{ name = "Prakash", custID = 98022 } }; + OrderRec[] inner = { new OrderRec{ orderID = 45321, custID = 98022, total = 50 } }; + + AssertExtensions.Throws("outerKeySelector", () => outer.AsQueryable().LeftJoin(inner.AsQueryable(), (Expression>)null, e => e.custID)); + } + + [Fact] + public void TupleLeftJoin_InnerKeySelectorNull() + { + CustomerRec[] outer = { new CustomerRec{ name = "Prakash", custID = 98022 } }; + OrderRec[] inner = { new OrderRec{ orderID = 45321, custID = 98022, total = 50 } }; + + AssertExtensions.Throws("innerKeySelector", () => outer.AsQueryable().LeftJoin(inner.AsQueryable(), e => e.custID, (Expression>)null)); + } + + [Fact] + public void TupleLeftJoin1() + { + var count = new[] { 0, 1, 2 }.AsQueryable().LeftJoin(new[] { 1, 2, 3 }, n1 => n1, n2 => n2).Count(); + Assert.Equal(3, count); + } + + [Fact] + public void TupleLeftJoin2() + { + var count = new[] { 0, 1, 2 }.AsQueryable().LeftJoin(new[] { 1, 2, 3 }, n1 => n1, n2 => n2, EqualityComparer.Default).Count(); + Assert.Equal(3, count); + } } } diff --git a/src/libraries/System.Linq.Queryable/tests/RightJoinTests.cs b/src/libraries/System.Linq.Queryable/tests/RightJoinTests.cs index 4ca4930a350edf..de0120176d6f20 100644 --- a/src/libraries/System.Linq.Queryable/tests/RightJoinTests.cs +++ b/src/libraries/System.Linq.Queryable/tests/RightJoinTests.cs @@ -269,5 +269,95 @@ public void Join2() var count = new[] { 0, 1, 2 }.AsQueryable().RightJoin(new[] { 1, 2, 3 }, n1 => n1, n2 => n2, (n1, n2) => n1 + n2, EqualityComparer.Default).Count(); Assert.Equal(3, count); } + + [Fact] + public void TupleRightJoin_Basic() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + }; + OrderRec[] inner = { + new OrderRec{ orderID = 45321, custID = 99022, total = 50 }, + new OrderRec{ orderID = 43421, custID = 29022, total = 20 }, + new OrderRec{ orderID = 95421, custID = 98022, total = 9 } + }; + + var result = outer.AsQueryable().RightJoin(inner.AsQueryable(), e => e.custID, e => e.custID).ToList(); + + Assert.Equal(3, result.Count); + Assert.Contains(result, r => r.Outer.name == "Robert" && r.Inner.orderID == 45321); + Assert.Contains(result, r => r.Outer.name == null && r.Inner.orderID == 43421); + Assert.Contains(result, r => r.Outer.name == "Prakash" && r.Inner.orderID == 95421); + } + + [Fact] + public void TupleRightJoin_WithComparer() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Tim", custID = 99021 } + }; + AnagramRec[] inner = { + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + }; + + var result = outer.AsQueryable().RightJoin(inner.AsQueryable(), e => e.name, e => e.name, new AnagramEqualityComparer()).ToList(); + + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Outer.name == "Tim" && r.Inner.name == "miT"); + Assert.Contains(result, r => r.Outer.name == null && r.Inner.name == "Prakash"); + } + + [Fact] + public void TupleRightJoin_OuterNull() + { + IQueryable outer = null; + OrderRec[] inner = { new OrderRec{ orderID = 45321, custID = 98022, total = 50 } }; + + AssertExtensions.Throws("outer", () => outer.RightJoin(inner.AsQueryable(), e => e.custID, e => e.custID)); + } + + [Fact] + public void TupleRightJoin_InnerNull() + { + CustomerRec[] outer = { new CustomerRec{ name = "Prakash", custID = 98022 } }; + IEnumerable inner = null; + + AssertExtensions.Throws("inner", () => outer.AsQueryable().RightJoin(inner, e => e.custID, e => e.custID)); + } + + [Fact] + public void TupleRightJoin_OuterKeySelectorNull() + { + CustomerRec[] outer = { new CustomerRec{ name = "Prakash", custID = 98022 } }; + OrderRec[] inner = { new OrderRec{ orderID = 45321, custID = 98022, total = 50 } }; + + AssertExtensions.Throws("outerKeySelector", () => outer.AsQueryable().RightJoin(inner.AsQueryable(), (Expression>)null, e => e.custID)); + } + + [Fact] + public void TupleRightJoin_InnerKeySelectorNull() + { + CustomerRec[] outer = { new CustomerRec{ name = "Prakash", custID = 98022 } }; + OrderRec[] inner = { new OrderRec{ orderID = 45321, custID = 98022, total = 50 } }; + + AssertExtensions.Throws("innerKeySelector", () => outer.AsQueryable().RightJoin(inner.AsQueryable(), e => e.custID, (Expression>)null)); + } + + [Fact] + public void TupleRightJoin1() + { + var count = new[] { 0, 1, 2 }.AsQueryable().RightJoin(new[] { 1, 2, 3 }, n1 => n1, n2 => n2).Count(); + Assert.Equal(3, count); + } + + [Fact] + public void TupleRightJoin2() + { + var count = new[] { 0, 1, 2 }.AsQueryable().RightJoin(new[] { 1, 2, 3 }, n1 => n1, n2 => n2, EqualityComparer.Default).Count(); + Assert.Equal(3, count); + } } } diff --git a/src/libraries/System.Linq/ref/System.Linq.cs b/src/libraries/System.Linq/ref/System.Linq.cs index 0c2d82b2111766..8451e05ac4fab6 100644 --- a/src/libraries/System.Linq/ref/System.Linq.cs +++ b/src/libraries/System.Linq/ref/System.Linq.cs @@ -81,6 +81,7 @@ public static System.Collections.Generic.IEnumerable< public static System.Collections.Generic.IEnumerable GroupBy(this System.Collections.Generic.IEnumerable source, System.Func keySelector, System.Func, TResult> resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } public static System.Collections.Generic.IEnumerable GroupBy(this System.Collections.Generic.IEnumerable source, System.Func keySelector, System.Func elementSelector, System.Func, TResult> resultSelector) { throw null; } public static System.Collections.Generic.IEnumerable GroupBy(this System.Collections.Generic.IEnumerable source, System.Func keySelector, System.Func elementSelector, System.Func, TResult> resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } + public static System.Collections.Generic.IEnumerable> GroupJoin(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IEnumerable GroupJoin(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func, TResult> resultSelector) { throw null; } public static System.Collections.Generic.IEnumerable GroupJoin(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func, TResult> resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } public static System.Collections.Generic.IEnumerable<(int Index, TSource Item)> Index(this System.Collections.Generic.IEnumerable source) { throw null; } @@ -91,6 +92,7 @@ public static System.Collections.Generic.IEnumerable< public static System.Collections.Generic.IEnumerable Intersect(this System.Collections.Generic.IEnumerable first, System.Collections.Generic.IEnumerable second, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } public static System.Collections.Generic.IEnumerable Join(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func resultSelector) { throw null; } public static System.Collections.Generic.IEnumerable Join(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } + public static System.Collections.Generic.IEnumerable<(TOuter Outer, TInner Inner)> Join(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static TSource? LastOrDefault(this System.Collections.Generic.IEnumerable source) { throw null; } public static TSource LastOrDefault(this System.Collections.Generic.IEnumerable source, TSource defaultValue) { throw null; } public static TSource? LastOrDefault(this System.Collections.Generic.IEnumerable source, System.Func predicate) { throw null; } @@ -99,6 +101,7 @@ public static System.Collections.Generic.IEnumerable< public static TSource Last(this System.Collections.Generic.IEnumerable source, System.Func predicate) { throw null; } public static System.Collections.Generic.IEnumerable LeftJoin(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func resultSelector) { throw null; } public static System.Collections.Generic.IEnumerable LeftJoin(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } + public static System.Collections.Generic.IEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static long LongCount(this System.Collections.Generic.IEnumerable source) { throw null; } public static long LongCount(this System.Collections.Generic.IEnumerable source, System.Func predicate) { throw null; } public static decimal Max(this System.Collections.Generic.IEnumerable source) { throw null; } @@ -167,6 +170,7 @@ public static System.Collections.Generic.IEnumerable< public static System.Collections.Generic.IEnumerable Reverse(this TSource[] source) { throw null; } public static System.Collections.Generic.IEnumerable RightJoin(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func resultSelector) { throw null; } public static System.Collections.Generic.IEnumerable RightJoin(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } + public static System.Collections.Generic.IEnumerable<(TOuter? Outer, TInner Inner)> RightJoin(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IEnumerable SelectMany(this System.Collections.Generic.IEnumerable source, System.Func> selector) { throw null; } public static System.Collections.Generic.IEnumerable SelectMany(this System.Collections.Generic.IEnumerable source, System.Func> selector) { throw null; } public static System.Collections.Generic.IEnumerable SelectMany(this System.Collections.Generic.IEnumerable source, System.Func> collectionSelector, System.Func resultSelector) { throw null; } diff --git a/src/libraries/System.Linq/src/System/Linq/GroupJoin.cs b/src/libraries/System.Linq/src/System/Linq/GroupJoin.cs index b59ca859ec568c..0fefce7dfc842e 100644 --- a/src/libraries/System.Linq/src/System/Linq/GroupJoin.cs +++ b/src/libraries/System.Linq/src/System/Linq/GroupJoin.cs @@ -1,12 +1,60 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections; using System.Collections.Generic; namespace System.Linq { public static partial class Enumerable { + /// Correlates the elements of two sequences based on key equality and groups the results. If is or omitted, the default equality comparer is used to compare keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to hash and compare keys, or to use . + /// + /// An that contains elements of type + /// where each grouping contains the outer element as the key and the matching inner elements. + /// + /// is . + /// is . + /// is . + /// is . + public static IEnumerable> GroupJoin(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer = null) + { + if (outer is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.outer); + } + + if (inner is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.inner); + } + + if (outerKeySelector is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.outerKeySelector); + } + + if (innerKeySelector is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.innerKeySelector); + } + + if (IsEmptyArray(outer)) + { + return []; + } + + return GroupJoinIterator(outer, inner, outerKeySelector, innerKeySelector, comparer); + } + public static IEnumerable GroupJoin(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func, TResult> resultSelector) => GroupJoin(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer: null); @@ -45,6 +93,21 @@ public static IEnumerable GroupJoin(this return GroupJoinIterator(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer); } + private static IEnumerable> GroupJoinIterator(IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer) + { + using IEnumerator e = outer.GetEnumerator(); + if (e.MoveNext()) + { + Lookup lookup = Lookup.CreateForJoin(inner, innerKeySelector, comparer); + do + { + TOuter item = e.Current; + yield return new GroupJoinGrouping(item, lookup[outerKeySelector(item)]); + } + while (e.MoveNext()); + } + } + private static IEnumerable GroupJoinIterator(IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func, TResult> resultSelector, IEqualityComparer? comparer) { using IEnumerator e = outer.GetEnumerator(); @@ -60,4 +123,22 @@ private static IEnumerable GroupJoinIterator : IGrouping + { + private readonly TKey _key; + private readonly IEnumerable _elements; + + public GroupJoinGrouping(TKey key, IEnumerable elements) + { + _key = key; + _elements = elements; + } + + public TKey Key => _key; + + public IEnumerator GetEnumerator() => _elements.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } } diff --git a/src/libraries/System.Linq/src/System/Linq/Join.cs b/src/libraries/System.Linq/src/System/Linq/Join.cs index 677f6bd0eb8b36..265757b021cf96 100644 --- a/src/libraries/System.Linq/src/System/Linq/Join.cs +++ b/src/libraries/System.Linq/src/System/Linq/Join.cs @@ -272,5 +272,83 @@ private static IEnumerable JoinIterator( } } } + + /// + /// Correlates the elements of two sequences based on matching keys. If is or omitted, the default equality comparer is used to compare keys. + /// + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to hash and compare keys, or to use . + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter Outer, TInner Inner) that are obtained by performing an inner join on two sequences. + /// or or or is . + /// + /// + /// This method is implemented by using deferred execution. The immediate return value is an object that stores + /// all the information that is required to perform the action. The query represented by this method is not + /// executed until the object is enumerated either by calling its GetEnumerator method directly or by + /// using foreach in C# or For Each in Visual Basic. + /// + /// + public static IEnumerable<(TOuter Outer, TInner Inner)> Join(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer = null) + { + if (outer is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.outer); + } + + if (inner is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.inner); + } + + if (outerKeySelector is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.outerKeySelector); + } + + if (innerKeySelector is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.innerKeySelector); + } + + if (IsEmptyArray(outer)) + { + return []; + } + + return JoinIterator(outer, inner, outerKeySelector, innerKeySelector, comparer); + } + + private static IEnumerable<(TOuter Outer, TInner Inner)> JoinIterator(IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer) + { + using IEnumerator e = outer.GetEnumerator(); + + if (e.MoveNext()) + { + Lookup lookup = Lookup.CreateForJoin(inner, innerKeySelector, comparer); + if (lookup.Count != 0) + { + do + { + TOuter item = e.Current; + Grouping? g = lookup.GetGrouping(outerKeySelector(item), create: false); + if (g is not null) + { + int count = g._count; + TInner[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return (item, elements[i]); + } + } + } while (e.MoveNext()); + } + } + } } } diff --git a/src/libraries/System.Linq/src/System/Linq/LeftJoin.cs b/src/libraries/System.Linq/src/System/Linq/LeftJoin.cs index 097e2453d0167e..59fa354aa928f8 100644 --- a/src/libraries/System.Linq/src/System/Linq/LeftJoin.cs +++ b/src/libraries/System.Linq/src/System/Linq/LeftJoin.cs @@ -272,5 +272,85 @@ private static IEnumerable LeftJoinIterator + /// Correlates the elements of two sequences based on matching keys. If is or omitted, the default equality comparer is used to compare keys. + /// + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to hash and compare keys, or to use . + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter Outer, TInner? Inner) that are obtained by performing a left outer join on two sequences. + /// or or or is . + /// + /// + /// This method is implemented by using deferred execution. The immediate return value is an object that stores + /// all the information that is required to perform the action. The query represented by this method is not + /// executed until the object is enumerated either by calling its GetEnumerator method directly or by + /// using foreach in C# or For Each in Visual Basic. + /// + /// + public static IEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer = null) + { + if (outer is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.outer); + } + + if (inner is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.inner); + } + + if (outerKeySelector is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.outerKeySelector); + } + + if (innerKeySelector is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.innerKeySelector); + } + + if (IsEmptyArray(outer)) + { + return []; + } + + return LeftJoinIterator(outer, inner, outerKeySelector, innerKeySelector, comparer); + } + + private static IEnumerable<(TOuter Outer, TInner? Inner)> LeftJoinIterator(IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer) + { + using IEnumerator e = outer.GetEnumerator(); + + if (e.MoveNext()) + { + Lookup innerLookup = Lookup.CreateForJoin(inner, innerKeySelector, comparer); + do + { + TOuter item = e.Current; + Grouping? g = innerLookup.GetGrouping(outerKeySelector(item), create: false); + if (g is null) + { + yield return (item, default); + } + else + { + int count = g._count; + TInner[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return (item, elements[i]); + } + } + } + while (e.MoveNext()); + } + } } } diff --git a/src/libraries/System.Linq/src/System/Linq/RightJoin.cs b/src/libraries/System.Linq/src/System/Linq/RightJoin.cs index 2485b7c13a281c..e54a0aea76ebe0 100644 --- a/src/libraries/System.Linq/src/System/Linq/RightJoin.cs +++ b/src/libraries/System.Linq/src/System/Linq/RightJoin.cs @@ -270,5 +270,85 @@ private static IEnumerable RightJoinIterator + /// Correlates the elements of two sequences based on matching keys. If is or omitted, the default equality comparer is used to compare keys. + /// + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to hash and compare keys, or to use . + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter? Outer, TInner Inner) that are obtained by performing a right outer join on two sequences. + /// or or or is . + /// + /// + /// This method is implemented by using deferred execution. The immediate return value is an object that stores + /// all the information that is required to perform the action. The query represented by this method is not + /// executed until the object is enumerated either by calling its GetEnumerator method directly or by + /// using foreach in C# or For Each in Visual Basic. + /// + /// + public static IEnumerable<(TOuter? Outer, TInner Inner)> RightJoin(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer = null) + { + if (outer is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.outer); + } + + if (inner is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.inner); + } + + if (outerKeySelector is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.outerKeySelector); + } + + if (innerKeySelector is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.innerKeySelector); + } + + if (IsEmptyArray(inner)) + { + return []; + } + + return RightJoinIterator(outer, inner, outerKeySelector, innerKeySelector, comparer); + } + + private static IEnumerable<(TOuter? Outer, TInner Inner)> RightJoinIterator(IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer) + { + using IEnumerator e = inner.GetEnumerator(); + + if (e.MoveNext()) + { + Lookup outerLookup = Lookup.CreateForJoin(outer, outerKeySelector, comparer); + do + { + TInner item = e.Current; + Grouping? g = outerLookup.GetGrouping(innerKeySelector(item), create: false); + if (g is null) + { + yield return (default, item); + } + else + { + int count = g._count; + TOuter[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return (elements[i], item); + } + } + } + while (e.MoveNext()); + } + } } } diff --git a/src/libraries/System.Linq/tests/GroupJoinTests.cs b/src/libraries/System.Linq/tests/GroupJoinTests.cs index 833ada49a5d89c..9144b718423333 100644 --- a/src/libraries/System.Linq/tests/GroupJoinTests.cs +++ b/src/libraries/System.Linq/tests/GroupJoinTests.cs @@ -513,5 +513,179 @@ public void ForcedToEnumeratorDoesntEnumerate() var en = iterator as IEnumerator>; Assert.False(en is not null && en.MoveNext()); } + + [Fact] + public void GroupJoinWithoutResultSelector_OuterEmptyInnerNonEmpty() + { + CustomerRec[] outer = []; + OrderRec[] inner = + [ + new OrderRec{ orderID = 45321, custID = 98022, total = 50 }, + new OrderRec{ orderID = 97865, custID = 32103, total = 25 } + ]; + Assert.Empty(outer.GroupJoin(inner, e => e.custID, e => e.custID)); + } + + [Fact] + public void GroupJoinWithoutResultSelector_OuterNonEmptyInnerEmpty() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Tim", custID = 43434 }, + new CustomerRec{ name = "Bob", custID = 34093 } + ]; + OrderRec[] inner = []; + + var result = outer.GroupJoin(inner, e => e.custID, e => e.custID).ToList(); + + Assert.Equal(2, result.Count); + Assert.Equal(outer[0], result[0].Key); + Assert.Empty(result[0]); + Assert.Equal(outer[1], result[1].Key); + Assert.Empty(result[1]); + } + + [Fact] + public void GroupJoinWithoutResultSelector_SingleElementEachAndMatches() + { + CustomerRec[] outer = [new CustomerRec{ name = "Tim", custID = 43434 }]; + OrderRec[] inner = [new OrderRec{ orderID = 97865, custID = 43434, total = 25 }]; + + var result = outer.GroupJoin(inner, e => e.custID, e => e.custID).ToList(); + + Assert.Single(result); + Assert.Equal(outer[0], result[0].Key); + Assert.Single(result[0]); + Assert.Equal(inner[0], result[0].First()); + } + + [Fact] + public void GroupJoinWithoutResultSelector_InnerSameKeyMoreThanOneElementAndMatches() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Tim", custID = 1234 }, + new CustomerRec{ name = "Bob", custID = 9865 } + ]; + OrderRec[] inner = + [ + new OrderRec{ orderID = 97865, custID = 1234, total = 25 }, + new OrderRec{ orderID = 34390, custID = 1234, total = 19 }, + new OrderRec{ orderID = 34390, custID = 9865, total = 19 } + ]; + + var result = outer.GroupJoin(inner, e => e.custID, e => e.custID).ToList(); + + Assert.Equal(2, result.Count); + Assert.Equal(outer[0], result[0].Key); + Assert.Equal(2, result[0].Count()); + Assert.Equal(outer[1], result[1].Key); + Assert.Single(result[1]); + } + + [Fact] + public void GroupJoinWithoutResultSelector_OuterNull() + { + CustomerRec[] outer = null; + OrderRec[] inner = + [ + new OrderRec{ orderID = 45321, custID = 98022, total = 50 } + ]; + + AssertExtensions.Throws("outer", () => outer.GroupJoin(inner, e => e.custID, e => e.custID)); + } + + [Fact] + public void GroupJoinWithoutResultSelector_InnerNull() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Tim", custID = 1234 } + ]; + OrderRec[] inner = null; + + AssertExtensions.Throws("inner", () => outer.GroupJoin(inner, e => e.custID, e => e.custID)); + } + + [Fact] + public void GroupJoinWithoutResultSelector_OuterKeySelectorNull() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Tim", custID = 1234 } + ]; + OrderRec[] inner = + [ + new OrderRec{ orderID = 45321, custID = 98022, total = 50 } + ]; + + AssertExtensions.Throws("outerKeySelector", () => outer.GroupJoin(inner, null, e => e.custID)); + } + + [Fact] + public void GroupJoinWithoutResultSelector_InnerKeySelectorNull() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Tim", custID = 1234 } + ]; + OrderRec[] inner = + [ + new OrderRec{ orderID = 45321, custID = 98022, total = 50 } + ]; + + AssertExtensions.Throws("innerKeySelector", () => outer.GroupJoin(inner, e => e.custID, null)); + } + + [Fact] + public void GroupJoinWithoutResultSelector_CanIterateMultipleTimes() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Tim", custID = 1234 } + ]; + OrderRec[] inner = + [ + new OrderRec{ orderID = 97865, custID = 1234, total = 25 } + ]; + + var result = outer.GroupJoin(inner, e => e.custID, e => e.custID).ToList(); + + Assert.Single(result); + + // Iterate the grouped elements multiple times + Assert.Single(result[0]); + Assert.Single(result[0]); + Assert.Equal(inner[0], result[0].First()); + Assert.Equal(inner[0], result[0].First()); + } + + [Fact] + public void GroupJoinWithoutResultSelector_CustomComparer() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Tim", custID = 1234 }, + new CustomerRec{ name = "Bob", custID = 9865 }, + new CustomerRec{ name = "Robert", custID = 9895 } + ]; + AnagramRec[] inner = + [ + new AnagramRec{ name = "Robert", orderID = 93483, total = 19 }, + new AnagramRec{ name = "miT", orderID = 93489, total = 45 } + ]; + + var result = outer.GroupJoin(inner, e => e.name, e => e.name, new AnagramEqualityComparer()).ToList(); + + Assert.Equal(3, result.Count); + Assert.Equal(outer[0], result[0].Key); + Assert.Single(result[0]); + Assert.Equal(inner[1], result[0].First()); + Assert.Equal(outer[1], result[1].Key); + Assert.Empty(result[1]); + Assert.Equal(outer[2], result[2].Key); + Assert.Single(result[2]); + Assert.Equal(inner[0], result[2].First()); + } } } diff --git a/src/libraries/System.Linq/tests/JoinTests.cs b/src/libraries/System.Linq/tests/JoinTests.cs index 4504fc47521d5c..5fc9571f0854b5 100644 --- a/src/libraries/System.Linq/tests/JoinTests.cs +++ b/src/libraries/System.Linq/tests/JoinTests.cs @@ -417,5 +417,109 @@ public void ForcedToEnumeratorDoesntEnumerate() var en = iterator as IEnumerator; Assert.False(en is not null && en.MoveNext()); } + + [Fact] + public void TupleJoin_Basic() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + ]; + OrderRec[] inner = + [ + new OrderRec{ orderID = 45321, custID = 99022, total = 50 }, + new OrderRec{ orderID = 43421, custID = 29022, total = 20 }, + new OrderRec{ orderID = 95421, custID = 98022, total = 9 } + ]; + + var result = outer.Join(inner, o => o.custID, i => i.custID); + + var expected = outer.Join(inner, o => o.custID, i => i.custID, (o, i) => (Outer: o, Inner: i)); + + Assert.Equal(expected, result); + } + + [Fact] + public void TupleJoin_EmptyOuter() + { + CustomerRec[] outer = []; + OrderRec[] inner = + [ + new OrderRec{ orderID = 45321, custID = 98022, total = 50 } + ]; + + Assert.Empty(outer.Join(inner, o => o.custID, i => i.custID)); + } + + [Fact] + public void TupleJoin_EmptyInner() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Prakash", custID = 98022 } + ]; + OrderRec[] inner = []; + + Assert.Empty(outer.Join(inner, o => o.custID, i => i.custID)); + } + + [Fact] + public void TupleJoin_WithComparer() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 } + ]; + AnagramRec[] inner = + [ + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + ]; + + var result = outer.Join(inner, o => o.name, i => i.name, new AnagramEqualityComparer()); + + Assert.Equal(2, result.Count()); + Assert.Contains(result, r => r.Outer.name == "Prakash" && r.Inner.name == "Prakash"); + Assert.Contains(result, r => r.Outer.name == "Tim" && r.Inner.name == "miT"); + } + + [Fact] + public void TupleJoin_OuterNull() + { + CustomerRec[] outer = null; + OrderRec[] inner = [new OrderRec{ orderID = 45321, custID = 98022, total = 50 }]; + + AssertExtensions.Throws("outer", () => outer.Join(inner, o => o.custID, i => i.custID)); + } + + [Fact] + public void TupleJoin_InnerNull() + { + CustomerRec[] outer = [new CustomerRec{ name = "Prakash", custID = 98022 }]; + OrderRec[] inner = null; + + AssertExtensions.Throws("inner", () => outer.Join(inner, o => o.custID, i => i.custID)); + } + + [Fact] + public void TupleJoin_OuterKeySelectorNull() + { + CustomerRec[] outer = [new CustomerRec{ name = "Prakash", custID = 98022 }]; + OrderRec[] inner = [new OrderRec{ orderID = 45321, custID = 98022, total = 50 }]; + + AssertExtensions.Throws("outerKeySelector", () => outer.Join(inner, (Func)null, i => i.custID)); + } + + [Fact] + public void TupleJoin_InnerKeySelectorNull() + { + CustomerRec[] outer = [new CustomerRec{ name = "Prakash", custID = 98022 }]; + OrderRec[] inner = [new OrderRec{ orderID = 45321, custID = 98022, total = 50 }]; + + AssertExtensions.Throws("innerKeySelector", () => outer.Join(inner, o => o.custID, (Func)null)); + } } } diff --git a/src/libraries/System.Linq/tests/LeftJoinTests.cs b/src/libraries/System.Linq/tests/LeftJoinTests.cs index 43e81014aad5b1..c5daddd5ec4182 100644 --- a/src/libraries/System.Linq/tests/LeftJoinTests.cs +++ b/src/libraries/System.Linq/tests/LeftJoinTests.cs @@ -446,5 +446,99 @@ public void ForcedToEnumeratorDoesntEnumerate() var en = iterator as IEnumerator; Assert.False(en is not null && en.MoveNext()); } + + [Fact] + public void TupleLeftJoin_Basic() + { + string[] outer = ["Prakash", "Tim", "Robert"]; + string[] inner = ["prakash", "robert"]; + + var result = outer.LeftJoin(inner, o => o.ToLowerInvariant(), i => i.ToLowerInvariant()).ToList(); + + var expected = outer.LeftJoin( + inner, + o => o.ToLowerInvariant(), + i => i.ToLowerInvariant(), + (o, i) => (Outer: o, Inner: i)).ToList(); + + Assert.Equal(expected, result); + } + + [Fact] + public void TupleLeftJoin_EmptyOuter() + { + string[] outer = []; + string[] inner = ["prakash"]; + + Assert.Empty(outer.LeftJoin(inner, o => o, i => i)); + } + + [Fact] + public void TupleLeftJoin_EmptyInner() + { + string[] outer = ["Prakash"]; + string[] inner = Array.Empty(); + + var result = outer.LeftJoin(inner, o => o, i => i).ToList(); + Assert.Single(result); + Assert.Equal("Prakash", result[0].Outer); + Assert.Null(result[0].Inner); + } + + [Fact] + public void TupleLeftJoin_WithComparer() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 } + ]; + AnagramRec[] inner = + [ + new AnagramRec{ name = "miT", orderID = 43455, total = 10 } + ]; + + var result = outer.LeftJoin(inner, o => o.name, i => i.name, new AnagramEqualityComparer()).ToList(); + + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Outer.name == "Prakash" && r.Inner.name == null); + Assert.Contains(result, r => r.Outer.name == "Tim" && r.Inner.name == "miT"); + } + + [Fact] + public void TupleLeftJoin_OuterNull() + { + CustomerRec[] outer = null; + OrderRec[] inner = [new OrderRec{ orderID = 45321, custID = 98022, total = 50 }]; + + AssertExtensions.Throws("outer", () => outer.LeftJoin(inner, o => o.custID, i => i.custID)); + } + + [Fact] + public void TupleLeftJoin_InnerNull() + { + CustomerRec[] outer = [new CustomerRec{ name = "Prakash", custID = 98022 }]; + OrderRec[] inner = null; + + AssertExtensions.Throws("inner", () => outer.LeftJoin(inner, o => o.custID, i => i.custID)); + } + + [Fact] + public void TupleLeftJoin_OuterKeySelectorNull() + { + CustomerRec[] outer = [new CustomerRec{ name = "Prakash", custID = 98022 }]; + OrderRec[] inner = [new OrderRec{ orderID = 45321, custID = 98022, total = 50 }]; + + AssertExtensions.Throws("outerKeySelector", () => outer.LeftJoin(inner, (Func)null, i => i.custID)); + } + + [Fact] + public void TupleLeftJoin_InnerKeySelectorNull() + { + CustomerRec[] outer = [new CustomerRec{ name = "Prakash", custID = 98022 }]; + OrderRec[] inner = [new OrderRec{ orderID = 45321, custID = 98022, total = 50 }]; + + AssertExtensions.Throws("innerKeySelector", () => outer.LeftJoin(inner, o => o.custID, (Func)null)); + } } } diff --git a/src/libraries/System.Linq/tests/RightJoinTests.cs b/src/libraries/System.Linq/tests/RightJoinTests.cs index 9eb51920f48be3..433b1fc21165af 100644 --- a/src/libraries/System.Linq/tests/RightJoinTests.cs +++ b/src/libraries/System.Linq/tests/RightJoinTests.cs @@ -443,5 +443,100 @@ public void ForcedToEnumeratorDoesntEnumerate() var en = iterator as IEnumerator; Assert.False(en is not null && en.MoveNext()); } + + [Fact] + public void TupleRightJoin_Basic() + { + string[] outer = ["prakash", "tim"]; + string[] inner = ["prakash", "robert", "unknown"]; + + var result = outer.RightJoin(inner, o => o, i => i).ToList(); + + (string? Outer, string Inner)[] expected = + [ + ("prakash", "prakash"), + (null, "robert"), + (null, "unknown") + ]; + + Assert.Equal(expected, result); + } + + [Fact] + public void TupleRightJoin_EmptyOuter() + { + string[] outer = []; + string[] inner = ["prakash"]; + + var result = outer.RightJoin(inner, o => o, i => i).ToList(); + Assert.Single(result); + Assert.Null(result[0].Outer); + Assert.Equal("prakash", result[0].Inner); + } + + [Fact] + public void TupleRightJoin_EmptyInner() + { + string[] outer = ["Prakash"]; + string[] inner = Array.Empty(); + + Assert.Empty(outer.RightJoin(inner, o => o, i => i)); + } + + [Fact] + public void TupleRightJoin_WithComparer() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Tim", custID = 99021 } + ]; + AnagramRec[] inner = + [ + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + ]; + + var result = outer.RightJoin(inner, o => o.name, i => i.name, new AnagramEqualityComparer()).ToList(); + + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Outer.name == "Tim" && r.Inner.name == "miT"); + Assert.Contains(result, r => r.Outer.name == null && r.Inner.name == "Prakash"); + } + + [Fact] + public void TupleRightJoin_OuterNull() + { + CustomerRec[] outer = null; + OrderRec[] inner = [new OrderRec{ orderID = 45321, custID = 98022, total = 50 }]; + + AssertExtensions.Throws("outer", () => outer.RightJoin(inner, o => o.custID, i => i.custID)); + } + + [Fact] + public void TupleRightJoin_InnerNull() + { + CustomerRec[] outer = [new CustomerRec{ name = "Prakash", custID = 98022 }]; + OrderRec[] inner = null; + + AssertExtensions.Throws("inner", () => outer.RightJoin(inner, o => o.custID, i => i.custID)); + } + + [Fact] + public void TupleRightJoin_OuterKeySelectorNull() + { + CustomerRec[] outer = [new CustomerRec{ name = "Prakash", custID = 98022 }]; + OrderRec[] inner = [new OrderRec{ orderID = 45321, custID = 98022, total = 50 }]; + + AssertExtensions.Throws("outerKeySelector", () => outer.RightJoin(inner, (Func)null, i => i.custID)); + } + + [Fact] + public void TupleRightJoin_InnerKeySelectorNull() + { + CustomerRec[] outer = [new CustomerRec{ name = "Prakash", custID = 98022 }]; + OrderRec[] inner = [new OrderRec{ orderID = 45321, custID = 98022, total = 50 }]; + + AssertExtensions.Throws("innerKeySelector", () => outer.RightJoin(inner, o => o.custID, (Func)null)); + } } } From 8acb7c23592c6fe57741ca7ff8552bf5ef2bd1ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Rozs=C3=ADval?= Date: Wed, 29 Apr 2026 12:44:33 +0200 Subject: [PATCH 014/115] Revert "[android] Enable CoreCLR runtime pack production for android-arm (#127225)" (#127547) Reverts #127225 Closes #127500 android-arm is crashing in CI with `SIGSEGV` in `System.DateTime.get_Now()`. See #127500. Reverting while the underlying crash is investigated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/Subsets.props | 3 ++- eng/pipelines/coreclr/templates/helix-queues-setup.yml | 7 ------- .../extra-platforms/runtime-extra-platforms-android.yml | 1 - eng/pipelines/runtime.yml | 3 +-- 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/eng/Subsets.props b/eng/Subsets.props index a7b6af15bac138..ba8bc03906a1cb 100644 --- a/eng/Subsets.props +++ b/eng/Subsets.props @@ -29,7 +29,8 @@ <_CoreCLRSupportedOS Condition="'$(TargetsMobile)' != 'true' and '$(TargetsLinuxBionic)' != 'true'">true - <_CoreCLRSupportedOS Condition="'$(TargetsAndroid)' == 'true' and '$(TargetArchitecture)' != 'x86'">true + + <_CoreCLRSupportedOS Condition="'$(TargetsAndroid)' == 'true' and '$(TargetArchitecture)' != 'arm' and '$(TargetArchitecture)' != 'x86'">true <_CoreCLRSupportedOS Condition="'$(TargetsBrowser)' == 'true'">true <_CoreCLRSupportedOS Condition="'$(TargetsAppleMobile)' == 'true'">true diff --git a/eng/pipelines/coreclr/templates/helix-queues-setup.yml b/eng/pipelines/coreclr/templates/helix-queues-setup.yml index a262c501258e41..ad687d71b45604 100644 --- a/eng/pipelines/coreclr/templates/helix-queues-setup.yml +++ b/eng/pipelines/coreclr/templates/helix-queues-setup.yml @@ -46,13 +46,6 @@ jobs: - ${{ if eq(variables['System.TeamProject'], 'internal') }}: - OSX.15.Amd64 - # Android arm - - ${{ if in(parameters.platform, 'android_arm') }}: - - ${{ if eq(variables['System.TeamProject'], 'public') }}: - - Windows.11.Amd64.Android.Open - - ${{ if eq(variables['System.TeamProject'], 'internal') }}: - - Windows.11.Amd64.Android - # Android arm64 - ${{ if in(parameters.platform, 'android_arm64') }}: - ${{ if eq(variables['System.TeamProject'], 'public') }}: diff --git a/eng/pipelines/extra-platforms/runtime-extra-platforms-android.yml b/eng/pipelines/extra-platforms/runtime-extra-platforms-android.yml index de253ff460f7a0..39c3a88c58993d 100644 --- a/eng/pipelines/extra-platforms/runtime-extra-platforms-android.yml +++ b/eng/pipelines/extra-platforms/runtime-extra-platforms-android.yml @@ -95,7 +95,6 @@ jobs: buildConfig: Release runtimeFlavor: coreclr platforms: - - android_arm - android_arm64 variables: # map dependencies variables to local variables diff --git a/eng/pipelines/runtime.yml b/eng/pipelines/runtime.yml index 76df5028f04bb4..9c5b24c8ceb3ee 100644 --- a/eng/pipelines/runtime.yml +++ b/eng/pipelines/runtime.yml @@ -1099,7 +1099,7 @@ extends: eq(variables['isRollingBuild'], true)) # - # Android arm/arm64 devices and x64 emulators + # Android arm64 devices and x64 emulators # Build the whole product using CoreCLR and run functional tests # - template: /eng/pipelines/common/platform-matrix.yml @@ -1109,7 +1109,6 @@ extends: buildConfig: Release runtimeFlavor: coreclr platforms: - - android_arm - android_x64 - android_arm64 variables: From 1f616d2734408490f63c25dbe4e9ddedda119e00 Mon Sep 17 00:00:00 2001 From: Ruihan-Yin <107431934+Ruihan-Yin@users.noreply.github.com> Date: Wed, 29 Apr 2026 03:49:05 -0700 Subject: [PATCH 015/115] [X64][APX] Fix a few bugs in APX implementation and improve the control knob design (#127033) Fix IMUL register encoding, remove embedded REX prefix in opcode when necessary Disable the NDD form of CMOV due to discrepant semantic of original CMOV and NDD form, follow-up PR to reintroduce the remaining NCI instructions (CTEST and CFCMOV) has been planned. Separate PP2 and PPX control knob Hide ConditionalChaining (`DOTNET_EnableApxConditionalChaining`) behind APX (`DOTNET_EnableAPX`) knob Disable IDIV/DIV due to lack of exception handling for REX2/EVEX prefixed instructions in VM Disable TEST ACC form for REX2, it is not compatible as the form does not use EGPRs. resolve merge errors --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Kendall Gonzalez Leon --- src/coreclr/jit/codegenxarch.cpp | 51 +++++++++++++-------------- src/coreclr/jit/emitxarch.cpp | 57 ++++++++++++++++++++++--------- src/coreclr/jit/instrsxarch.h | 40 +++++++++++----------- src/coreclr/jit/jitconfigvalues.h | 3 +- src/coreclr/jit/lowerxarch.cpp | 2 +- src/coreclr/jit/lsraxarch.cpp | 2 +- src/coreclr/jit/optimizebools.cpp | 3 +- 7 files changed, 91 insertions(+), 67 deletions(-) diff --git a/src/coreclr/jit/codegenxarch.cpp b/src/coreclr/jit/codegenxarch.cpp index 6d480c17bdfe11..b8d5ffb8f9452c 100644 --- a/src/coreclr/jit/codegenxarch.cpp +++ b/src/coreclr/jit/codegenxarch.cpp @@ -8974,8 +8974,6 @@ void CodeGen::genAmd64EmitterUnitTestsApx() // INS_bt only has reg-to-reg form. theEmitter->emitIns_R_R(INS_bt, EA_2BYTE, REG_EAX, REG_EDX); - theEmitter->emitIns_R(INS_idiv, EA_8BYTE, REG_EDX); - theEmitter->emitIns_R_R(INS_xchg, EA_8BYTE, REG_EAX, REG_EDX); theEmitter->emitIns_R(INS_div, EA_8BYTE, REG_EDX); @@ -9106,8 +9104,6 @@ void CodeGen::genAmd64EmitterUnitTestsApx() theEmitter->emitIns_R(INS_imulEAX, EA_8BYTE, REG_R12, INS_OPTS_EVEX_nf); theEmitter->emitIns_R(INS_mulEAX, EA_8BYTE, REG_R12, INS_OPTS_EVEX_nf); - theEmitter->emitIns_R(INS_div, EA_8BYTE, REG_R12, INS_OPTS_EVEX_nf); - theEmitter->emitIns_R(INS_idiv, EA_8BYTE, REG_R12, INS_OPTS_EVEX_nf); theEmitter->emitIns_R_R(INS_tzcnt_apx, EA_8BYTE, REG_R12, REG_R11, INS_OPTS_EVEX_nf); theEmitter->emitIns_R_R(INS_lzcnt_apx, EA_8BYTE, REG_R12, REG_R11, INS_OPTS_EVEX_nf); @@ -9183,6 +9179,12 @@ void CodeGen::genAmd64EmitterUnitTestsApx() theEmitter->emitIns_Mov(INS_movd32, EA_4BYTE, REG_R16, REG_XMM16, false); theEmitter->emitIns_R(INS_seto_apx, EA_1BYTE, REG_R11, INS_OPTS_EVEX_zu); + theEmitter->emitIns_I_AR(INS_imul_09, EA_4BYTE, 0x8149a, REG_EAX, 0x14); + theEmitter->emitIns_I_AR(INS_imul_19, EA_4BYTE, 0x8149a, REG_EAX, 0x14); + theEmitter->emitIns_I_AR(INS_imul_19, EA_4BYTE, 0x8149a, REG_R16, 0x14); + theEmitter->emitIns_S_I(INS_imul_19, EA_4BYTE, 0, 20, 30); + theEmitter->emitIns_S_I(INS_imul_09, EA_4BYTE, 0, 20, 30); + theEmitter->emitIns_R_AR(INS_crc32_apx, EA_4BYTE, REG_R17, REG_EAX, 0x14); } void CodeGen::genAmd64EmitterUnitTestsAvx10v2() @@ -10098,7 +10100,7 @@ void CodeGen::genPushCalleeSavedRegisters(regNumber initReg, bool* pInitRegZeroe #endif // DEBUG #ifdef TARGET_AMD64 - if (m_compiler->canUseApxEvexEncoding() && JitConfig.EnableApxPPX()) + if (m_compiler->canUseApxEvexEncoding() && JitConfig.EnableApxPP2()) { genPushCalleeSavedRegistersFromMaskAPX(rsPushRegs); return; @@ -10113,7 +10115,13 @@ void CodeGen::genPushCalleeSavedRegisters(regNumber initReg, bool* pInitRegZeroe if ((regBit & rsPushRegs) != 0) { +#ifdef TARGET_AMD64 + insOpts instOptions = + (m_compiler->canUseApxEvexEncoding() && JitConfig.EnableApxPPHint()) ? INS_OPTS_APX_ppx : INS_OPTS_NONE; + GetEmitter()->emitIns_R(INS_push, EA_PTRSIZE, reg, instOptions); +#else inst_RV(INS_push, reg, TYP_REF); +#endif m_compiler->unwindPush(reg); rsPushRegs &= ~regBit; } @@ -10224,7 +10232,7 @@ void CodeGen::genPopCalleeSavedRegisters(bool jmpEpilog) return; } - if (m_compiler->canUseApxEvexEncoding() && JitConfig.EnableApxPPX()) + if (m_compiler->canUseApxEvexEncoding() && JitConfig.EnableApxPP2()) { regMaskTP rsPopRegs = regSet.rsGetModifiedIntCalleeSavedRegsMask(); const unsigned popCount = genPopCalleeSavedRegistersFromMaskAPX(rsPopRegs); @@ -10248,10 +10256,12 @@ void CodeGen::genPopCalleeSavedRegisters(bool jmpEpilog) unsigned CodeGen::genPopCalleeSavedRegistersFromMask(regMaskTP rsPopRegs) { unsigned popCount = 0; + insOpts instOptions = + (m_compiler->canUseApxEvexEncoding() && JitConfig.EnableApxPPHint()) ? INS_OPTS_APX_ppx : INS_OPTS_NONE; if ((rsPopRegs & RBM_EBX) != 0) { popCount++; - inst_RV(INS_pop, REG_EBX, TYP_I_IMPL); + GetEmitter()->emitIns_R(INS_pop, EA_PTRSIZE, REG_EBX, instOptions); } if ((rsPopRegs & RBM_FPBASE) != 0) { @@ -10259,7 +10269,7 @@ unsigned CodeGen::genPopCalleeSavedRegistersFromMask(regMaskTP rsPopRegs) assert(!doubleAlignOrFramePointerUsed()); popCount++; - inst_RV(INS_pop, REG_EBP, TYP_I_IMPL); + GetEmitter()->emitIns_R(INS_pop, EA_PTRSIZE, REG_EBP, instOptions); } #ifndef UNIX_AMD64_ABI @@ -10267,35 +10277,22 @@ unsigned CodeGen::genPopCalleeSavedRegistersFromMask(regMaskTP rsPopRegs) if ((rsPopRegs & RBM_ESI) != 0) { popCount++; - inst_RV(INS_pop, REG_ESI, TYP_I_IMPL); + GetEmitter()->emitIns_R(INS_pop, EA_PTRSIZE, REG_ESI, instOptions); } if ((rsPopRegs & RBM_EDI) != 0) { popCount++; - inst_RV(INS_pop, REG_EDI, TYP_I_IMPL); + GetEmitter()->emitIns_R(INS_pop, EA_PTRSIZE, REG_EDI, instOptions); } #endif // !defined(UNIX_AMD64_ABI) #ifdef TARGET_AMD64 - if ((rsPopRegs & RBM_R12) != 0) - { - popCount++; - inst_RV(INS_pop, REG_R12, TYP_I_IMPL); - } - if ((rsPopRegs & RBM_R13) != 0) - { - popCount++; - inst_RV(INS_pop, REG_R13, TYP_I_IMPL); - } - if ((rsPopRegs & RBM_R14) != 0) - { - popCount++; - inst_RV(INS_pop, REG_R14, TYP_I_IMPL); - } - if ((rsPopRegs & RBM_R15) != 0) + regMaskTP popRegs = rsPopRegs & (RBM_R12 | RBM_R13 | RBM_R14 | RBM_R15); + while (popRegs != RBM_NONE) { + regNumber reg = genFirstRegNumFromMaskAndToggle(popRegs); popCount++; - inst_RV(INS_pop, REG_R15, TYP_I_IMPL); + GetEmitter()->emitIns_R(INS_pop, EA_PTRSIZE, reg, instOptions); } #endif // TARGET_AMD64 diff --git a/src/coreclr/jit/emitxarch.cpp b/src/coreclr/jit/emitxarch.cpp index 06edda78c24fbe..e8b6cd44a8ce64 100644 --- a/src/coreclr/jit/emitxarch.cpp +++ b/src/coreclr/jit/emitxarch.cpp @@ -2164,11 +2164,6 @@ emitter::code_t emitter::AddEvexPrefix(const instrDesc* id, code_t code, emitAtt code |= EXTENDED_EVEX_PP_BITS; } - if (instrIsExtendedReg3opImul(ins)) - { - // EVEX.R3 - code &= 0xFF7FFFFFFFFFFFFFULL; - } #ifdef TARGET_AMD64 if (IsCCMP(ins)) { @@ -2381,9 +2376,16 @@ emitter::code_t emitter::AddRex2Prefix(instruction ins, code_t code) { assert(IsRex2EncodableInstruction(ins)); +#ifdef TARGET_AMD64 + if (ins >= INS_imul_08 && ins <= INS_imul_15) + { + // These instructions have a built-in REX prefix, so it needs to be zeroed out when adding the prefix. + code &= 0xFFFFFFFFULL; + } +#endif + // Note that there are cases that some register field might be filled before adding prefix, // So we don't check if the code has REX2 prefix already or not. - code |= DEFAULT_2BYTE_REX2_PREFIX; if (IsLegacyMap1(code)) // 2-byte opcode on Map-1 { @@ -7189,6 +7191,19 @@ void emitter::emitIns_R_I(instruction ins, // ACC form is not promoted into EVEX space, need to emit with MI form. sz += 1; } + + if (ins == INS_test && reg == REG_EAX && TakesRex2Prefix(id)) + { + // test eax/rax will use ACC form, which is not REX2 compatible. + if (size == EA_8BYTE) + { + sz -= 1; + } + else + { + sz -= 2; + } + } #endif // TARGET_AMD64 // Do we need a REX prefix for AMD64? We need one if we are using any extended register (REX.R), or if we have a @@ -14697,11 +14712,13 @@ BYTE* emitter::emitOutputAM(BYTE* dst, instrDesc* id, code_t code, CnsVal* addc) case EA_4BYTE: #ifdef TARGET_AMD64 case EA_8BYTE: + // EVEX.MOVBE is assigned with RM opcode that does not follow the following rule. + if (ins != INS_movbe_apx) #endif - - /* Set the 'w' bit to get the large version */ - - code |= 0x1; + { + /* Set the 'w' bit to get the large version */ + code |= 0x1; + } break; #ifdef TARGET_X86 @@ -14719,9 +14736,11 @@ BYTE* emitter::emitOutputAM(BYTE* dst, instrDesc* id, code_t code, CnsVal* addc) break; } #ifdef TARGET_AMD64 - if (ins == INS_crc32_apx || ins == INS_movbe_apx) + if (ins >= INS_imul_08 && ins <= INS_imul_31) { - code |= (insEncodeReg345(id, id->idReg1(), size, &code) << 8); + // The built-in REX has been zeroed out in AddX86PrefixIfNeededAndNotPresent, need to add the register + // addressing bits in the prefix. + insEncodeReg345(id, inst3opImulReg(ins), size, &code); } #endif // TARGET_AMD64 } @@ -15608,11 +15627,11 @@ BYTE* emitter::emitOutputSV(BYTE* dst, instrDesc* id, code_t code, CnsVal* addc) break; } #ifdef TARGET_AMD64 - if (ins == INS_crc32_apx || ins == INS_movbe_apx) + if (ins >= INS_imul_08 && ins <= INS_imul_31) { - // The promoted CRC32 is in 1-byte opcode, unlike other instructions on this path, the register encoding for - // CRC32 need to be done here. - code |= (insEncodeReg345(id, id->idReg1(), size, &code) << 8); + // The built-in REX has been zero-ed out in AddX86PrefixIfNeededAndNotPresent, need to add the register + // addressing bits in the prefix. + insEncodeReg345(id, inst3opImulReg(ins), size, &code); } #endif // TARGET_AMD64 } @@ -17420,6 +17439,12 @@ BYTE* emitter::emitOutputRI(BYTE* dst, instrDesc* id) code = insCodeMI(ins); code = AddX86PrefixIfNeeded(id, code, size); code = insEncodeMIreg(id, reg, size, code); +#ifdef TARGET_AMD64 + if (ins >= INS_imul_08 && ins <= INS_imul_31) + { + insEncodeReg345(id, inst3opImulReg(ins), size, &code); + } +#endif // TARGET_AMD64 } } diff --git a/src/coreclr/jit/instrsxarch.h b/src/coreclr/jit/instrsxarch.h index fdadb2b2f52afe..53e3e28d3adc7d 100644 --- a/src/coreclr/jit/instrsxarch.h +++ b/src/coreclr/jit/instrsxarch.h @@ -102,24 +102,24 @@ INST3(movsx, "movsx", IUM_WR, BAD_CODE, BAD_CODE, #ifdef TARGET_AMD64 INST3(movsxd, "movsxd", IUM_WR, BAD_CODE, BAD_CODE, 0x000063, ZERO, 4X, INS_TT_NONE, REX_W1 | Encoding_REX2) #endif -INST3(movzx, "movzx", IUM_WR, BAD_CODE, BAD_CODE, 0x0F00B6, ZERO, 4X, INS_TT_NONE, INS_FLAGS_Has_Wbit | Encoding_REX2) - -INST3(cmovo, "cmovo", IUM_WR, BAD_CODE, BAD_CODE, 0x0F0040, 1C, 2X, INS_TT_NONE, Reads_OF | Encoding_REX2 | INS_Flags_Has_NDD) -INST3(cmovno, "cmovno", IUM_WR, BAD_CODE, BAD_CODE, 0x0F0041, 1C, 2X, INS_TT_NONE, Reads_OF | Encoding_REX2 | INS_Flags_Has_NDD) -INST3(cmovb, "cmovb", IUM_WR, BAD_CODE, BAD_CODE, 0x0F0042, 1C, 2X, INS_TT_NONE, Reads_CF | Encoding_REX2 | INS_Flags_Has_NDD) -INST3(cmovae, "cmovae", IUM_WR, BAD_CODE, BAD_CODE, 0x0F0043, 1C, 2X, INS_TT_NONE, Reads_CF | Encoding_REX2 | INS_Flags_Has_NDD) -INST3(cmove, "cmove", IUM_WR, BAD_CODE, BAD_CODE, 0x0F0044, 1C, 2X, INS_TT_NONE, Reads_ZF | Encoding_REX2 | INS_Flags_Has_NDD) -INST3(cmovne, "cmovne", IUM_WR, BAD_CODE, BAD_CODE, 0x0F0045, 1C, 2X, INS_TT_NONE, Reads_ZF | Encoding_REX2 | INS_Flags_Has_NDD) -INST3(cmovbe, "cmovbe", IUM_WR, BAD_CODE, BAD_CODE, 0x0F0046, 1C, 2X, INS_TT_NONE, Reads_ZF | Reads_CF | Encoding_REX2 | INS_Flags_Has_NDD) -INST3(cmova, "cmova", IUM_WR, BAD_CODE, BAD_CODE, 0x0F0047, 1C, 2X, INS_TT_NONE, Reads_ZF | Reads_CF | Encoding_REX2 | INS_Flags_Has_NDD) -INST3(cmovs, "cmovs", IUM_WR, BAD_CODE, BAD_CODE, 0x0F0048, 1C, 2X, INS_TT_NONE, Reads_SF | Encoding_REX2 | INS_Flags_Has_NDD) -INST3(cmovns, "cmovns", IUM_WR, BAD_CODE, BAD_CODE, 0x0F0049, 1C, 2X, INS_TT_NONE, Reads_SF | Encoding_REX2 | INS_Flags_Has_NDD) -INST3(cmovp, "cmovp", IUM_WR, BAD_CODE, BAD_CODE, 0x0F004A, 1C, 2X, INS_TT_NONE, Reads_PF | Encoding_REX2 | INS_Flags_Has_NDD) -INST3(cmovnp, "cmovnp", IUM_WR, BAD_CODE, BAD_CODE, 0x0F004B, 1C, 2X, INS_TT_NONE, Reads_PF | Encoding_REX2 | INS_Flags_Has_NDD) -INST3(cmovl, "cmovl", IUM_WR, BAD_CODE, BAD_CODE, 0x0F004C, 1C, 2X, INS_TT_NONE, Reads_OF | Reads_SF | Encoding_REX2 | INS_Flags_Has_NDD) -INST3(cmovge, "cmovge", IUM_WR, BAD_CODE, BAD_CODE, 0x0F004D, 1C, 2X, INS_TT_NONE, Reads_OF | Reads_SF | Encoding_REX2 | INS_Flags_Has_NDD) -INST3(cmovle, "cmovle", IUM_WR, BAD_CODE, BAD_CODE, 0x0F004E, 1C, 2X, INS_TT_NONE, Reads_OF | Reads_SF | Reads_ZF | Encoding_REX2 | INS_Flags_Has_NDD) -INST3(cmovg, "cmovg", IUM_WR, BAD_CODE, BAD_CODE, 0x0F004F, 1C, 2X, INS_TT_NONE, Reads_OF | Reads_SF | Reads_ZF | Encoding_REX2 | INS_Flags_Has_NDD) +INST3(movzx, "movzx", IUM_WR, BAD_CODE, BAD_CODE, 0x0F00B6, ZERO, 4X, INS_TT_NONE, INS_FLAGS_Has_Wbit | Encoding_REX2) + +INST3(cmovo, "cmovo", IUM_WR, BAD_CODE, BAD_CODE, 0x0F0040, 1C, 2X, INS_TT_NONE, Reads_OF | Encoding_REX2) +INST3(cmovno, "cmovno", IUM_WR, BAD_CODE, BAD_CODE, 0x0F0041, 1C, 2X, INS_TT_NONE, Reads_OF | Encoding_REX2) +INST3(cmovb, "cmovb", IUM_WR, BAD_CODE, BAD_CODE, 0x0F0042, 1C, 2X, INS_TT_NONE, Reads_CF | Encoding_REX2) +INST3(cmovae, "cmovae", IUM_WR, BAD_CODE, BAD_CODE, 0x0F0043, 1C, 2X, INS_TT_NONE, Reads_CF | Encoding_REX2) +INST3(cmove, "cmove", IUM_WR, BAD_CODE, BAD_CODE, 0x0F0044, 1C, 2X, INS_TT_NONE, Reads_ZF | Encoding_REX2) +INST3(cmovne, "cmovne", IUM_WR, BAD_CODE, BAD_CODE, 0x0F0045, 1C, 2X, INS_TT_NONE, Reads_ZF | Encoding_REX2) +INST3(cmovbe, "cmovbe", IUM_WR, BAD_CODE, BAD_CODE, 0x0F0046, 1C, 2X, INS_TT_NONE, Reads_ZF | Reads_CF | Encoding_REX2) +INST3(cmova, "cmova", IUM_WR, BAD_CODE, BAD_CODE, 0x0F0047, 1C, 2X, INS_TT_NONE, Reads_ZF | Reads_CF | Encoding_REX2) +INST3(cmovs, "cmovs", IUM_WR, BAD_CODE, BAD_CODE, 0x0F0048, 1C, 2X, INS_TT_NONE, Reads_SF | Encoding_REX2) +INST3(cmovns, "cmovns", IUM_WR, BAD_CODE, BAD_CODE, 0x0F0049, 1C, 2X, INS_TT_NONE, Reads_SF | Encoding_REX2) +INST3(cmovp, "cmovp", IUM_WR, BAD_CODE, BAD_CODE, 0x0F004A, 1C, 2X, INS_TT_NONE, Reads_PF | Encoding_REX2) +INST3(cmovnp, "cmovnp", IUM_WR, BAD_CODE, BAD_CODE, 0x0F004B, 1C, 2X, INS_TT_NONE, Reads_PF | Encoding_REX2) +INST3(cmovl, "cmovl", IUM_WR, BAD_CODE, BAD_CODE, 0x0F004C, 1C, 2X, INS_TT_NONE, Reads_OF | Reads_SF | Encoding_REX2) +INST3(cmovge, "cmovge", IUM_WR, BAD_CODE, BAD_CODE, 0x0F004D, 1C, 2X, INS_TT_NONE, Reads_OF | Reads_SF | Encoding_REX2) +INST3(cmovle, "cmovle", IUM_WR, BAD_CODE, BAD_CODE, 0x0F004E, 1C, 2X, INS_TT_NONE, Reads_OF | Reads_SF | Reads_ZF | Encoding_REX2) +INST3(cmovg, "cmovg", IUM_WR, BAD_CODE, BAD_CODE, 0x0F004F, 1C, 2X, INS_TT_NONE, Reads_OF | Reads_SF | Reads_ZF | Encoding_REX2) INST3(xchg, "xchg", IUM_RW, 0x000086, BAD_CODE, 0x000086, ILLEGAL, ILLEGAL, INS_TT_NONE, INS_FLAGS_Has_Wbit | Encoding_REX2) INST3(imul, "imul", IUM_RW, 0x0F00AC, BAD_CODE, 0x0F00AF, 3C, 1C, INS_TT_NONE, Writes_OF | Undefined_SF | Undefined_ZF | Undefined_AF | Undefined_PF | Writes_CF | INS_FLAGS_Has_Sbit | INS_Flags_Has_NDD | INS_Flags_Has_NF | Encoding_REX2) @@ -1279,9 +1279,9 @@ INST1(serialize, "serialize", IUM_RD, 0x0fe801, INST1(cwde, "cwde", IUM_RD, 0x000098, 1C, 4X, INS_TT_NONE, INS_FLAGS_HasPseudoName) INST1(cdq, "cdq", IUM_RD, 0x000099, 1C, 2X, INS_TT_NONE, INS_FLAGS_HasPseudoName) -INST1(idiv, "idiv", IUM_RD, 0x0038F6, ILLEGAL, ILLEGAL, INS_TT_NONE, Undefined_OF | Undefined_SF | Undefined_ZF | Undefined_AF | Undefined_PF | Undefined_CF | INS_FLAGS_Has_Wbit | Encoding_REX2 | INS_Flags_Has_NF) +INST1(idiv, "idiv", IUM_RD, 0x0038F6, ILLEGAL, ILLEGAL, INS_TT_NONE, Undefined_OF | Undefined_SF | Undefined_ZF | Undefined_AF | Undefined_PF | Undefined_CF | INS_FLAGS_Has_Wbit) INST1(imulEAX, "imul", IUM_RD, 0x0028F6, 4C, 1C, INS_TT_NONE, Writes_OF | Undefined_SF | Undefined_ZF | Undefined_AF | Undefined_PF | Writes_CF | INS_FLAGS_Has_Wbit | Encoding_REX2 | INS_Flags_Has_NF) -INST1(div, "div", IUM_RD, 0x0030F6, ILLEGAL, ILLEGAL, INS_TT_NONE, Undefined_OF | Undefined_SF | Undefined_ZF | Undefined_AF | Undefined_PF | Undefined_CF | INS_FLAGS_Has_Wbit | Encoding_REX2 | INS_Flags_Has_NF) +INST1(div, "div", IUM_RD, 0x0030F6, ILLEGAL, ILLEGAL, INS_TT_NONE, Undefined_OF | Undefined_SF | Undefined_ZF | Undefined_AF | Undefined_PF | Undefined_CF | INS_FLAGS_Has_Wbit) INST1(mulEAX, "mul", IUM_RD, 0x0020F6, 4C, 1C, INS_TT_NONE, Writes_OF | Undefined_SF | Undefined_ZF | Undefined_AF | Undefined_PF | Writes_CF | INS_FLAGS_Has_Wbit | Encoding_REX2 | INS_Flags_Has_NF) INST1(sahf, "sahf", IUM_RD, 0x00009E, ILLEGAL, ILLEGAL, INS_TT_NONE, Restore_SF_ZF_AF_PF_CF) diff --git a/src/coreclr/jit/jitconfigvalues.h b/src/coreclr/jit/jitconfigvalues.h index 41f52190955dab..a87b20f8491c78 100644 --- a/src/coreclr/jit/jitconfigvalues.h +++ b/src/coreclr/jit/jitconfigvalues.h @@ -445,7 +445,8 @@ RELEASE_CONFIG_INTEGER(EnableEmbeddedBroadcast, "EnableEmbeddedBroadcast", RELEASE_CONFIG_INTEGER(EnableEmbeddedMasking, "EnableEmbeddedMasking", 1) // Allows embedded masking to be disabled RELEASE_CONFIG_INTEGER(EnableApxNDD, "EnableApxNDD", 0) // Allows APX NDD feature to be disabled RELEASE_CONFIG_INTEGER(EnableApxConditionalChaining, "EnableApxConditionalChaining", 0) // Allows APX conditional compare chaining -RELEASE_CONFIG_INTEGER(EnableApxPPX, "EnableApxPPX", 0) // Allows APX PPX feature to be disabled +RELEASE_CONFIG_INTEGER(EnableApxPPHint, "EnableApxPPHint", 0) // Allows APX PPX Hint feature to be disabled +RELEASE_CONFIG_INTEGER(EnableApxPP2, "EnableApxPP2", 0) // Allows APX PP2 feature to be disabled RELEASE_CONFIG_INTEGER(EnableApxZU, "EnableApxZU", 0) // Allows APX ZU feature to be disabled // clang-format on diff --git a/src/coreclr/jit/lowerxarch.cpp b/src/coreclr/jit/lowerxarch.cpp index ea3684e0ff0ba0..493fa1e1a06acf 100644 --- a/src/coreclr/jit/lowerxarch.cpp +++ b/src/coreclr/jit/lowerxarch.cpp @@ -297,7 +297,7 @@ GenTree* Lowering::LowerBinaryArithmetic(GenTreeOp* binOp) ContainCheckBinary(binOp); #ifdef TARGET_AMD64 - if (JitConfig.EnableApxConditionalChaining()) + if (m_compiler->canUseApxEvexEncoding() && JitConfig.EnableApxConditionalChaining()) { if (binOp->OperIs(GT_AND, GT_OR)) { diff --git a/src/coreclr/jit/lsraxarch.cpp b/src/coreclr/jit/lsraxarch.cpp index b00873cfe5fa8f..02aee985e6ef71 100644 --- a/src/coreclr/jit/lsraxarch.cpp +++ b/src/coreclr/jit/lsraxarch.cpp @@ -1944,7 +1944,7 @@ int LinearScan::BuildModDiv(GenTree* tree) tgtPrefUse = op1Use; srcCount = 1; } - srcCount += BuildDelayFreeUses(op2, op1, availableIntRegs & ~(SRBM_RAX | SRBM_RDX)); + srcCount += BuildDelayFreeUses(op2, op1, lowGprRegs & ~(SRBM_RAX | SRBM_RDX)); buildInternalRegisterUses(); diff --git a/src/coreclr/jit/optimizebools.cpp b/src/coreclr/jit/optimizebools.cpp index c9ca63a77751ad..54f2b5bc4203be 100644 --- a/src/coreclr/jit/optimizebools.cpp +++ b/src/coreclr/jit/optimizebools.cpp @@ -1659,7 +1659,8 @@ PhaseStatus Compiler::optOptimizeBools() // trigger or not // else if ((compOpportunisticallyDependsOn(InstructionSet_APX) || JitConfig.JitEnableApxIfConv()) && // optBoolsDsc.optOptimizeCompareChainCondBlock()) - else if (JitConfig.EnableApxConditionalChaining() && !optSwitchDetectAndConvert(b1, true, &ccmpVec) && + else if (canUseApxEvexEncoding() && JitConfig.EnableApxConditionalChaining() && + !optSwitchDetectAndConvert(b1, true, &ccmpVec) && optBoolsDsc.optOptimizeCompareChainCondBlock()) { // The optimization will have merged b1 and b2. Retry the loop so that From 76c4f8b2c6052188940e86867688e24b865f26eb Mon Sep 17 00:00:00 2001 From: Milos Kotlar Date: Wed, 29 Apr 2026 14:32:12 +0200 Subject: [PATCH 016/115] [ios-clr] Enable StackTrace tests on Apple mobile (#127060) ## Description This PR enables StackTrace library tests on Apple mobile. The line numbers are not displayed on Apple mobile, tracked in https://github.com/dotnet/runtime/issues/124087. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/DiagnosticMethodInfoTests.cs | 3 ++- .../System.Diagnostics.StackTrace/tests/StackFrameTests.cs | 1 - .../System.Diagnostics.StackTrace/tests/StackTraceTests.cs | 4 ++-- src/libraries/tests.proj | 1 - 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Diagnostics.StackTrace/tests/DiagnosticMethodInfoTests.cs b/src/libraries/System.Diagnostics.StackTrace/tests/DiagnosticMethodInfoTests.cs index 98bfd84ffb5ba8..d40deeec4a2f51 100644 --- a/src/libraries/System.Diagnostics.StackTrace/tests/DiagnosticMethodInfoTests.cs +++ b/src/libraries/System.Diagnostics.StackTrace/tests/DiagnosticMethodInfoTests.cs @@ -23,7 +23,7 @@ public void Create_Null() public static IEnumerable Create_OpenDelegate_TestData() { // Tracked at https://github.com/dotnet/runtime/issues/100748 - bool hasGvmOpenDelegateBug = !PlatformDetection.IsMonoRuntime && !PlatformDetection.IsNativeAot && !PlatformDetection.IsAppleMobile; + bool hasGvmOpenDelegateBug = !PlatformDetection.IsMonoRuntime && !PlatformDetection.IsNativeAot; const string TestNamespace = nameof(System) + "." + nameof(System.Diagnostics) + "." + nameof(System.Diagnostics.Tests) + "."; @@ -71,6 +71,7 @@ public static IEnumerable Create_OpenDelegate_TestData() } [Theory] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124344", typeof(PlatformDetection), nameof(PlatformDetection.IsAppleMobile), nameof(PlatformDetection.IsCoreCLR))] [MemberData(nameof(Create_OpenDelegate_TestData))] public void Create_OpenDelegate(Delegate del, string expectedName, string expectedTypeName) { diff --git a/src/libraries/System.Diagnostics.StackTrace/tests/StackFrameTests.cs b/src/libraries/System.Diagnostics.StackTrace/tests/StackFrameTests.cs index e7fc88815908ff..0766cbeedcd9dc 100644 --- a/src/libraries/System.Diagnostics.StackTrace/tests/StackFrameTests.cs +++ b/src/libraries/System.Diagnostics.StackTrace/tests/StackFrameTests.cs @@ -37,7 +37,6 @@ public void Ctor_FNeedFileInfo(bool fNeedFileInfo) [Theory] [ActiveIssue("https://github.com/mono/mono/issues/15187", TestRuntimes.Mono)] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124344", typeof(PlatformDetection), nameof(PlatformDetection.IsAppleMobile))] [InlineData(StackFrame.OFFSET_UNKNOWN, true)] [InlineData(0, true)] [InlineData(1, true)] diff --git a/src/libraries/System.Diagnostics.StackTrace/tests/StackTraceTests.cs b/src/libraries/System.Diagnostics.StackTrace/tests/StackTraceTests.cs index d466e0986ed29b..38b974da105135 100644 --- a/src/libraries/System.Diagnostics.StackTrace/tests/StackTraceTests.cs +++ b/src/libraries/System.Diagnostics.StackTrace/tests/StackTraceTests.cs @@ -603,10 +603,10 @@ public void ToString_ShowILOffset_ByteArrayLoad() } } - // On Android, stack traces do not include file names and line numbers + // On Android and Apple mobile, stack traces do not include file names and line numbers // Tracking issue: https://github.com/dotnet/runtime/issues/124087 private static string FileInfoPattern(string fileLinePattern) => - PlatformDetection.IsAndroid ? "" : fileLinePattern; + PlatformDetection.IsAndroid || PlatformDetection.IsAppleMobile ? "" : fileLinePattern; public static Dictionary MethodExceptionStrings = new() { diff --git a/src/libraries/tests.proj b/src/libraries/tests.proj index 169c05b0b06517..5951e8d101798f 100644 --- a/src/libraries/tests.proj +++ b/src/libraries/tests.proj @@ -656,7 +656,6 @@ - From 6499da1ff76dbea11b1d083a63a167d99332d0e1 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Wed, 29 Apr 2026 14:35:54 +0200 Subject: [PATCH 017/115] JIT: Fully share helper-based suspension tails (#126360) When multiple awaits have common context handling logic during suspension we can share that logic completely by jumping to a common block. This PR implements that size optimization. --- src/coreclr/jit/async.cpp | 618 +++++++++++++++++++++++++++----------- src/coreclr/jit/async.h | 118 +++++--- src/coreclr/jit/gentree.h | 5 + 3 files changed, 517 insertions(+), 224 deletions(-) diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index 31384f45a64e12..dc5e45ffa8d43e 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -544,39 +544,35 @@ PhaseStatus AsyncTransformation::Run() PhaseStatus result = PhaseStatus::MODIFIED_NOTHING; ArrayStack blocksWithNormalAwaits(m_compiler->getAllocator(CMK_Async)); ArrayStack blocksWithTailAwaits(m_compiler->getAllocator(CMK_Async)); - int numNormalAwaits = 0; - int numTailAwaits = 0; - FindAwaits(blocksWithNormalAwaits, blocksWithTailAwaits, &numNormalAwaits, &numTailAwaits); + AggregatedAwaitInfo awaits = FindAwaits(blocksWithNormalAwaits, blocksWithTailAwaits); - if (numNormalAwaits + numTailAwaits > 1) + if (awaits.NumNormalAwaits + awaits.NumTailAwaits > 1) { CreateSharedReturnBB(); } // Transform all tail awaits first. They will not require running all of // our analyses. - if (numTailAwaits > 0) + if (awaits.NumTailAwaits > 0) { - JITDUMP("Found %d tail awaits in %d blocks\n", numTailAwaits, blocksWithTailAwaits.Height()); + JITDUMP("Found %u tail awaits in %d blocks\n", awaits.NumTailAwaits, blocksWithTailAwaits.Height()); TransformTailAwaits(blocksWithTailAwaits); m_compiler->fgInvalidateDfsTree(); - if (numNormalAwaits > 0) + if (awaits.NumNormalAwaits > 0) { // This may have changed blocks, so refind the normal awaits. blocksWithNormalAwaits.Reset(); blocksWithTailAwaits.Reset(); - numNormalAwaits = 0; - numTailAwaits = 0; - FindAwaits(blocksWithNormalAwaits, blocksWithTailAwaits, &numNormalAwaits, &numTailAwaits); + awaits = FindAwaits(blocksWithNormalAwaits, blocksWithTailAwaits); } result = PhaseStatus::MODIFIED_EVERYTHING; } - JITDUMP("Found %d awaits in %d blocks\n", numNormalAwaits, blocksWithNormalAwaits.Height()); + JITDUMP("Found %u awaits in %d blocks\n", awaits.NumNormalAwaits, blocksWithNormalAwaits.Height()); - if (numNormalAwaits <= 0) + if (awaits.NumNormalAwaits <= 0) { return result; } @@ -733,34 +729,40 @@ PhaseStatus AsyncTransformation::Run() // Parameters: // blocksWithNormalAwaits - [out] Blocks with normal awaits are pushed onto this stack // blocksWithTailAwaits - [out] Blocks with tail awaits are pushed onto this stack -// numNormalAwaits - [out] Number of normal awaits found -// numTailAwaits - [out] Number of tail awaits found // -void AsyncTransformation::FindAwaits(ArrayStack& blocksWithNormalAwaits, - ArrayStack& blocksWithTailAwaits, - int* numNormalAwaits, - int* numTailAwaits) +// Returns: +// Information about awaits in the function. +// +AggregatedAwaitInfo AsyncTransformation::FindAwaits(ArrayStack& blocksWithNormalAwaits, + ArrayStack& blocksWithTailAwaits) { + AggregatedAwaitInfo awaits; for (BasicBlock* block : m_compiler->Blocks()) { bool hasNormalAwait = false; bool hasTailAwait = false; for (GenTree* tree : LIR::AsRange(block)) { - if (!tree->IsCall() || !tree->AsCall()->IsAsync() || tree->AsCall()->IsTailCall()) + if (!tree->IsCall()) + { + continue; + } + + GenTreeCall* call = tree->AsCall(); + if (!call->IsAsync() || call->IsTailCall()) { continue; } - if (tree->AsCall()->GetAsyncInfo().IsTailAwait) + if (call->GetAsyncInfo().IsTailAwait) { hasTailAwait = true; - (*numTailAwaits)++; + awaits.NumTailAwaits++; } else { hasNormalAwait = true; - (*numNormalAwaits)++; + awaits.NumNormalAwaits++; } } @@ -774,6 +776,8 @@ void AsyncTransformation::FindAwaits(ArrayStack& blocksWithNormalAw blocksWithTailAwaits.Push(block); } } + + return awaits; } //------------------------------------------------------------------------ @@ -1235,8 +1239,12 @@ void AsyncTransformation::BuildContinuation(BasicBlock* block, JITDUMP(" Continuation will have keep alive object\n"); } - layoutBuilder->SetNeedsExecutionContext(); - JITDUMP(" Call has async-only save and restore of ExecutionContext; continuation will have ExecutionContext\n"); + if (call->GetAsyncInfo().NeedsToSaveAndRestoreExecutionContext()) + { + layoutBuilder->SetNeedsExecutionContext(); + JITDUMP( + " Call has async-only save and restore of ExecutionContext; continuation will have ExecutionContext\n"); + } } #ifdef DEBUG @@ -1645,22 +1653,28 @@ CallDefinitionInfo AsyncTransformation::CanonicalizeCallDefinition(BasicBlock* // BasicBlock* AsyncTransformation::CreateSuspensionBlock(BasicBlock* block, unsigned stateNum) { + BasicBlock* suspendBB; if (m_lastSuspensionBB == nullptr) { - m_lastSuspensionBB = m_compiler->fgLastBBInMainFunction(); + if (m_sharedReturnBB != nullptr) + { + suspendBB = m_compiler->fgNewBBbefore(BBJ_RETURN, m_sharedReturnBB, false); + } + else + { + m_lastSuspensionBB = m_compiler->fgLastBBInMainFunction(); + suspendBB = m_compiler->fgNewBBafter(BBJ_RETURN, m_lastSuspensionBB, false); + } + } + else + { + suspendBB = m_compiler->fgNewBBafter(BBJ_RETURN, m_lastSuspensionBB, false); } - BasicBlock* suspendBB = m_compiler->fgNewBBafter(BBJ_RETURN, m_lastSuspensionBB, false); suspendBB->clearTryIndex(); suspendBB->clearHndIndex(); suspendBB->inheritWeightPercentage(block, 0); m_lastSuspensionBB = suspendBB; - - if (m_sharedReturnBB != nullptr) - { - suspendBB->SetKindAndTargetEdge(BBJ_ALWAYS, m_compiler->fgAddRefPred(m_sharedReturnBB, suspendBB)); - } - JITDUMP(" Creating suspension " FMT_BB " for state %u\n", suspendBB->bbNum, stateNum); return suspendBB; @@ -1824,14 +1838,7 @@ void AsyncTransformation::CreateSuspension(BasicBlock* call FillInDataOnSuspension(call, layout, subLayout, suspendBB, mutatedSinceResumption, tailSaveSet); - FinishContextHandlingOnSuspension(callBlock, call, suspendBB, layout, subLayout); - - if (suspendBB->KindIs(BBJ_RETURN)) - { - newContinuation = m_compiler->gtNewLclvNode(newContinuationVar, TYP_REF); - GenTree* ret = m_compiler->gtNewOperNode(GT_RETURN_SUSPEND, TYP_VOID, newContinuation); - LIR::AsRange(suspendBB).InsertAtEnd(newContinuation, ret); - } + FinishContextHandlingAndSuspension(callBlock, call, suspendBB, layout, subLayout); } //------------------------------------------------------------------------ @@ -2005,13 +2012,56 @@ SaveSet AsyncTransformation::GetLocalSaveSet(const LclVarDsc* dsc, VARSET_VALARG } //------------------------------------------------------------------------ -// AsyncTransformation::FinishContextHandlingOnSuspension: -// Generate code to finish handling of contexts on suspension: +// AsyncTransformation::GetSuspensionContextHelper: +// Figure out what context handling helper can be used during suspension. +// +// Parameters: +// call - The async call +// +// Returns: +// Kind of helper that can be used, or None if no helper can be used. +// +// Remarks: +// - No helper exists for the case where there are no contexts to restore +// (when CORINFO_ASYNC_SAVE_CONTEXTS was not given by the EE), or when no +// execution context needs to be saved/restored. The former happens only in +// thunks where there is only one async call anyway, while the latter never +// currently happens. +// - We have two different helpers, depending on whether a continuation context needs to be captured. +// + For task awaits with ConfigureAwait(false), or for custom awaits, no continuation context is needed +// + For normal task awaits a continuation context is needed +// +SuspensionContextHelper AsyncTransformation::GetSuspensionContextHelper(GenTreeCall* call) +{ + CallArg* execContextArg = call->gtArgs.FindWellKnownArg(WellKnownArg::AsyncExecutionContext); + CallArg* syncContextArg = call->gtArgs.FindWellKnownArg(WellKnownArg::AsyncSynchronizationContext); + assert((execContextArg != nullptr) == (syncContextArg != nullptr)); + + // In most cases we can use a helper. It is not the case when the call has + // no contexts to restore, which is the case for task-returning thunks or + // more specifically when the EE told us !CORINFO_ASYNC_SAVE_CONTEXTS. + if ((execContextArg == nullptr) || !call->GetAsyncInfo().NeedsToSaveAndRestoreExecutionContext()) + { + return SuspensionContextHelper::None; + } + + if (call->GetAsyncInfo().ContinuationContextHandling == ContinuationContextHandling::ContinueOnCapturedContext) + { + return SuspensionContextHelper::WithContinuationContext; + } + + return SuspensionContextHelper::WithoutContinuationContext; +} + +//------------------------------------------------------------------------ +// AsyncTransformation::FinishContextHandlingAndSuspension: +// Generate code to finish handling of contexts on suspension, and finish the suspension: // - Capture SynchronizationContext or TaskScheduler into the continuation // if needed when later resuming // - Capture ExecutionContext into the continuation // - Restore current Thread._synchronizationContext and // Thread._executionContext from the state before the async call +// - Return continuation back to caller. // // Parameters: // callBlock - The block containing the async call @@ -2020,24 +2070,19 @@ SaveSet AsyncTransformation::GetLocalSaveSet(const LclVarDsc* dsc, VARSET_VALARG // layout - Information about the continuation layout. // subLayout - Per-call layout builder indicating which fields are needed. // -void AsyncTransformation::FinishContextHandlingOnSuspension(BasicBlock* callBlock, - GenTreeCall* call, - BasicBlock* suspendBB, - const ContinuationLayout& layout, - const ContinuationLayoutBuilder& subLayout) +void AsyncTransformation::FinishContextHandlingAndSuspension(BasicBlock* callBlock, + GenTreeCall* call, + BasicBlock* suspendBB, + const ContinuationLayout& layout, + const ContinuationLayoutBuilder& subLayout) { - CallArg* execContextArg = call->gtArgs.FindWellKnownArg(WellKnownArg::AsyncExecutionContext); - CallArg* syncContextArg = call->gtArgs.FindWellKnownArg(WellKnownArg::AsyncSynchronizationContext); - assert((execContextArg != nullptr) == (syncContextArg != nullptr)); + SuspensionContextHelper helper = GetSuspensionContextHelper(call); - // In most cases we can use a helper. It is not the case when the call has - // no contexts to restore, which is the case for task-returning thunks or - // more specifically when the EE told us !CORINFO_ASYNC_SAVE_CONTEXTS. - if (execContextArg != nullptr && subLayout.NeedsExecutionContext()) + if (helper != SuspensionContextHelper::None) { JITDUMP(" Call [%06u] has async context and captured execution context; using finish-suspension helper\n", Compiler::dspTreeID(call)); - FinishContextHandlingOnSuspensionWithHelper(callBlock, call, suspendBB, layout, subLayout); + FinishContextHandlingAndSuspensionWithHelper(callBlock, call, suspendBB, layout, subLayout, helper); return; } @@ -2108,10 +2153,23 @@ void AsyncTransformation::FinishContextHandlingOnSuspension(BasicBlock* } RestoreContexts(callBlock, call, suspendBB); + + assert(suspendBB->KindIs(BBJ_RETURN)); + + if (m_sharedReturnBB != nullptr) + { + suspendBB->SetKindAndTargetEdge(BBJ_ALWAYS, m_compiler->fgAddRefPred(m_sharedReturnBB, suspendBB)); + } + else + { + GenTree* newContinuation = m_compiler->gtNewLclvNode(GetNewContinuationVar(), TYP_REF); + GenTree* ret = m_compiler->gtNewOperNode(GT_RETURN_SUSPEND, TYP_VOID, newContinuation); + LIR::AsRange(suspendBB).InsertAtEnd(newContinuation, ret); + } } //------------------------------------------------------------------------ -// AsyncTransformation::FinishContextHandlingOnSuspensionWithHelper: +// AsyncTransformation::FinishContextHandlingAndSuspensionWithHelper: // Generate code to finish handling of contexts on suspension by calling into a helper. // // Parameters: @@ -2126,119 +2184,29 @@ void AsyncTransformation::FinishContextHandlingOnSuspension(BasicBlock* // context restores. We do that with a single helper call that does // everything, for both size and to avoid multiple loads of the Thread TLS. // -void AsyncTransformation::FinishContextHandlingOnSuspensionWithHelper(BasicBlock* callBlock, - GenTreeCall* call, - BasicBlock* suspendBB, - const ContinuationLayout& layout, - const ContinuationLayoutBuilder& subLayout) +void AsyncTransformation::FinishContextHandlingAndSuspensionWithHelper(BasicBlock* callBlock, + GenTreeCall* call, + BasicBlock* suspendBB, + const ContinuationLayout& layout, + const ContinuationLayoutBuilder& subLayout, + SuspensionContextHelper helper) { - CORINFO_METHOD_HANDLE helper = subLayout.NeedsContinuationContext() - ? m_asyncInfo->finishSuspensionWithContinuationContextMethHnd - : m_asyncInfo->finishSuspensionNoContinuationContextMethHnd; + assert(helper != SuspensionContextHelper::None); + assert((helper != SuspensionContextHelper::WithContinuationContext) || subLayout.NeedsContinuationContext()); - // Insert call - // finishSuspension[With|No]ContinuationContext( - // ref newContinuation.ContinuationContext, // optional - // ref newContinuation.Flags, // optional - // ref newContinuation.ExecutionContext, - // resumed, - // execContext, - // syncContext) - // + BasicBlock* sharedFinish = (helper == SuspensionContextHelper::WithContinuationContext) + ? m_sharedFinishContextHandlingWithContinuationContextBB + : m_sharedFinishContextHandlingWithoutContinuationContextBB; CallArg* execContextArg = call->gtArgs.FindWellKnownArg(WellKnownArg::AsyncExecutionContext); CallArg* syncContextArg = call->gtArgs.FindWellKnownArg(WellKnownArg::AsyncSynchronizationContext); assert((execContextArg != nullptr) && (syncContextArg != nullptr)); - GenTree* contContextAddrPlaceholder = nullptr; - GenTree* flagsPlaceholder = nullptr; - GenTree* execContextAddrPlaceholder = m_compiler->gtNewZeroConNode(TYP_BYREF); - GenTree* resumedPlaceholder = m_compiler->gtNewIconNode(0); - GenTree* execContextPlaceholder = m_compiler->gtNewNull(); - GenTree* syncContextPlaceholder = m_compiler->gtNewNull(); - - GenTreeCall* finishCall = m_compiler->gtNewCallNode(CT_USER_FUNC, helper, TYP_VOID); - SetCallEntrypointForR2R(finishCall, m_compiler, helper); - - finishCall->gtArgs.PushFront(m_compiler, NewCallArg::Primitive(syncContextPlaceholder)); - finishCall->gtArgs.PushFront(m_compiler, NewCallArg::Primitive(execContextPlaceholder)); - finishCall->gtArgs.PushFront(m_compiler, NewCallArg::Primitive(resumedPlaceholder)); - finishCall->gtArgs.PushFront(m_compiler, NewCallArg::Primitive(execContextAddrPlaceholder)); - - if (subLayout.NeedsContinuationContext()) - { - contContextAddrPlaceholder = m_compiler->gtNewZeroConNode(TYP_BYREF); - flagsPlaceholder = m_compiler->gtNewZeroConNode(TYP_BYREF); - finishCall->gtArgs.PushFront(m_compiler, NewCallArg::Primitive(flagsPlaceholder)); - finishCall->gtArgs.PushFront(m_compiler, NewCallArg::Primitive(contContextAddrPlaceholder)); - } - - m_compiler->compCurBB = suspendBB; - m_compiler->fgMorphTree(finishCall); - - LIR::AsRange(suspendBB).InsertAtEnd(LIR::SeqTree(m_compiler, finishCall)); - - if (subLayout.NeedsContinuationContext()) - { - // Replace contContextAddrPlaceholder with actual address of the continuation context - LIR::Use use; - bool gotUse = LIR::AsRange(suspendBB).TryGetUse(contContextAddrPlaceholder, &use); - assert(gotUse); - - GenTree* newContinuation = m_compiler->gtNewLclvNode(GetNewContinuationVar(), TYP_REF); - unsigned contContextOffset = OFFSETOF__CORINFO_Continuation__data + layout.ContinuationContextOffset; - GenTree* contContextAddrOffset = - m_compiler->gtNewOperNode(GT_ADD, TYP_BYREF, newContinuation, - m_compiler->gtNewIconNode((ssize_t)contContextOffset, TYP_I_IMPL)); - - LIR::AsRange(suspendBB).InsertBefore(contContextAddrPlaceholder, - LIR::SeqTree(m_compiler, contContextAddrOffset)); - use.ReplaceWith(contContextAddrOffset); - LIR::AsRange(suspendBB).Remove(contContextAddrPlaceholder); - - // Replace flagsPlaceholder with actual address of the flags - gotUse = LIR::AsRange(suspendBB).TryGetUse(flagsPlaceholder, &use); - assert(gotUse); - - newContinuation = m_compiler->gtNewLclvNode(GetNewContinuationVar(), TYP_REF); - unsigned flagsOffset = m_compiler->info.compCompHnd->getFieldOffset(m_asyncInfo->continuationFlagsFldHnd); - GenTree* flagsOffsetNode = - m_compiler->gtNewOperNode(GT_ADD, TYP_BYREF, newContinuation, - m_compiler->gtNewIconNode((ssize_t)flagsOffset, TYP_I_IMPL)); - - LIR::AsRange(suspendBB).InsertBefore(flagsPlaceholder, LIR::SeqTree(m_compiler, flagsOffsetNode)); - use.ReplaceWith(flagsOffsetNode); - LIR::AsRange(suspendBB).Remove(flagsPlaceholder); - } - - // Replace execContextAddrPlaceholder with actual address of the execution context - LIR::Use use; - bool gotUse = LIR::AsRange(suspendBB).TryGetUse(execContextAddrPlaceholder, &use); - assert(gotUse); - - GenTree* newContinuation = m_compiler->gtNewLclvNode(GetNewContinuationVar(), TYP_REF); - unsigned execContextOffset = OFFSETOF__CORINFO_Continuation__data + layout.ExecutionContextOffset; - GenTree* execContextAddrOffset = - m_compiler->gtNewOperNode(GT_ADD, TYP_BYREF, newContinuation, - m_compiler->gtNewIconNode((ssize_t)execContextOffset, TYP_I_IMPL)); - - LIR::AsRange(suspendBB).InsertBefore(execContextAddrPlaceholder, LIR::SeqTree(m_compiler, execContextAddrOffset)); - use.ReplaceWith(execContextAddrOffset); - LIR::AsRange(suspendBB).Remove(execContextAddrPlaceholder); - - // Replace resumedPlaceholder with actual "continuationParameter != null" arg - gotUse = LIR::AsRange(suspendBB).TryGetUse(resumedPlaceholder, &use); - assert(gotUse); - - GenTree* continuation = m_compiler->gtNewLclvNode(m_compiler->lvaAsyncContinuationArg, TYP_REF); - GenTree* null = m_compiler->gtNewNull(); - GenTree* resumed = m_compiler->gtNewOperNode(GT_NE, TYP_INT, continuation, null); - - LIR::AsRange(suspendBB).InsertBefore(resumedPlaceholder, LIR::SeqTree(m_compiler, resumed)); - use.ReplaceWith(resumed); - LIR::AsRange(suspendBB).Remove(resumedPlaceholder); - - // Replace execContextPlaceholder with actual value + // Get the contexts from the call node: + // 1. For shared finish, store it directly to the shared locals in the same block + // 2. For non-shared finish, just make sure it is a GT_LCL_VAR since we need to create + // a use in a different block. + // Also remove the nodes from the original block and the call args. GenTree* execContext = execContextArg->GetNode(); if (!execContext->OperIs(GT_LCL_VAR)) { @@ -2247,18 +2215,9 @@ void AsyncTransformation::FinishContextHandlingOnSuspensionWithHelper(BasicBlock use.ReplaceWithLclVar(m_compiler); execContext = use.Def(); } - - gotUse = LIR::AsRange(suspendBB).TryGetUse(execContextPlaceholder, &use); - assert(gotUse); - LIR::AsRange(callBlock).Remove(execContext); - LIR::AsRange(suspendBB).InsertBefore(execContextPlaceholder, execContext); - use.ReplaceWith(execContext); - LIR::AsRange(suspendBB).Remove(execContextPlaceholder); - call->gtArgs.RemoveUnsafe(execContextArg); - // Replace syncContextPlaceholder with actual value GenTree* syncContext = syncContextArg->GetNode(); if (!syncContext->OperIs(GT_LCL_VAR)) { @@ -2267,19 +2226,46 @@ void AsyncTransformation::FinishContextHandlingOnSuspensionWithHelper(BasicBlock use.ReplaceWithLclVar(m_compiler); syncContext = use.Def(); } + LIR::AsRange(callBlock).Remove(syncContext); + call->gtArgs.RemoveUnsafe(syncContextArg); - gotUse = LIR::AsRange(suspendBB).TryGetUse(syncContextPlaceholder, &use); - assert(gotUse); + if (sharedFinish != nullptr) + { + // Store the contexts to the shared locals that the shared finish block will take them from. + if (m_sharedFinishContextHandlingExecContextVar != BAD_VAR_NUM) + { + GenTree* storeExecContext = + m_compiler->gtNewStoreLclVarNode(m_sharedFinishContextHandlingExecContextVar, execContext); + LIR::AsRange(suspendBB).InsertAtEnd(LIR::SeqTree(m_compiler, storeExecContext)); + } - LIR::AsRange(callBlock).Remove(syncContext); - LIR::AsRange(suspendBB).InsertBefore(syncContextPlaceholder, syncContext); - use.ReplaceWith(syncContext); - LIR::AsRange(suspendBB).Remove(syncContextPlaceholder); + if (m_sharedFinishContextHandlingSyncContextVar != BAD_VAR_NUM) + { + GenTree* storeSyncContext = + m_compiler->gtNewStoreLclVarNode(m_sharedFinishContextHandlingSyncContextVar, syncContext); + LIR::AsRange(suspendBB).InsertAtEnd(LIR::SeqTree(m_compiler, storeSyncContext)); + } - call->gtArgs.RemoveUnsafe(syncContextArg); + // Then just finish by jumping. + suspendBB->SetKindAndTargetEdge(BBJ_ALWAYS, m_compiler->fgAddRefPred(sharedFinish, suspendBB)); + } + else + { + // Otherwise insert a new call + InsertFinishContextHandlingCall(suspendBB, layout, helper, execContext, syncContext); - JITDUMP(" Created FinishSuspension call on suspension:\n"); - DISPTREERANGE(LIR::AsRange(suspendBB), finishCall); + // And return either via a new GT_RETURN_SUSPEND or via the shared return BB. + if (m_sharedReturnBB != nullptr) + { + suspendBB->SetKindAndTargetEdge(BBJ_ALWAYS, m_compiler->fgAddRefPred(m_sharedReturnBB, suspendBB)); + } + else + { + GenTree* newContinuation = m_compiler->gtNewLclvNode(GetNewContinuationVar(), TYP_REF); + GenTree* ret = m_compiler->gtNewOperNode(GT_RETURN_SUSPEND, TYP_VOID, newContinuation); + LIR::AsRange(suspendBB).InsertAtEnd(newContinuation, ret); + } + } } //------------------------------------------------------------------------ @@ -2998,6 +2984,219 @@ void AsyncTransformation::CreateSharedReturnBB() DISPRANGE(LIR::AsRange(m_sharedReturnBB)); } +//------------------------------------------------------------------------ +// AsyncTransformation::CreateSharedFinishContextHandlingBB: +// Create a shared BB that finishes all necessary context handling and +// suspends the method. +// +// Parameters: +// helper - The type of helper to call +// layout - The continuation layout +// execContextMayVary - If true, callers may use different execution +// contexts, and thus we need a local to allow it to vary. +// syncContextMayVary - If true, callers may use different synchronization +// contexts, and thus we need a local to allow it to vary. +// +// Returns: +// Basic block that handles the shared finish logic. +// +BasicBlock* AsyncTransformation::CreateSharedFinishContextHandlingBB(SuspensionContextHelper helper, + const ContinuationLayout& layout, + bool execContextMayVary, + bool syncContextMayVary) +{ + assert(m_sharedReturnBB != nullptr); + BasicBlock* block = m_compiler->fgNewBBbefore(BBJ_ALWAYS, m_sharedReturnBB, false); + block->SetKindAndTargetEdge(BBJ_ALWAYS, m_compiler->fgAddRefPred(m_sharedReturnBB, block)); + block->bbSetRunRarely(); + block->clearTryIndex(); + block->clearHndIndex(); + + if (m_compiler->fgIsUsingProfileWeights()) + { + // All suspension BBs are cold, so we do not need to propagate any + // weights, but we do need to propagate the flag. + block->SetFlags(BBF_PROF_WEIGHT); + } + + unsigned execContextLclNum; + if (execContextMayVary) + { + if (m_sharedFinishContextHandlingExecContextVar == BAD_VAR_NUM) + { + m_sharedFinishContextHandlingExecContextVar = + m_compiler->lvaGrabTemp(false DEBUGARG("exec context for shared finish context handling")); + m_compiler->lvaGetDesc(m_sharedFinishContextHandlingExecContextVar)->lvType = TYP_REF; + } + + execContextLclNum = m_sharedFinishContextHandlingExecContextVar; + } + else + { + execContextLclNum = m_compiler->lvaAsyncExecutionContextVar; + } + + unsigned syncContextLclNum; + if (syncContextMayVary) + { + if (m_sharedFinishContextHandlingSyncContextVar == BAD_VAR_NUM) + { + m_sharedFinishContextHandlingSyncContextVar = + m_compiler->lvaGrabTemp(false DEBUGARG("sync context for shared finish context handling")); + m_compiler->lvaGetDesc(m_sharedFinishContextHandlingSyncContextVar)->lvType = TYP_REF; + } + + syncContextLclNum = m_sharedFinishContextHandlingSyncContextVar; + } + else + { + syncContextLclNum = m_compiler->lvaAsyncSynchronizationContextVar; + } + + InsertFinishContextHandlingCall(block, layout, helper, m_compiler->gtNewLclvNode(execContextLclNum, TYP_REF), + m_compiler->gtNewLclvNode(syncContextLclNum, TYP_REF)); + + return block; +} + +//------------------------------------------------------------------------ +// AsyncTransformation::InsertFinishContextHandlingCall: +// Insert a call to the specified context handling helper. +// +// Parameters: +// block - Block that should contain the call (inserted at the end) +// layout - The continuation layout +// helper - The type of helper +// execContext - The execution context tree to pass to the helper +// syncContext - The synchronization context tree to pass to the helper +// +void AsyncTransformation::InsertFinishContextHandlingCall(BasicBlock* block, + const ContinuationLayout& layout, + SuspensionContextHelper helper, + GenTree* execContext, + GenTree* syncContext) +{ + CORINFO_METHOD_HANDLE helperMethod = (helper == SuspensionContextHelper::WithContinuationContext) + ? m_asyncInfo->finishSuspensionWithContinuationContextMethHnd + : m_asyncInfo->finishSuspensionNoContinuationContextMethHnd; + + // Insert call + // finishSuspension[With|No]ContinuationContext( + // ref newContinuation.ContinuationContext, // optional + // ref newContinuation.Flags, // optional + // ref newContinuation.ExecutionContext, + // resumed, + // execContext, + // syncContext) + // + + GenTree* contContextAddrPlaceholder = nullptr; + GenTree* flagsPlaceholder = nullptr; + GenTree* execContextAddrPlaceholder = m_compiler->gtNewZeroConNode(TYP_BYREF); + GenTree* resumedPlaceholder = m_compiler->gtNewIconNode(0); + GenTree* execContextPlaceholder = m_compiler->gtNewNull(); + GenTree* syncContextPlaceholder = m_compiler->gtNewNull(); + + GenTreeCall* finishCall = m_compiler->gtNewCallNode(CT_USER_FUNC, helperMethod, TYP_VOID); + SetCallEntrypointForR2R(finishCall, m_compiler, helperMethod); + + finishCall->gtArgs.PushFront(m_compiler, NewCallArg::Primitive(syncContextPlaceholder)); + finishCall->gtArgs.PushFront(m_compiler, NewCallArg::Primitive(execContextPlaceholder)); + finishCall->gtArgs.PushFront(m_compiler, NewCallArg::Primitive(resumedPlaceholder)); + finishCall->gtArgs.PushFront(m_compiler, NewCallArg::Primitive(execContextAddrPlaceholder)); + + if (helper == SuspensionContextHelper::WithContinuationContext) + { + contContextAddrPlaceholder = m_compiler->gtNewZeroConNode(TYP_BYREF); + flagsPlaceholder = m_compiler->gtNewZeroConNode(TYP_BYREF); + finishCall->gtArgs.PushFront(m_compiler, NewCallArg::Primitive(flagsPlaceholder)); + finishCall->gtArgs.PushFront(m_compiler, NewCallArg::Primitive(contContextAddrPlaceholder)); + } + + m_compiler->compCurBB = block; + m_compiler->fgMorphTree(finishCall); + + LIR::AsRange(block).InsertAtEnd(LIR::SeqTree(m_compiler, finishCall)); + + if (helper == SuspensionContextHelper::WithContinuationContext) + { + // Replace contContextAddrPlaceholder with actual address of the continuation context + LIR::Use use; + bool gotUse = LIR::AsRange(block).TryGetUse(contContextAddrPlaceholder, &use); + assert(gotUse); + + GenTree* newContinuation = m_compiler->gtNewLclvNode(GetNewContinuationVar(), TYP_REF); + unsigned contContextOffset = OFFSETOF__CORINFO_Continuation__data + layout.ContinuationContextOffset; + GenTree* contContextAddrOffset = + m_compiler->gtNewOperNode(GT_ADD, TYP_BYREF, newContinuation, + m_compiler->gtNewIconNode((ssize_t)contContextOffset, TYP_I_IMPL)); + + LIR::AsRange(block).InsertBefore(contContextAddrPlaceholder, LIR::SeqTree(m_compiler, contContextAddrOffset)); + use.ReplaceWith(contContextAddrOffset); + LIR::AsRange(block).Remove(contContextAddrPlaceholder); + + // Replace flagsPlaceholder with actual address of the flags + gotUse = LIR::AsRange(block).TryGetUse(flagsPlaceholder, &use); + assert(gotUse); + + newContinuation = m_compiler->gtNewLclvNode(GetNewContinuationVar(), TYP_REF); + unsigned flagsOffset = m_compiler->info.compCompHnd->getFieldOffset(m_asyncInfo->continuationFlagsFldHnd); + GenTree* flagsOffsetNode = + m_compiler->gtNewOperNode(GT_ADD, TYP_BYREF, newContinuation, + m_compiler->gtNewIconNode((ssize_t)flagsOffset, TYP_I_IMPL)); + + LIR::AsRange(block).InsertBefore(flagsPlaceholder, LIR::SeqTree(m_compiler, flagsOffsetNode)); + use.ReplaceWith(flagsOffsetNode); + LIR::AsRange(block).Remove(flagsPlaceholder); + } + + // Replace execContextAddrPlaceholder with actual address of the execution context + LIR::Use use; + bool gotUse = LIR::AsRange(block).TryGetUse(execContextAddrPlaceholder, &use); + assert(gotUse); + + GenTree* newContinuation = m_compiler->gtNewLclvNode(GetNewContinuationVar(), TYP_REF); + unsigned execContextOffset = OFFSETOF__CORINFO_Continuation__data + layout.ExecutionContextOffset; + GenTree* execContextAddrOffset = + m_compiler->gtNewOperNode(GT_ADD, TYP_BYREF, newContinuation, + m_compiler->gtNewIconNode((ssize_t)execContextOffset, TYP_I_IMPL)); + + LIR::AsRange(block).InsertBefore(execContextAddrPlaceholder, LIR::SeqTree(m_compiler, execContextAddrOffset)); + use.ReplaceWith(execContextAddrOffset); + LIR::AsRange(block).Remove(execContextAddrPlaceholder); + + // Replace resumedPlaceholder with actual "continuationParameter != null" arg + gotUse = LIR::AsRange(block).TryGetUse(resumedPlaceholder, &use); + assert(gotUse); + + GenTree* continuation = m_compiler->gtNewLclvNode(m_compiler->lvaAsyncContinuationArg, TYP_REF); + GenTree* null = m_compiler->gtNewNull(); + GenTree* resumed = m_compiler->gtNewOperNode(GT_NE, TYP_INT, continuation, null); + + LIR::AsRange(block).InsertBefore(resumedPlaceholder, LIR::SeqTree(m_compiler, resumed)); + use.ReplaceWith(resumed); + LIR::AsRange(block).Remove(resumedPlaceholder); + + // Replace execContextPlaceholder with actual value + gotUse = LIR::AsRange(block).TryGetUse(execContextPlaceholder, &use); + assert(gotUse); + + LIR::AsRange(block).InsertBefore(execContextPlaceholder, execContext); + use.ReplaceWith(execContext); + LIR::AsRange(block).Remove(execContextPlaceholder); + + // Replace syncContextPlaceholder with actual value + gotUse = LIR::AsRange(block).TryGetUse(syncContextPlaceholder, &use); + assert(gotUse); + + LIR::AsRange(block).InsertBefore(syncContextPlaceholder, syncContext); + use.ReplaceWith(syncContext); + LIR::AsRange(block).Remove(syncContextPlaceholder); + + JITDUMP(" Created FinishSuspension call:\n"); + DISPTREERANGE(LIR::AsRange(block), finishCall); +} + //------------------------------------------------------------------------ // AsyncTransformation::CreateResumptionsAndSuspensions: // Walk all recorded async states and create the suspension and resumption @@ -3014,6 +3213,65 @@ void AsyncTransformation::CreateResumptionsAndSuspensions() ContinuationLayoutBuilder* sharedLayoutBuilder = ContinuationLayoutBuilder::CreateSharedLayout(m_compiler, m_states); sharedLayout = sharedLayoutBuilder->Create(); + + unsigned numSharedSuspensionsWithContinuationContext = 0; + unsigned numSharedSuspensionsWithoutContinuationContext = 0; + + bool execContextMayVary = false; + bool syncContextMayVary = false; + + for (const AsyncState& state : m_states) + { + SuspensionContextHelper helper = GetSuspensionContextHelper(state.Call); + switch (helper) + { + case SuspensionContextHelper::WithContinuationContext: + numSharedSuspensionsWithContinuationContext++; + break; + case SuspensionContextHelper::WithoutContinuationContext: + numSharedSuspensionsWithoutContinuationContext++; + break; + default: + break; + } + + // If all calls still have the async context vars we created early + // then avoid round tripping through a local which will create + // unnecessary additional register moves. This is a common case. + if (helper != SuspensionContextHelper::None) + { + CallArg* execContextArg = state.Call->gtArgs.FindWellKnownArg(WellKnownArg::AsyncExecutionContext); + CallArg* syncContextArg = + state.Call->gtArgs.FindWellKnownArg(WellKnownArg::AsyncSynchronizationContext); + assert((execContextArg != nullptr) && (syncContextArg != nullptr)); + + execContextMayVary |= + !execContextArg->GetNode()->OperIsScalarLocal() || + (execContextArg->GetNode()->AsLclVar()->GetLclNum() != m_compiler->lvaAsyncExecutionContextVar); + syncContextMayVary |= !syncContextArg->GetNode()->OperIsScalarLocal() || + (syncContextArg->GetNode()->AsLclVar()->GetLclNum() != + m_compiler->lvaAsyncSynchronizationContextVar); + } + } + + if (numSharedSuspensionsWithContinuationContext > 1) + { + JITDUMP("Using shared path for final context handling with continuation context -- needed by %u awaits\n", + numSharedSuspensionsWithContinuationContext); + m_sharedFinishContextHandlingWithContinuationContextBB = + CreateSharedFinishContextHandlingBB(SuspensionContextHelper::WithContinuationContext, *sharedLayout, + execContextMayVary, syncContextMayVary); + } + + if (numSharedSuspensionsWithoutContinuationContext > 1) + { + JITDUMP( + "Using shared path for final context handling without continuation context -- needed by %u awaits\n", + numSharedSuspensionsWithoutContinuationContext); + m_sharedFinishContextHandlingWithoutContinuationContextBB = + CreateSharedFinishContextHandlingBB(SuspensionContextHelper::WithoutContinuationContext, *sharedLayout, + execContextMayVary, syncContextMayVary); + } } JITDUMP("Creating suspensions and resumptions for %zu states\n", m_states.size()); diff --git a/src/coreclr/jit/async.h b/src/coreclr/jit/async.h index c1df2f21e3fc6a..ce25cd991362cf 100644 --- a/src/coreclr/jit/async.h +++ b/src/coreclr/jit/async.h @@ -331,6 +331,19 @@ enum class SaveSet MutatedLocals, }; +enum class SuspensionContextHelper +{ + None, + WithContinuationContext, + WithoutContinuationContext, +}; + +struct AggregatedAwaitInfo +{ + unsigned NumNormalAwaits = 0; + unsigned NumTailAwaits = 0; +}; + class AsyncTransformation { friend class AsyncAnalysis; @@ -349,10 +362,16 @@ class AsyncTransformation BasicBlock* m_lastResumptionBB = nullptr; BasicBlock* m_sharedReturnBB = nullptr; - void FindAwaits(ArrayStack& blocksWithNormalAwaits, - ArrayStack& blocksWithTailAwaits, - int* numNormalAwaits, - int* numTailAwaits); + // Shared basic blocks used by suspensions that handle required context + // saves/restores and then suspend. + BasicBlock* m_sharedFinishContextHandlingWithContinuationContextBB = nullptr; + BasicBlock* m_sharedFinishContextHandlingWithoutContinuationContextBB = nullptr; + // Variables that shared suspension finishing BBs take the exec/sync contexts in + unsigned m_sharedFinishContextHandlingExecContextVar = BAD_VAR_NUM; + unsigned m_sharedFinishContextHandlingSyncContextVar = BAD_VAR_NUM; + + AggregatedAwaitInfo FindAwaits(ArrayStack& blocksWithNormalAwaits, + ArrayStack& blocksWithTailAwaits); void TransformTailAwaits(ArrayStack& blocksWithTailAwaits); void TransformTailAwait(BasicBlock* block, GenTreeCall* call, BasicBlock** remainder); @@ -399,36 +418,38 @@ class AsyncTransformation GenTree* prevContinuation, const ContinuationLayout& layout); - void FillInDataOnSuspension(GenTreeCall* call, - const ContinuationLayout& layout, - const ContinuationLayoutBuilder& subLayout, - BasicBlock* suspendBB, - VARSET_VALARG_TP mutatedSinceResumption, - SaveSet saveSet); - SaveSet GetLocalSaveSet(const LclVarDsc* dsc, VARSET_VALARG_TP mutatedSinceResumption); - void FinishContextHandlingOnSuspension(BasicBlock* callBlock, - GenTreeCall* call, - BasicBlock* suspendBB, - const ContinuationLayout& layout, - const ContinuationLayoutBuilder& subLayout); - void FinishContextHandlingOnSuspensionWithHelper(BasicBlock* callBlock, - GenTreeCall* call, - BasicBlock* suspendBB, - const ContinuationLayout& layout, - const ContinuationLayoutBuilder& subLayout); - void RestoreContexts(BasicBlock* block, GenTreeCall* call, BasicBlock* insertionBB); - void CreateCheckAndSuspendAfterCall(BasicBlock* block, - GenTreeCall* call, - const CallDefinitionInfo& callDefInfo, - BasicBlock* suspendBB, - BasicBlock** remainder); - BasicBlock* CreateResumptionBlock(BasicBlock* remainder, unsigned stateNum); - void CreateResumption(BasicBlock* callBlock, - GenTreeCall* call, - BasicBlock* resumeBB, - const CallDefinitionInfo& callDefInfo, - const ContinuationLayout& layout, - const ContinuationLayoutBuilder& subLayout); + void FillInDataOnSuspension(GenTreeCall* call, + const ContinuationLayout& layout, + const ContinuationLayoutBuilder& subLayout, + BasicBlock* suspendBB, + VARSET_VALARG_TP mutatedSinceResumption, + SaveSet saveSet); + SaveSet GetLocalSaveSet(const LclVarDsc* dsc, VARSET_VALARG_TP mutatedSinceResumption); + SuspensionContextHelper GetSuspensionContextHelper(GenTreeCall* call); + void FinishContextHandlingAndSuspension(BasicBlock* callBlock, + GenTreeCall* call, + BasicBlock* suspendBB, + const ContinuationLayout& layout, + const ContinuationLayoutBuilder& subLayout); + void FinishContextHandlingAndSuspensionWithHelper(BasicBlock* callBlock, + GenTreeCall* call, + BasicBlock* suspendBB, + const ContinuationLayout& layout, + const ContinuationLayoutBuilder& subLayout, + SuspensionContextHelper helper); + void RestoreContexts(BasicBlock* block, GenTreeCall* call, BasicBlock* insertionBB); + void CreateCheckAndSuspendAfterCall(BasicBlock* block, + GenTreeCall* call, + const CallDefinitionInfo& callDefInfo, + BasicBlock* suspendBB, + BasicBlock** remainder); + BasicBlock* CreateResumptionBlock(BasicBlock* remainder, unsigned stateNum); + void CreateResumption(BasicBlock* callBlock, + GenTreeCall* call, + BasicBlock* resumeBB, + const CallDefinitionInfo& callDefInfo, + const ContinuationLayout& layout, + const ContinuationLayoutBuilder& subLayout); void RestoreFromDataOnResumption(const ContinuationLayout& layout, const ContinuationLayoutBuilder& subLayout, @@ -449,16 +470,25 @@ class AsyncTransformation var_types storeType, GenTreeFlags indirFlags = GTF_IND_NONFAULTING); - void CreateDebugInfoForSuspensionPoint(const ContinuationLayout& layout, - const ContinuationLayoutBuilder& subLayout); - unsigned GetReturnedContinuationVar(); - unsigned GetNewContinuationVar(); - unsigned GetResultBaseVar(); - unsigned GetExceptionVar(); - void CreateSharedReturnBB(); - bool ReuseContinuations(); - void CreateResumptionsAndSuspensions(); - void CreateResumptionSwitch(); + void CreateDebugInfoForSuspensionPoint(const ContinuationLayout& layout, + const ContinuationLayoutBuilder& subLayout); + unsigned GetReturnedContinuationVar(); + unsigned GetNewContinuationVar(); + unsigned GetResultBaseVar(); + unsigned GetExceptionVar(); + void CreateSharedReturnBB(); + BasicBlock* CreateSharedFinishContextHandlingBB(SuspensionContextHelper helper, + const ContinuationLayout& layout, + bool execContextMayVary, + bool syncContextMayVary); + void InsertFinishContextHandlingCall(BasicBlock* block, + const ContinuationLayout& layout, + SuspensionContextHelper helper, + GenTree* execContext, + GenTree* syncContext); + bool ReuseContinuations(); + void CreateResumptionsAndSuspensions(); + void CreateResumptionSwitch(); public: AsyncTransformation(Compiler* comp) diff --git a/src/coreclr/jit/gentree.h b/src/coreclr/jit/gentree.h index e55073378cddd9..490ea6c08f1d40 100644 --- a/src/coreclr/jit/gentree.h +++ b/src/coreclr/jit/gentree.h @@ -4496,6 +4496,11 @@ struct AsyncCallInfo // Tail awaits do not generate suspension points and the JIT instead // directly returns the callee's continuation to the caller. bool IsTailAwait = false; + + bool NeedsToSaveAndRestoreExecutionContext() const + { + return true; + } }; // Return type descriptor of a GT_CALL node. From d1163e5a8f3f3aaa374993e8b5805911689aba28 Mon Sep 17 00:00:00 2001 From: Clinton Ingram Date: Wed, 29 Apr 2026 06:06:58 -0700 Subject: [PATCH 018/115] JIT: Accelerate floating->long casts on x86 (#125180) This adds floating->long/ulong cast codegen for AVX-512 and AVX10.2 on x86. With this, all non-overflow casts are now hardware accelerated. This is the last bit pulled from #116805. Typical Diff (double->long AVX-512): ```diff - sub esp, 8 - vzeroupper - vmovsd xmm0, qword ptr [esp+0x0C] - sub esp, 8 - ; npt arg push 0 - ; npt arg push 1 - vmovsd qword ptr [esp], xmm0 - call CORINFO_HELP_DBL2LNG - ; gcr arg pop 2 + vmovsd xmm0, qword ptr [esp+0x04] + vcmpordsd k1, xmm0, xmm0 + vcmpge_oqsd k2, xmm0, qword ptr [@RWD00] + vcvttpd2qq xmm0 {k1}{z}, xmm0 + vpblendmq xmm0 {k2}, xmm0, qword ptr [@RWD08] {1to2} + vmovd eax, xmm0 + vpextrd edx, xmm0, 1 - add esp, 8 ret 8 +RWD00 dq 43E0000000000000h +RWD08 dq 7FFFFFFFFFFFFFFFh -; Total bytes of code 31 +; Total bytes of code 53 ``` Full [Diffs](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1391699&view=ms.vss-build-web.run-extensions-tab) Breakdown of the double->long asm: ```asm ; load the scalar double vmovsd xmm0, qword ptr [esp+0x04] ; set the low bit of k1 if the scalar value is not NaN vcmpordsd k1, xmm0, xmm0 ; set the low bit of k2 if the input was greater than or equal to 2^63 (nearest double greater than long.MaxValue) vcmpge_oqsd k2, xmm0, qword ptr [@RWD00] ; convert, using k1 mask bit. if the mask bit is not set (meaning we have a NaN), set the value to zero vcvttpd2qq xmm0 {k1}{z}, xmm0 ; if the low bit of k2 is set (meaning overflow), set the value to long.MaxValue, otherwise take the conversion result vpblendmq xmm0 {k2}, xmm0, qword ptr [@RWD08] {1to2} ; extract the two 32-bit halves of the long result vmovd eax, xmm0 vpextrd edx, xmm0, 1 ``` --------- Co-authored-by: Tanner Gooding Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/coreclr/jit/decomposelongs.cpp | 160 ++++++++++++++++++++++--- src/coreclr/jit/flowgraph.cpp | 8 +- src/coreclr/jit/gentree.cpp | 106 ++++++++++++---- src/coreclr/jit/hwintrinsiclistxarch.h | 1 + src/coreclr/jit/hwintrinsicxarch.cpp | 1 + src/coreclr/jit/lowerxarch.cpp | 5 +- 6 files changed, 238 insertions(+), 43 deletions(-) diff --git a/src/coreclr/jit/decomposelongs.cpp b/src/coreclr/jit/decomposelongs.cpp index 97ba9bb6ad53b7..b14aab6bfb3ec1 100644 --- a/src/coreclr/jit/decomposelongs.cpp +++ b/src/coreclr/jit/decomposelongs.cpp @@ -587,40 +587,172 @@ GenTree* DecomposeLongs::DecomposeCast(LIR::Use& use) } #if defined(FEATURE_HW_INTRINSICS) && defined(TARGET_X86) - if (varTypeIsFloating(dstType)) + if (varTypeIsFloating(srcType) || varTypeIsFloating(dstType)) { // We will reach this path only if morph did not convert the cast to a helper call, // meaning we can perform the cast using SIMD instructions. - // The sequence this creates is simply: - // AVX512DQ.VL.ConvertToVector128Single(Vector128.CreateScalarUnsafe(LONG)).ToScalar() - - NamedIntrinsic intrinsicId = NI_Illegal; - GenTree* srcOp = cast->CastOp(); assert(!cast->gtOverflow()); assert(m_compiler->compIsaSupportedDebugOnly(InstructionSet_AVX512)); - intrinsicId = (dstType == TYP_FLOAT) ? NI_AVX512_ConvertToVector128Single : NI_AVX512_ConvertToVector128Double; + GenTree* srcOp = cast->CastOp(); + GenTree* castResult = nullptr; + LIR::Range castRange = LIR::EmptyRange(); - GenTree* createScalar = m_compiler->gtNewSimdCreateScalarUnsafeNode(TYP_SIMD16, srcOp, srcType, 16); - GenTree* convert = m_compiler->gtNewSimdHWIntrinsicNode(TYP_SIMD16, createScalar, intrinsicId, srcType, 16); - GenTree* toScalar = m_compiler->gtNewSimdToScalarNode(dstType, convert, dstType, 16); + // This creates the equivalent of the following C# code: + // var srcVec = Vector128.CreateScalarUnsafe(castOp); - Range().InsertAfter(cast, createScalar, convert, toScalar); - Range().Remove(cast); + GenTree* srcVector = m_compiler->gtNewSimdCreateScalarUnsafeNode(TYP_SIMD16, srcOp, srcType, 16); + castRange.InsertAtEnd(srcVector); - if (createScalar->IsCnsVec()) + if (srcVector->IsCnsVec()) { Range().Remove(srcOp); } + if (varTypeIsFloating(dstType)) + { + // long->floating casts don't require any kind of fixup. We simply use the vector + // form of the instructions, because the scalar form is not supported on 32-bit. + + NamedIntrinsic intrinsicId = + (dstType == TYP_FLOAT) ? NI_AVX512_ConvertToVector128Single : NI_AVX512_ConvertToVector128Double; + + castResult = m_compiler->gtNewSimdHWIntrinsicNode(TYP_SIMD16, srcVector, intrinsicId, srcType, 16); + } + else if (m_compiler->compOpportunisticallyDependsOn(InstructionSet_AVX10v2)) + { + // Likewise, the AVX10.2 saturating floating->long instructions give the correct result, + // but we have to use the vector form. + + NamedIntrinsic intrinsicId = (dstType == TYP_ULONG) + ? NI_AVX10v2_ConvertToVectorUInt64WithTruncatedSaturation + : NI_AVX10v2_ConvertToVectorInt64WithTruncatedSaturation; + + castResult = m_compiler->gtNewSimdHWIntrinsicNode(TYP_SIMD16, srcVector, intrinsicId, srcType, 16); + } + else if (dstType == TYP_ULONG) + { + // AVX-512 unsigned conversion instructions correctly saturate for positive overflow, so + // we only need to fix up negative or NaN values before conversion. + // + // maxs[sd] will take the value from the second operand if the first operand's value is + // NaN, which allows us to fix up both negative and NaN values with a single instruction. + // + // This creates the equivalent of the following C# code: + // var fixupVal = Sse.MaxScalar(srcVec, Vector128.Zero); + // castResult = Avx512DQ.VL.ConvertToVector128UInt64WithTruncation(fixupVal); + + GenTree* zero = m_compiler->gtNewZeroConNode(TYP_SIMD16); + GenTree* fixupVal = + m_compiler->gtNewSimdHWIntrinsicNode(TYP_SIMD16, srcVector, zero, NI_X86Base_MaxScalar, srcType, 16); + + castRange.InsertAtEnd(zero); + castRange.InsertAtEnd(fixupVal); + + castResult = + m_compiler->gtNewSimdHWIntrinsicNode(TYP_SIMD16, fixupVal, + NI_AVX512_ConvertToVector128UInt64WithTruncation, srcType, 16); + } + else + { + assert(dstType == TYP_LONG); + + // We will use the input value multiple times, so we replace it with a lclVar. + LIR::Use srcUse; + LIR::Use::MakeDummyUse(castRange, srcVector, &srcUse); + srcUse.ReplaceWithLclVar(m_compiler); + srcVector = srcUse.Def(); + + // This logic is similar to the floating->long saturating logic in Lowering::LowerCast, + // except that here we must keep everything in SIMD registers. We can also take advantage + // of EVEX masking since the conversion itself requires AVX-512. + // + // We fix up NaN values by masking in zero during conversion. Negative saturation is handled + // correctly by the conversion instructions. Positive saturation is handled after conversion, + // because MaxValue is not precisely representable in the floating format. + // + // This creates roughly the equivalent of the following C# code: + // var nanMask = Avx.CompareScalar(srcVec, srcVec, FloatComparisonMode.OrderedNonSignaling); + // + // var compareMode = FloatComparisonMode.OrderedGreaterThanOrEqualNonSignaling; + // var ovfFloatingValue = Vector128.Create(9223372036854775808.0); + // var ovfMask = Avx.CompareScalar(srcVec, ovfFloatingValue, compareMode); + + GenTree* srcClone = m_compiler->gtClone(srcVector); + GenTree* compareMode = + m_compiler->gtNewIconNode(static_cast(FloatComparisonMode::OrderedNonSignaling)); + GenTree* nanMask = m_compiler->gtNewSimdHWIntrinsicNode(TYP_MASK, srcVector, srcClone, compareMode, + NI_AVX512_CompareScalarMask, srcType, 16); + + castRange.InsertAtEnd(srcClone); + castRange.InsertAtEnd(compareMode); + castRange.InsertAtEnd(nanMask); + + compareMode = m_compiler->gtNewIconNode( + static_cast(FloatComparisonMode::OrderedGreaterThanOrEqualNonSignaling)); + + GenTreeVecCon* ovfFloatingValue = m_compiler->gtNewVconNode(TYP_SIMD16); + ovfFloatingValue->EvaluateBroadcastInPlace(srcType, 9223372036854775808.0); // 2^63 + + srcClone = m_compiler->gtClone(srcVector); + GenTree* ovfMask = m_compiler->gtNewSimdHWIntrinsicNode(TYP_MASK, srcClone, ovfFloatingValue, compareMode, + NI_AVX512_CompareScalarMask, srcType, 16); + + castRange.InsertAtEnd(srcClone); + castRange.InsertAtEnd(ovfFloatingValue); + castRange.InsertAtEnd(compareMode); + castRange.InsertAtEnd(ovfMask); + + // Now we convert, using the masks created above for NaN and positive overflow saturation. + // + // This creates roughly the equivalent of the following C# code: + // var convert = Avx512DQ.VL.ConvertToVector128Int64WithTruncation(srcVec); + // var convertMasked = Avx512F.VL.BlendVariable(Vector128.Zero, convert, nanMask); + // + // var maxLong = Vector128.Create(long.MaxValue); + // castResult = Avx512F.VL.BlendVariable(convertMasked, maxLong, ovfMask); + + GenTree* zero = m_compiler->gtNewZeroConNode(TYP_SIMD16); + + srcClone = m_compiler->gtClone(srcVector); + GenTree* convert = + m_compiler->gtNewSimdHWIntrinsicNode(TYP_SIMD16, srcClone, + NI_AVX512_ConvertToVector128Int64WithTruncation, srcType, 16); + + castRange.InsertAtEnd(zero); + castRange.InsertAtEnd(srcClone); + castRange.InsertAtEnd(convert); + + GenTree* convertMasked = m_compiler->gtNewSimdHWIntrinsicNode(TYP_SIMD16, zero, convert, nanMask, + NI_AVX512_BlendVariableMask, dstType, 16); + + GenTreeVecCon* maxLong = m_compiler->gtNewVconNode(TYP_SIMD16); + maxLong->EvaluateBroadcastInPlace(dstType, INT64_MAX); + + castRange.InsertAtEnd(convertMasked); + castRange.InsertAtEnd(maxLong); + + castResult = m_compiler->gtNewSimdHWIntrinsicNode(TYP_SIMD16, convertMasked, maxLong, ovfMask, + NI_AVX512_BlendVariableMask, dstType, 16); + } + + // Because the results are in a SIMD register, we need to ToScalar() them out. + GenTree* toScalar = m_compiler->gtNewSimdToScalarNode(genActualType(dstType), castResult, dstType, 16); + + castRange.InsertAtEnd(castResult); + castRange.InsertAtEnd(toScalar); + + Range().InsertAfter(cast, std::move(castRange)); + Range().Remove(cast); + if (use.IsDummyUse()) { toScalar->SetUnusedValue(); } use.ReplaceWith(toScalar); - return toScalar->gtNext; + return toScalar; } #endif // FEATURE_HW_INTRINSICS && TARGET_X86 diff --git a/src/coreclr/jit/flowgraph.cpp b/src/coreclr/jit/flowgraph.cpp index d03716102f0ee8..654cb4a75c83f5 100644 --- a/src/coreclr/jit/flowgraph.cpp +++ b/src/coreclr/jit/flowgraph.cpp @@ -1339,12 +1339,8 @@ bool Compiler::fgCastRequiresHelper(var_types fromType, var_types toType, bool o } #if defined(TARGET_X86) || defined(TARGET_ARM) - if (varTypeIsFloating(fromType) && varTypeIsLong(toType)) - { - return true; - } - - if (varTypeIsLong(fromType) && varTypeIsFloating(toType)) + if ((varTypeIsLong(fromType) && varTypeIsFloating(toType)) || + (varTypeIsFloating(fromType) && varTypeIsLong(toType))) { #if defined(TARGET_X86) return !compOpportunisticallyDependsOn(InstructionSet_AVX512); diff --git a/src/coreclr/jit/gentree.cpp b/src/coreclr/jit/gentree.cpp index e9d04fa4ecd156..dd85dcf67691bc 100644 --- a/src/coreclr/jit/gentree.cpp +++ b/src/coreclr/jit/gentree.cpp @@ -6495,7 +6495,38 @@ unsigned Compiler::gtSetEvalOrder(GenTree* tree) { var_types dstType = tree->AsCast()->CastToType(); - if (varTypeIsLong(dstType)) + if (compOpportunisticallyDependsOn(InstructionSet_AVX10v2)) + { +#if defined(TARGET_X86) + if (varTypeIsLong(dstType)) + { + // unsigned: vcvttp*2uqqs xmm0, xmm0 + // vmovq [mem], xmm0 + // + // signed: vcvttp*2qqs xmm0, xmm0 + // vmovq [mem], xmm0 + + costEx = 4 + FLT_IND_COST_EX; // 4 + FLT_IND_COST_EX + costSz = 6 + 6; // 12 + + if (op1Type == TYP_FLOAT) + { + // vector widening float->long instructions take 1 extra cycle + // compared to same-size conversion + costEx += 1; + } + } + else +#endif + { + // unsigned: vcvtts*2usis eax, xmm0 + // signed: vcvtts*2sis eax, xmm0 + + costEx = 7; + costSz = 6; + } + } + else if (varTypeIsLong(dstType)) { #if defined(TARGET_AMD64) if (varTypeIsUnsigned(dstType)) @@ -6543,24 +6574,59 @@ unsigned Compiler::gtSetEvalOrder(GenTree* tree) costSz = 5 + 4 + 10 + 5 + 8 + 4; // 36 } #else - // unsigned: ... - // call CORINFO_HELP_DBL2ULNG - // - // signed: ... - // call CORINFO_HELP_DBL2ULNG - - costEx = 5 + (3 * IND_COST_EX); // CALL - costSz = 5; // 5 + if (compOpportunisticallyDependsOn(InstructionSet_AVX512)) + { + if (varTypeIsUnsigned(dstType)) + { + // vxorps xmm1, xmm1, xmm1 + // vmaxs* xmm0, xmm0, xmm1 + // vcvttp*2uqq xmm0, xmm0 + // vmovq [mem], xmm0 - level++; + costEx = 1 + 4 + 4 + FLT_IND_COST_EX; // 9 + FLT_IND_COST_EX + costSz = 4 + 4 + 6 + 6; // 20 + } + else + { + // vcmpords* k1, xmm0, xmm0 + // vcmpge_oqs* k2, xmm0, qword ptr [@RWD00] + // vcvttp*2qq xmm0 {k1}{z}, xmm0 + // vpblendmq xmm0 {k2}, xmm0, qword ptr [@RWD08] {1to2} + // vmovq [mem], xmm0 + + costEx = 4 + (4 + FLT_IND_COST_EX) + 4 + (1 + FLT_IND_COST_EX) + + FLT_IND_COST_EX; // 13 + (3 * FLT_IND_COST_EX) + costSz = 7 + 11 + 6 + 10 + 6; // 40 + } - if (op1Type == TYP_FLOAT) + if (op1Type == TYP_FLOAT) + { + // vector widening float->long instructions take 1 extra cycle + // compared to same-size conversion + costEx += 1; + } + } + else { - // vcvtss2sd xmm0, xmm0, xmm0 - // ... + // unsigned: ... + // call CORINFO_HELP_DBL2ULNG + // + // signed: ... + // call CORINFO_HELP_DBL2ULNG + + costEx = 5 + (3 * IND_COST_EX); // CALL + costSz = 5; // 5 + + level++; - costEx += 4; // 4 + CALL - costSz += 4; // 9 + if (op1Type == TYP_FLOAT) + { + // vcvtss2sd xmm0, xmm0, xmm0 + // ... + + costEx += 4; // 4 + CALL + costSz += 4; // 9 + } } #endif } @@ -34999,8 +35065,8 @@ GenTree* Compiler::gtFoldExprHWIntrinsic(GenTreeHWIntrinsic* tree) break; } - bool maskIsZero = false; - bool maskIsAllOnes = false; + bool maskIsZero = false; + bool maskIsAllBitsSet = false; if (op3->IsCnsMsk()) { @@ -35011,7 +35077,7 @@ GenTree* Compiler::gtFoldExprHWIntrinsic(GenTreeHWIntrinsic* tree) GenTreeMskCon* mask = op3->AsMskCon(); uint32_t elemCount = simdSize / genTypeSize(simdBaseType); - maskIsAllOnes = mask->gtSimdMaskVal.GetRawBits() == simdmask_t::GetBitMask(elemCount); + maskIsAllBitsSet = mask->gtSimdMaskVal.GetRawBits() == simdmask_t::GetBitMask(elemCount); } } else @@ -35022,11 +35088,11 @@ GenTree* Compiler::gtFoldExprHWIntrinsic(GenTreeHWIntrinsic* tree) if (!maskIsZero) { - maskIsAllOnes = op3->IsVectorAllBitsSet(); + maskIsAllBitsSet = op3->IsVectorAllBitsSet(); } } - if (maskIsAllOnes) + if (maskIsAllBitsSet) { if ((op1->gtFlags & GTF_SIDE_EFFECT) != 0) { diff --git a/src/coreclr/jit/hwintrinsiclistxarch.h b/src/coreclr/jit/hwintrinsiclistxarch.h index af6e0c1f86f162..de2322872c60fc 100644 --- a/src/coreclr/jit/hwintrinsiclistxarch.h +++ b/src/coreclr/jit/hwintrinsiclistxarch.h @@ -1252,6 +1252,7 @@ HARDWARE_INTRINSIC(AVX512, CompareNotGreaterThanOrEqualMask, HARDWARE_INTRINSIC(AVX512, CompareNotLessThanMask, -1, 2, {INS_vpcmpb, INS_vpcmpub, INS_vpcmpw, INS_vpcmpuw, INS_vpcmpd, INS_vpcmpud, INS_vpcmpq, INS_vpcmpuq, INS_vcmpps, INS_vcmppd}, 1, 4, HW_Category_SimpleSIMD, HW_Flag_ReturnsPerElementMask) HARDWARE_INTRINSIC(AVX512, CompareNotLessThanOrEqualMask, -1, 2, {INS_vpcmpb, INS_vpcmpub, INS_vpcmpw, INS_vpcmpuw, INS_vpcmpd, INS_vpcmpud, INS_vpcmpq, INS_vpcmpuq, INS_vcmpps, INS_vcmppd}, 1, 4, HW_Category_SimpleSIMD, HW_Flag_ReturnsPerElementMask) HARDWARE_INTRINSIC(AVX512, CompareOrderedMask, -1, 2, {INS_invalid, INS_invalid, INS_invalid, INS_invalid, INS_invalid, INS_invalid, INS_invalid, INS_invalid, INS_vcmpps, INS_vcmppd}, -1, 4, HW_Category_SimpleSIMD, HW_Flag_ReturnsPerElementMask) +HARDWARE_INTRINSIC(AVX512, CompareScalarMask, 16, 3, {INS_invalid, INS_invalid, INS_invalid, INS_invalid, INS_invalid, INS_invalid, INS_invalid, INS_invalid, INS_vcmpss, INS_vcmpsd}, -1, 4, HW_Category_IMM, HW_Flag_ReturnsPerElementMask) HARDWARE_INTRINSIC(AVX512, CompareUnorderedMask, -1, 2, {INS_invalid, INS_invalid, INS_invalid, INS_invalid, INS_invalid, INS_invalid, INS_invalid, INS_invalid, INS_vcmpps, INS_vcmppd}, -1, 4, HW_Category_SimpleSIMD, HW_Flag_ReturnsPerElementMask) HARDWARE_INTRINSIC(AVX512, CompressMask, -1, 3, {INS_vpcompressb, INS_vpcompressb, INS_vpcompressw, INS_vpcompressw, INS_vpcompressd, INS_vpcompressd, INS_vpcompressq, INS_vpcompressq, INS_vcompressps, INS_vcompresspd}, 3, 3, HW_Category_SimpleSIMD, HW_Flag_BaseTypeFromSecondArg) HARDWARE_INTRINSIC(AVX512, CompressStoreMask, -1, 3, {INS_vpcompressb, INS_vpcompressb, INS_vpcompressw, INS_vpcompressw, INS_vpcompressd, INS_vpcompressd, INS_vpcompressq, INS_vpcompressq, INS_vcompressps, INS_vcompresspd}, -1, -1, HW_Category_MemoryStore, HW_Flag_NoFlag) diff --git a/src/coreclr/jit/hwintrinsicxarch.cpp b/src/coreclr/jit/hwintrinsicxarch.cpp index 9c57cded52e184..ef52964d430eac 100644 --- a/src/coreclr/jit/hwintrinsicxarch.cpp +++ b/src/coreclr/jit/hwintrinsicxarch.cpp @@ -498,6 +498,7 @@ int HWIntrinsicInfo::lookupImmUpperBound(NamedIntrinsic id) case NI_AVX_CompareScalar: case NI_AVX512_Compare: case NI_AVX512_CompareMask: + case NI_AVX512_CompareScalarMask: case NI_AVX10v2_MinMaxScalar: case NI_AVX10v2_MinMax: { diff --git a/src/coreclr/jit/lowerxarch.cpp b/src/coreclr/jit/lowerxarch.cpp index 493fa1e1a06acf..050cf1d0824f33 100644 --- a/src/coreclr/jit/lowerxarch.cpp +++ b/src/coreclr/jit/lowerxarch.cpp @@ -1140,8 +1140,6 @@ void Lowering::LowerCast(GenTree* tree) // This creates the equivalent of the following C# code: // var wrapVal = Sse.SubtractScalar(srcVec, ovfFloatingValue); - NamedIntrinsic subtractIntrinsic = NI_X86Base_SubtractScalar; - // We're going to use ovfFloatingValue twice, so replace the constant with a lclVar. castRange.InsertAtEnd(ovfFloatingValue); @@ -1173,7 +1171,7 @@ void Lowering::LowerCast(GenTree* tree) } GenTree* wrapVal = m_compiler->gtNewSimdHWIntrinsicNode(TYP_SIMD16, floorVal, ovfFloatingValue, - subtractIntrinsic, srcType, 16); + NI_X86Base_SubtractScalar, srcType, 16); castRange.InsertAtEnd(wrapVal); ovfFloatingValue = m_compiler->gtClone(ovfFloatingValue); @@ -10516,6 +10514,7 @@ void Lowering::ContainCheckHWIntrinsic(GenTreeHWIntrinsic* node) case NI_AVX512_Shuffle: case NI_AVX512_SumAbsoluteDifferencesInBlock32: case NI_AVX512_CompareMask: + case NI_AVX512_CompareScalarMask: case NI_AES_CarrylessMultiply: case NI_AES_V256_CarrylessMultiply: case NI_AES_V512_CarrylessMultiply: From 8cfe75361e1d4f1fff1354020baca950ddde3aff Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Wed, 29 Apr 2026 16:23:55 +0200 Subject: [PATCH 019/115] [wasm][coreclr] Update the ConvertToInteger tests active issue (#127220) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jan Kotas --- .../System.Runtime.Tests/System/DoubleTests.GenericMath.cs | 2 +- .../System.Runtime.Tests/System/SingleTests.GenericMath.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.GenericMath.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.GenericMath.cs index a0b3afb2382c80..153d4fdf22a729 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.GenericMath.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.GenericMath.cs @@ -346,7 +346,7 @@ public static void op_InequalityTest() [Fact] [SkipOnMono("https://github.com/dotnet/runtime/issues/100368")] - [ActiveIssue("https://github.com/dotnet/runtime/issues/123011", typeof(PlatformDetection), nameof(PlatformDetection.IsBrowser), nameof(PlatformDetection.IsCoreCLR))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/116823", typeof(PlatformDetection), nameof(PlatformDetection.IsCoreClrInterpreter))] public static void ConvertToIntegerTest() { // Signed Values diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/SingleTests.GenericMath.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/SingleTests.GenericMath.cs index 75f6ba30363b36..407cb6c41164ad 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/SingleTests.GenericMath.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/SingleTests.GenericMath.cs @@ -346,7 +346,7 @@ public static void op_InequalityTest() [Fact] [SkipOnMono("https://github.com/dotnet/runtime/issues/100368")] - [ActiveIssue("https://github.com/dotnet/runtime/issues/123011", typeof(PlatformDetection), nameof(PlatformDetection.IsBrowser), nameof(PlatformDetection.IsCoreCLR))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/116823", typeof(PlatformDetection), nameof(PlatformDetection.IsCoreClrInterpreter))] public static void ConvertToIntegerTest() { // Signed Values From 13ab2739db33c2c0efdd425ac1f9db1b201ac25f Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Wed, 29 Apr 2026 08:14:48 -0700 Subject: [PATCH 020/115] X25519DiffieHellmanOpenSsl This is the `X25519DiffieHellmanOpenSsl` implementation that works with EVP_PKEY handles. --- .../Interop.EvpPkey.X25519.cs | 25 +++ .../ref/System.Security.Cryptography.cs | 16 ++ .../src/Resources/Strings.resx | 3 + .../src/System.Security.Cryptography.csproj | 6 + ...5519DiffieHellmanImplementation.OpenSsl.cs | 1 + ...X25519DiffieHellmanOpenSsl.NotSupported.cs | 46 +++++ .../X25519DiffieHellmanOpenSsl.OpenSsl.cs | 106 +++++++++++ .../X25519DiffieHellmanOpenSsl.cs | 57 ++++++ .../System.Security.Cryptography.Tests.csproj | 5 + ...9DiffieHellmanOpenSslTests.NotSupported.cs | 16 ++ .../X25519DiffieHellmanOpenSslTests.Unix.cs | 166 ++++++++++++++++++ .../entrypoints.c | 1 + .../pal_evp_pkey_x25519.c | 26 +++ .../pal_evp_pkey_x25519.h | 7 + 14 files changed, 481 insertions(+) create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanOpenSsl.NotSupported.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanOpenSsl.OpenSsl.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanOpenSsl.cs create mode 100644 src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanOpenSslTests.NotSupported.cs create mode 100644 src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanOpenSslTests.Unix.cs diff --git a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EvpPkey.X25519.cs b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EvpPkey.X25519.cs index cd5640e407b267..3bf002aa12158e 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EvpPkey.X25519.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EvpPkey.X25519.cs @@ -30,6 +30,11 @@ private static partial int X25519ExportPublicKey( [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_X25519GenerateKey")] private static partial SafeEvpPKeyHandle CryptoNative_X25519GenerateKey(); + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_X25519IsValidHandle")] + private static partial int CryptoNative_X25519IsValidHandle( + SafeEvpPKeyHandle key, + [MarshalAs(UnmanagedType.Bool)] out bool hasPrivateKey); + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_X25519ImportPrivateKey")] private static partial SafeEvpPKeyHandle X25519ImportPrivateKey(ReadOnlySpan source, int sourceLength); @@ -88,6 +93,26 @@ internal static SafeEvpPKeyHandle X25519GenerateKey() return key; } + internal static bool X25519IsValidHandle(SafeEvpPKeyHandle key, out bool hasPrivateKey) + { + const int Success = 1; + const int Fail = 0; + + int ret = CryptoNative_X25519IsValidHandle(key, out hasPrivateKey); + + switch (ret) + { + case Success: + return true; + case Fail: + hasPrivateKey = false; + return false; + default: + Debug.Fail($"{nameof(CryptoNative_X25519IsValidHandle)} returned '{ret}' unexpectedly."); + throw CreateOpenSslCryptographicException(); + } + } + internal static SafeEvpPKeyHandle X25519ImportPrivateKey(ReadOnlySpan source) { SafeEvpPKeyHandle key = X25519ImportPrivateKey(source, source.Length); diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index 7a2269429c83e7..0da5a776b5ee8d 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -3493,6 +3493,22 @@ public void ExportPublicKey(System.Span destination) { } protected abstract bool TryExportPkcs8PrivateKeyCore(System.Span destination, out int bytesWritten); public bool TryExportSubjectPublicKeyInfo(System.Span destination, out int bytesWritten) { throw null; } } + public sealed partial class X25519DiffieHellmanOpenSsl : System.Security.Cryptography.X25519DiffieHellman + { + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("android")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("osx")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("windows")] + public X25519DiffieHellmanOpenSsl(System.Security.Cryptography.SafeEvpPKeyHandle pkeyHandle) { } + protected override void DeriveRawSecretAgreementCore(System.Security.Cryptography.X25519DiffieHellman otherParty, System.Span destination) { } + protected override void Dispose(bool disposing) { } + public System.Security.Cryptography.SafeEvpPKeyHandle DuplicateKeyHandle() { throw null; } + protected override void ExportPrivateKeyCore(System.Span destination) { } + protected override void ExportPublicKeyCore(System.Span destination) { } + protected override bool TryExportPkcs8PrivateKeyCore(System.Span destination, out int bytesWritten) { throw null; } + } } namespace System.Security.Cryptography.X509Certificates { diff --git a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx index de2d5b8cbbe5cd..4086d30920f071 100644 --- a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx @@ -543,6 +543,9 @@ The specified EVP_PKEY handle is not a known ML-KEM algorithm. + + The specified EVP_PKEY handle is not an X25519 Diffie-Hellman key. + The current instance does not contain a decapsulation key. diff --git a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj index d99843f361213c..e4bdb62e1c1dd0 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -688,6 +688,7 @@ + @@ -886,6 +887,7 @@ + @@ -1130,6 +1132,7 @@ + System\Security\Cryptography\X509Certificates\Asn1\DistributionPointAsn.xml @@ -1299,6 +1302,7 @@ + @@ -1437,6 +1441,7 @@ + @@ -2031,6 +2036,7 @@ + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.OpenSsl.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.OpenSsl.cs index 7d144ec72411c4..762b748ff1bf02 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.OpenSsl.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.OpenSsl.cs @@ -11,6 +11,7 @@ internal sealed class X25519DiffieHellmanImplementation : X25519DiffieHellman private readonly bool _hasPrivate; internal static new bool IsSupported { get; } = Interop.Crypto.X25519Available(); + internal SafeEvpPKeyHandle Key => _key; private X25519DiffieHellmanImplementation(SafeEvpPKeyHandle key, bool hasPrivate) { diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanOpenSsl.NotSupported.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanOpenSsl.NotSupported.cs new file mode 100644 index 00000000000000..7099fb5f9d7050 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanOpenSsl.NotSupported.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace System.Security.Cryptography +{ + public sealed partial class X25519DiffieHellmanOpenSsl + { + public partial X25519DiffieHellmanOpenSsl(SafeEvpPKeyHandle pkeyHandle) + { + _ = pkeyHandle; + throw new PlatformNotSupportedException(); + } + + public partial SafeEvpPKeyHandle DuplicateKeyHandle() + { + Debug.Fail("Caller should have checked platform availability."); + throw new PlatformNotSupportedException(); + } + + protected override void DeriveRawSecretAgreementCore(X25519DiffieHellman otherParty, Span destination) + { + Debug.Fail("Caller should have checked platform availability."); + throw new PlatformNotSupportedException(); + } + + protected override void ExportPrivateKeyCore(Span destination) + { + Debug.Fail("Caller should have checked platform availability."); + throw new PlatformNotSupportedException(); + } + + protected override void ExportPublicKeyCore(Span destination) + { + Debug.Fail("Caller should have checked platform availability."); + throw new PlatformNotSupportedException(); + } + + protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) + { + Debug.Fail("Caller should have checked platform availability."); + throw new PlatformNotSupportedException(); + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanOpenSsl.OpenSsl.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanOpenSsl.OpenSsl.cs new file mode 100644 index 00000000000000..c7f767052305c7 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanOpenSsl.OpenSsl.cs @@ -0,0 +1,106 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace System.Security.Cryptography +{ + public sealed partial class X25519DiffieHellmanOpenSsl + { + private readonly SafeEvpPKeyHandle _key; + private readonly bool _hasPrivate; + + public partial X25519DiffieHellmanOpenSsl(SafeEvpPKeyHandle pkeyHandle) + { + ArgumentNullException.ThrowIfNull(pkeyHandle); + + if (pkeyHandle.IsInvalid) + { + throw new ArgumentException(SR.Cryptography_OpenInvalidHandle, nameof(pkeyHandle)); + } + + _key = pkeyHandle.DuplicateHandle(); + bool isValid = Interop.Crypto.X25519IsValidHandle(_key, out _hasPrivate); + + if (!isValid) + { + _key.Dispose(); + throw new CryptographicException(SR.Cryptography_X25519InvalidAlgorithmHandle); + } + } + + public partial SafeEvpPKeyHandle DuplicateKeyHandle() + { + ThrowIfDisposed(); + return _key.DuplicateHandle(); + } + + protected override void DeriveRawSecretAgreementCore(X25519DiffieHellman otherParty, Span destination) + { + Debug.Assert(destination.Length == SecretAgreementSizeInBytes); + ThrowIfPrivateNeeded(); + + int written; + + if (otherParty is X25519DiffieHellmanOpenSsl x25519OpenSsl) + { + written = Interop.Crypto.EvpPKeyDeriveSecretAgreement(_key, x25519OpenSsl._key, destination); + } + else if (otherParty is X25519DiffieHellmanImplementation x25519Impl) + { + written = Interop.Crypto.EvpPKeyDeriveSecretAgreement(_key, x25519Impl.Key, destination); + } + else + { + Span publicKey = stackalloc byte[PublicKeySizeInBytes]; + otherParty.ExportPublicKey(publicKey); + + using (SafeEvpPKeyHandle peerKeyHandle = Interop.Crypto.X25519ImportPublicKey(publicKey)) + { + written = Interop.Crypto.EvpPKeyDeriveSecretAgreement(_key, peerKeyHandle, destination); + } + } + + if (written != SecretAgreementSizeInBytes) + { + Debug.Fail($"{nameof(Interop.Crypto.EvpPKeyDeriveSecretAgreement)} wrote an unexpected number of bytes: {written}."); + throw new CryptographicException(); + } + } + + protected override void ExportPrivateKeyCore(Span destination) + { + Debug.Assert(destination.Length == PrivateKeySizeInBytes); + ThrowIfPrivateNeeded(); + Interop.Crypto.X25519ExportPrivateKey(_key, destination); + } + + protected override void ExportPublicKeyCore(Span destination) + { + Debug.Assert(destination.Length == PublicKeySizeInBytes); + Interop.Crypto.X25519ExportPublicKey(_key, destination); + } + + protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) + { + ThrowIfPrivateNeeded(); + return TryExportPkcs8PrivateKeyImpl(destination, out bytesWritten); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _key.Dispose(); + } + + base.Dispose(disposing); + } + + private void ThrowIfPrivateNeeded() + { + if (!_hasPrivate) + throw new CryptographicException(SR.Cryptography_CSP_NoPrivateKey); + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanOpenSsl.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanOpenSsl.cs new file mode 100644 index 00000000000000..5f085579c81e80 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanOpenSsl.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.Versioning; + +namespace System.Security.Cryptography +{ + /// + /// Represents an X25519 Diffie-Hellman key backed by OpenSSL. + /// + /// + /// + /// This algorithm is specified by RFC 7748. + /// + /// + /// Developers are encouraged to program against the X25519DiffieHellman base class, + /// rather than any specific derived class. + /// The derived classes are intended for interop with the underlying system + /// cryptographic libraries. + /// + /// + public sealed partial class X25519DiffieHellmanOpenSsl : X25519DiffieHellman + { + /// + /// Initializes a new instance of the class from an existing OpenSSL key + /// represented as an EVP_PKEY*. + /// + /// + /// The OpenSSL EVP_PKEY* value to use as the key, represented as a . + /// + /// + /// is . + /// + /// + /// The handle in is not recognized as an X25519 Diffie-Hellman key. + /// -or- + /// An error occurred while creating the algorithm instance. + /// + /// + /// The handle in is already disposed. + /// + [UnsupportedOSPlatform("android")] + [UnsupportedOSPlatform("browser")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("osx")] + [UnsupportedOSPlatform("tvos")] + [UnsupportedOSPlatform("windows")] + public partial X25519DiffieHellmanOpenSsl(SafeEvpPKeyHandle pkeyHandle); + + /// + /// Gets a representation of the cryptographic key. + /// + /// A representation of the cryptographic key. + /// The object has already been disposed. + public partial SafeEvpPKeyHandle DuplicateKeyHandle(); + } +} diff --git a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj index cac44ca8e544a2..396242cc2d09ad 100644 --- a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj +++ b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj @@ -715,6 +715,7 @@ + @@ -785,6 +786,8 @@ Link="Common\Interop\Unix\System.Security.Cryptography.Native\Interop.EvpPkey.MLDsa.cs" /> + + @@ -842,6 +846,7 @@ + diff --git a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanOpenSslTests.NotSupported.cs b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanOpenSslTests.NotSupported.cs new file mode 100644 index 00000000000000..cab9747f763efd --- /dev/null +++ b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanOpenSslTests.NotSupported.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Security.Cryptography.Tests +{ + public sealed class X25519DiffieHellmanOpenSslTests + { + [Fact] + public static void X25519DiffieHellmanOpenSsl_NotSupportedOnNonUnixPlatforms() + { + Assert.Throws(() => new X25519DiffieHellmanOpenSsl(new SafeEvpPKeyHandle())); + } + } +} diff --git a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanOpenSslTests.Unix.cs b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanOpenSslTests.Unix.cs new file mode 100644 index 00000000000000..cf92d66f33c22e --- /dev/null +++ b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanOpenSslTests.Unix.cs @@ -0,0 +1,166 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Security.Cryptography.Tests +{ + [ConditionalClass(typeof(X25519DiffieHellman), nameof(X25519DiffieHellman.IsSupported))] + public sealed class X25519DiffieHellmanOpenSslTests : X25519DiffieHellmanBaseTests + { + public override X25519DiffieHellmanOpenSsl GenerateKey() + { + using SafeEvpPKeyHandle key = Interop.Crypto.X25519GenerateKey(); + return new X25519DiffieHellmanOpenSsl(key); + } + + public override X25519DiffieHellmanOpenSsl ImportPrivateKey(ReadOnlySpan source) + { + using SafeEvpPKeyHandle key = Interop.Crypto.X25519ImportPrivateKey(source); + return new X25519DiffieHellmanOpenSsl(key); + } + + public override X25519DiffieHellmanOpenSsl ImportPublicKey(ReadOnlySpan source) + { + using SafeEvpPKeyHandle key = Interop.Crypto.X25519ImportPublicKey(source); + return new X25519DiffieHellmanOpenSsl(key); + } + + [Fact] + public void X25519DiffieHellmanOpenSsl_Ctor_ArgValidation() + { + AssertExtensions.Throws("pkeyHandle", static () => new X25519DiffieHellmanOpenSsl(null)); + } + + [Fact] + public void X25519DiffieHellmanOpenSsl_Ctor_InvalidHandle() + { + AssertExtensions.Throws("pkeyHandle", static () => new X25519DiffieHellmanOpenSsl(new SafeEvpPKeyHandle())); + } + + [Fact] + public void X25519DiffieHellmanOpenSsl_WrongAlgorithm() + { + using RSAOpenSsl rsa = new(); + using SafeEvpPKeyHandle rsaHandle = rsa.DuplicateKeyHandle(); + + Assert.Throws(() => new X25519DiffieHellmanOpenSsl(rsaHandle)); + } + + [Fact] + public void X25519DiffieHellmanOpenSsl_DuplicateKeyHandle() + { + using SafeEvpPKeyHandle key = Interop.Crypto.X25519GenerateKey(); + using X25519DiffieHellmanOpenSsl xdh = new(key); + SafeEvpPKeyHandle secondKey; + + using (secondKey = xdh.DuplicateKeyHandle()) + { + Assert.False(secondKey.IsInvalid, nameof(secondKey.IsInvalid)); + } + + Assert.True(secondKey.IsInvalid, nameof(secondKey.IsInvalid)); + Assert.False(key.IsInvalid, nameof(key.IsInvalid)); + Assert.NotNull(xdh.ExportPrivateKey()); + } + + [Fact] + public void DeriveRawSecretAgreement_OpenSslKeyWithCreateKey_Symmetric() + { + using X25519DiffieHellmanOpenSsl openSslKey = GenerateKey(); + using X25519DiffieHellman createKey = X25519DiffieHellman.GenerateKey(); + + byte[] secret1 = openSslKey.DeriveRawSecretAgreement(createKey); + byte[] secret2 = createKey.DeriveRawSecretAgreement(openSslKey); + + AssertExtensions.SequenceEqual(secret1, secret2); + } + + [Fact] + public void DeriveRawSecretAgreement_OpenSslKeyWithCreateKey_ExactBuffers() + { + using X25519DiffieHellmanOpenSsl openSslKey = GenerateKey(); + using X25519DiffieHellman createKey = X25519DiffieHellman.GenerateKey(); + + byte[] secret1 = new byte[X25519DiffieHellman.SecretAgreementSizeInBytes]; + byte[] secret2 = new byte[X25519DiffieHellman.SecretAgreementSizeInBytes]; + openSslKey.DeriveRawSecretAgreement(createKey, secret1); + createKey.DeriveRawSecretAgreement(openSslKey, secret2); + + AssertExtensions.SequenceEqual(secret1, secret2); + } + + [Fact] + public void DeriveRawSecretAgreement_OpenSslKeyWithImportedCreateKey() + { + using X25519DiffieHellmanOpenSsl openSslKey = GenerateKey(); + + byte[] openSslPublicKey = openSslKey.ExportPublicKey(); + using X25519DiffieHellman createKeyFromPublic = X25519DiffieHellman.ImportPublicKey(openSslPublicKey); + + using X25519DiffieHellman createKey = X25519DiffieHellman.GenerateKey(); + + byte[] secret1 = createKey.DeriveRawSecretAgreement(openSslKey); + byte[] secret2 = createKey.DeriveRawSecretAgreement(createKeyFromPublic); + + AssertExtensions.SequenceEqual(secret1, secret2); + } + + [Fact] + public void DeriveRawSecretAgreement_CreateKeyWithOpenSslPublicOnly() + { + using X25519DiffieHellman createKey = X25519DiffieHellman.GenerateKey(); + + byte[] createPublicKey = createKey.ExportPublicKey(); + using X25519DiffieHellmanOpenSsl openSslPublicOnly = ImportPublicKey(createPublicKey); + + using X25519DiffieHellmanOpenSsl openSslPrivate = GenerateKey(); + + byte[] secret1 = openSslPrivate.DeriveRawSecretAgreement(createKey); + byte[] secret2 = openSslPrivate.DeriveRawSecretAgreement(openSslPublicOnly); + + AssertExtensions.SequenceEqual(secret1, secret2); + } + + [Fact] + public void DeriveRawSecretAgreement_PrivateKeyRoundtripBetweenOpenSslAndCreate() + { + using X25519DiffieHellmanOpenSsl openSslKey = GenerateKey(); + byte[] privateKey = openSslKey.ExportPrivateKey(); + + using X25519DiffieHellman createFromPrivate = X25519DiffieHellman.ImportPrivateKey(privateKey); + using X25519DiffieHellman peer = X25519DiffieHellman.GenerateKey(); + + byte[] secret1 = openSslKey.DeriveRawSecretAgreement(peer); + byte[] secret2 = createFromPrivate.DeriveRawSecretAgreement(peer); + + AssertExtensions.SequenceEqual(secret1, secret2); + } + + [Fact] + public void DeriveRawSecretAgreement_Pkcs8RoundtripBetweenOpenSslAndCreate() + { + using X25519DiffieHellmanOpenSsl openSslKey = GenerateKey(); + byte[] pkcs8 = openSslKey.ExportPkcs8PrivateKey(); + + using X25519DiffieHellman createFromPkcs8 = X25519DiffieHellman.ImportPkcs8PrivateKey(pkcs8); + using X25519DiffieHellman peer = X25519DiffieHellman.GenerateKey(); + + byte[] secret1 = openSslKey.DeriveRawSecretAgreement(peer); + byte[] secret2 = createFromPkcs8.DeriveRawSecretAgreement(peer); + + AssertExtensions.SequenceEqual(secret1, secret2); + } + + [Fact] + public void ExportPublicKey_ConsistentBetweenOpenSslAndCreate() + { + using X25519DiffieHellmanOpenSsl openSslKey = GenerateKey(); + byte[] privateKey = openSslKey.ExportPrivateKey(); + + using X25519DiffieHellman createKey = X25519DiffieHellman.ImportPrivateKey(privateKey); + + AssertExtensions.SequenceEqual(openSslKey.ExportPublicKey(), createKey.ExportPublicKey()); + } + } +} diff --git a/src/native/libs/System.Security.Cryptography.Native/entrypoints.c b/src/native/libs/System.Security.Cryptography.Native/entrypoints.c index 87a567832bfad0..59b0a6207f815b 100644 --- a/src/native/libs/System.Security.Cryptography.Native/entrypoints.c +++ b/src/native/libs/System.Security.Cryptography.Native/entrypoints.c @@ -427,6 +427,7 @@ static const Entry s_cryptoNative[] = DllImportEntry(CryptoNative_X25519GenerateKey) DllImportEntry(CryptoNative_X25519ImportPrivateKey) DllImportEntry(CryptoNative_X25519ImportPublicKey) + DllImportEntry(CryptoNative_X25519IsValidHandle) DllImportEntry(CryptoNative_X509DecodeOcspToExpiration) DllImportEntry(CryptoNative_X509Duplicate) DllImportEntry(CryptoNative_SslGet0AlpnSelected) diff --git a/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.c b/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.c index 43c4a5ae0c8a4e..66ab4e4533e30f 100644 --- a/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.c +++ b/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.c @@ -119,3 +119,29 @@ EVP_PKEY* CryptoNative_X25519ImportPublicKey(const uint8_t* source, int32_t sour source, Int32ToSizeT(sourceLength)); } + +int32_t CryptoNative_X25519IsValidHandle(const EVP_PKEY* key, int32_t* hasPrivateKey) +{ + assert(key != NULL && hasPrivateKey != NULL); + ERR_clear_error(); + + *hasPrivateKey = 0; + + if (EVP_PKEY_get_base_id(key) != EVP_PKEY_X25519) + { + return 0; + } + + size_t privateKeyLength = 0; + + if (EVP_PKEY_get_raw_private_key(key, NULL, &privateKeyLength) == 1) + { + *hasPrivateKey = 1; + } + else + { + ERR_clear_error(); + } + + return 1; +} diff --git a/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.h b/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.h index 10ce3831db2282..53ec6ee9a3d51d 100644 --- a/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.h +++ b/src/native/libs/System.Security.Cryptography.Native/pal_evp_pkey_x25519.h @@ -46,3 +46,10 @@ Generates a new X25519 key pair and returns it as an EVP_PKEY. Returns the new EVP_PKEY on success, NULL on failure. */ PALEXPORT EVP_PKEY* CryptoNative_X25519GenerateKey(void); + +/* +Determines if an EVP_PKEY is an X25519 key, and whether it contains private key material. + +Returns 1 if the key is an X25519 key, 0 otherwise. +*/ +PALEXPORT int32_t CryptoNative_X25519IsValidHandle(const EVP_PKEY* key, int32_t* hasPrivateKey); From 209f871f0fe3b53e52e80a7db1df21fbb8dd4c1a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:26:10 -0700 Subject: [PATCH 021/115] Exclude DeadThreads test from GC stress compatible set (#127478) ## Description `DeadThreads` creates threads at a very high rate, making it unsuitable for GC stress runs where frequent GCs slow execution enough to cause OOM or excessive paging. ### Changes - **`src/tests/baseservices/threading/DeadThreads/DeadThreads.csproj`**: Added `true` to opt the test out of GC stress (GCStress3/GCStressC) runs. ## AI Generated Content Notice > [!NOTE] > This PR description was generated by GitHub Copilot. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: VSadov <8218165+VSadov@users.noreply.github.com> --- .../baseservices/threading/DeadThreads/DeadThreads.csproj | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tests/baseservices/threading/DeadThreads/DeadThreads.csproj b/src/tests/baseservices/threading/DeadThreads/DeadThreads.csproj index 684e6842b12e48..c2df2f8a3ba6a5 100644 --- a/src/tests/baseservices/threading/DeadThreads/DeadThreads.csproj +++ b/src/tests/baseservices/threading/DeadThreads/DeadThreads.csproj @@ -1,7 +1,9 @@ - + true + + true From cc5d5894bb4bd7c536e5eb74cca214eade93288b Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Wed, 29 Apr 2026 13:40:31 -0400 Subject: [PATCH 022/115] [cDAC] Implement EnumTypeDefs, EnumMethods, GetExportedTypeProps, FindExportedTypeByName (#127471) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > [!NOTE] > This PR was AI/Copilot-generated. ## Summary Implements 4 IMetaDataImport/IMetaDataAssemblyImport methods that were previously legacy-only stubs in `MetaDataImportImpl`. These APIs are used by an internal debugger and need cDAC implementations for no-fallback mode. Also adds comprehensive DEBUG parity validation across all cDAC-implemented methods and improves code quality. ## Changes ### New cDAC implementations in `MetaDataImportImpl.cs` | Method | Interface | Description | |--------|-----------|-------------| | `EnumTypeDefs` | IMetaDataImport | Enumerates all TypeDef tokens via `MetadataReader.TypeDefinitions` | | `EnumMethods` | IMetaDataImport | Enumerates methods of a TypeDef via `TypeDefinition.GetMethods()` | | `GetExportedTypeProps` | IMetaDataAssemblyImport | Returns exported type name (namespace.name), implementation token, TypeDef ID, and flags | | `FindExportedTypeByName` | IMetaDataAssemblyImport | Finds an exported type by full name, with nested type support via enclosing type token | All implementations follow the established patterns in MetaDataImportImpl: - try/catch with HResult return - `FillEnum` infrastructure for enum methods - `CopyStringToBuffer` with truncation → `CLDB_S_TRUNCATION` - `CLDB_E_RECORD_NOTFOUND` for missing records - `#if DEBUG` validation against legacy DAC ### Comprehensive DEBUG parity validation Added or improved `#if DEBUG` validation blocks for **all** cDAC-implemented methods: - **3 enum methods** (`EnumInterfaceImpls`, `EnumFields`, `EnumGenericParams`) — added missing DEBUG blocks that enumerate via the legacy DAC and compare token lists - **9 Get methods** — added string content validation (not just length) by passing `stackalloc` name buffers to the legacy DAC and comparing the actual strings - **2 Get methods** (`GetGenericParamProps`, `GetParamProps`) — added name length + string validation that was previously missing entirely - All enum DEBUG blocks follow the SOSDacImpl pattern: placed **after** the catch block, not inside the try ### Code quality improvements - **`EcmaMetadataUtils.GetRowId`** made public; added `private static int GetRID(uint token)` helper in `MetaDataImportImpl` to replace all raw `& 0x00FFFFFF` RID mask usage - **Named CLDB HRESULT constants** (`CldbHResults.CLDB_S_TRUNCATION`, etc.) used in tests instead of magic hex literals ### Dump tests in `MetaDataImportDumpTests.cs` - `EnumTypeDefs_MatchesMetadataReader` — enumerates TypeDefs via cDAC and compares against `MetadataReader.TypeDefinitions` - `EnumMethods_MatchesMetadataReader` — enumerates methods per TypeDef and compares against `MetadataReader` ### Unit tests in `MetaDataImportImplTests.cs` - 11 new unit tests covering: basic enumeration, pagination, per-TypeDef method enumeration, global methods, empty method lists, exported type properties, nested exported types, truncation, find by name, nested find, and not-found cases - Updated `NotImplementedMethods_ReturnENotImpl` to remove `EnumTypeDefs` (now implemented) - Added exported type entries to shared test metadata builder ## Testing - **1856 unit tests** pass (all cDAC tests) - **5 dump tests** pass (MetaDataImport-specific) - Build succeeds with 0 errors, 0 warnings --------- Co-authored-by: Max Charlamb Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../EcmaMetadataUtils.cs | 2 +- .../MetaDataImportImpl.cs | 447 ++++++++++++++++-- .../DumpTests/MetaDataImportDumpTests.cs | 80 ++++ .../cdac/tests/MetaDataImportImplTests.cs | 232 ++++++++- 4 files changed, 710 insertions(+), 51 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/EcmaMetadataUtils.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/EcmaMetadataUtils.cs index 75657a4bb4d2f6..6ecbff7791bfad 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/EcmaMetadataUtils.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/EcmaMetadataUtils.cs @@ -10,7 +10,7 @@ public static class EcmaMetadataUtils internal const int RowIdBitCount = 24; internal const uint RIDMask = (1 << RowIdBitCount) - 1; - internal static uint GetRowId(uint token) => token & RIDMask; + public static uint GetRowId(uint token) => token & RIDMask; internal static uint MakeToken(uint rid, uint table) => rid | (table << RowIdBitCount); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/MetaDataImportImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/MetaDataImportImpl.cs index 2a2711751d6efa..2ee6c19c075907 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/MetaDataImportImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/MetaDataImportImpl.cs @@ -23,6 +23,8 @@ internal sealed unsafe partial class MetaDataImportImpl : ICustomQueryInterface, private Dictionary? _interfaceImplToTypeDef; private Dictionary? _paramToMethod; + private static int GetRID(uint token) => (int)EcmaMetadataUtils.GetRowId(token); + // Tracks GCHandle values allocated by AllocEnum so that CountEnum, ResetEnum, // and CloseEnum can distinguish cDAC-created enum handles from legacy HENUMInternal*. // ConcurrentDictionary is used because COM objects may be called from multiple threads. @@ -156,11 +158,55 @@ int IMetaDataImport.ResetEnum(nint hEnum, uint ulPos) } int IMetaDataImport.EnumTypeDefs(nint* phEnum, uint* rTypeDefs, uint cMax, uint* pcTypeDefs) - => _legacyImport is not null ? _legacyImport.EnumTypeDefs(phEnum, rTypeDefs, cMax, pcTypeDefs) : HResults.E_NOTIMPL; + { + int hr = HResults.S_OK; + List? tokens = null; + try + { + if (phEnum is not null && *phEnum != 0) + { + hr = FillEnum(phEnum, GetEnum(*phEnum).Tokens, rTypeDefs, cMax, pcTypeDefs); + } + else + { + tokens = new(); + foreach (TypeDefinitionHandle h in _reader.TypeDefinitions) + tokens.Add((uint)MetadataTokens.GetToken(h)); + hr = FillEnum(phEnum, tokens, rTypeDefs, cMax, pcTypeDefs); + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (tokens is not null && _legacyImport is not null) + { + nint hEnumLocal = 0; + List legacyTokens = new(); + uint* buf = stackalloc uint[64]; + while (true) + { + uint count; + int hrLegacy = _legacyImport.EnumTypeDefs(&hEnumLocal, buf, 64, &count); + if (hrLegacy < 0 || count == 0) break; + for (uint i = 0; i < count; i++) + legacyTokens.Add(buf[i]); + } + _legacyImport.CloseEnum(hEnumLocal); + Debug.Assert(tokens.Count == legacyTokens.Count, $"EnumTypeDefs count mismatch: cDAC={tokens.Count}, DAC={legacyTokens.Count}"); + for (int i = 0; i < Math.Min(tokens.Count, legacyTokens.Count); i++) + Debug.Assert(tokens[i] == legacyTokens[i], $"EnumTypeDefs token mismatch at [{i}]: cDAC=0x{tokens[i]:X}, DAC=0x{legacyTokens[i]:X}"); + } +#endif + return hr; + } int IMetaDataImport.EnumInterfaceImpls(nint* phEnum, uint td, uint* rImpls, uint cMax, uint* pcImpls) { int hr = HResults.S_OK; + List? tokens = null; try { if (phEnum is not null && *phEnum != 0) @@ -169,9 +215,9 @@ int IMetaDataImport.EnumInterfaceImpls(nint* phEnum, uint td, uint* rImpls, uint } else { - TypeDefinitionHandle typeHandle = MetadataTokens.TypeDefinitionHandle((int)(td & 0x00FFFFFF)); + TypeDefinitionHandle typeHandle = MetadataTokens.TypeDefinitionHandle(GetRID(td)); TypeDefinition typeDef = _reader.GetTypeDefinition(typeHandle); - List tokens = new(); + tokens = new(); foreach (InterfaceImplementationHandle h in typeDef.GetInterfaceImplementations()) tokens.Add((uint)MetadataTokens.GetToken(h)); hr = FillEnum(phEnum, tokens, rImpls, cMax, pcImpls); @@ -182,6 +228,26 @@ int IMetaDataImport.EnumInterfaceImpls(nint* phEnum, uint td, uint* rImpls, uint hr = ex.HResult; } +#if DEBUG + if (tokens is not null && _legacyImport is not null) + { + nint hEnumLocal = 0; + List legacyTokens = new(); + uint* buf = stackalloc uint[64]; + while (true) + { + uint count; + int hrLegacy = _legacyImport.EnumInterfaceImpls(&hEnumLocal, td, buf, 64, &count); + if (hrLegacy < 0 || count == 0) break; + for (uint i = 0; i < count; i++) + legacyTokens.Add(buf[i]); + } + _legacyImport.CloseEnum(hEnumLocal); + Debug.Assert(tokens.Count == legacyTokens.Count, $"EnumInterfaceImpls count mismatch for 0x{td:X}: cDAC={tokens.Count}, DAC={legacyTokens.Count}"); + for (int i = 0; i < Math.Min(tokens.Count, legacyTokens.Count); i++) + Debug.Assert(tokens[i] == legacyTokens[i], $"EnumInterfaceImpls token mismatch at [{i}] for 0x{td:X}: cDAC=0x{tokens[i]:X}, DAC=0x{legacyTokens[i]:X}"); + } +#endif return hr; } @@ -192,11 +258,57 @@ int IMetaDataImport.EnumMembers(nint* phEnum, uint cl, uint* rMembers, uint cMax => _legacyImport is not null ? _legacyImport.EnumMembers(phEnum, cl, rMembers, cMax, pcTokens) : HResults.E_NOTIMPL; int IMetaDataImport.EnumMethods(nint* phEnum, uint cl, uint* rMethods, uint cMax, uint* pcTokens) - => _legacyImport is not null ? _legacyImport.EnumMethods(phEnum, cl, rMethods, cMax, pcTokens) : HResults.E_NOTIMPL; + { + int hr = HResults.S_OK; + List? tokens = null; + try + { + if (phEnum is not null && *phEnum != 0) + { + hr = FillEnum(phEnum, GetEnum(*phEnum).Tokens, rMethods, cMax, pcTokens); + } + else + { + TypeDefinitionHandle typeHandle = MetadataTokens.TypeDefinitionHandle(GetRID(cl)); + TypeDefinition typeDef = _reader.GetTypeDefinition(typeHandle); + tokens = new(); + foreach (MethodDefinitionHandle h in typeDef.GetMethods()) + tokens.Add((uint)MetadataTokens.GetToken(h)); + hr = FillEnum(phEnum, tokens, rMethods, cMax, pcTokens); + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (tokens is not null && _legacyImport is not null) + { + nint hEnumLocal = 0; + List legacyTokens = new(); + uint* buf = stackalloc uint[64]; + while (true) + { + uint count; + int hrLegacy = _legacyImport.EnumMethods(&hEnumLocal, cl, buf, 64, &count); + if (hrLegacy < 0 || count == 0) break; + for (uint i = 0; i < count; i++) + legacyTokens.Add(buf[i]); + } + _legacyImport.CloseEnum(hEnumLocal); + Debug.Assert(tokens.Count == legacyTokens.Count, $"EnumMethods count mismatch for 0x{cl:X}: cDAC={tokens.Count}, DAC={legacyTokens.Count}"); + for (int i = 0; i < Math.Min(tokens.Count, legacyTokens.Count); i++) + Debug.Assert(tokens[i] == legacyTokens[i], $"EnumMethods token mismatch at [{i}] for 0x{cl:X}: cDAC=0x{tokens[i]:X}, DAC=0x{legacyTokens[i]:X}"); + } +#endif + return hr; + } int IMetaDataImport.EnumFields(nint* phEnum, uint cl, uint* rFields, uint cMax, uint* pcTokens) { int hr = HResults.S_OK; + List? tokens = null; try { if (phEnum is not null && *phEnum != 0) @@ -205,9 +317,9 @@ int IMetaDataImport.EnumFields(nint* phEnum, uint cl, uint* rFields, uint cMax, } else { - TypeDefinitionHandle typeHandle = MetadataTokens.TypeDefinitionHandle((int)(cl & 0x00FFFFFF)); + TypeDefinitionHandle typeHandle = MetadataTokens.TypeDefinitionHandle(GetRID(cl)); TypeDefinition typeDef = _reader.GetTypeDefinition(typeHandle); - List tokens = new(); + tokens = new(); foreach (FieldDefinitionHandle h in typeDef.GetFields()) tokens.Add((uint)MetadataTokens.GetToken(h)); hr = FillEnum(phEnum, tokens, rFields, cMax, pcTokens); @@ -218,6 +330,26 @@ int IMetaDataImport.EnumFields(nint* phEnum, uint cl, uint* rFields, uint cMax, hr = ex.HResult; } +#if DEBUG + if (tokens is not null && _legacyImport is not null) + { + nint hEnumLocal = 0; + List legacyTokens = new(); + uint* buf = stackalloc uint[64]; + while (true) + { + uint count; + int hrLegacy = _legacyImport.EnumFields(&hEnumLocal, cl, buf, 64, &count); + if (hrLegacy < 0 || count == 0) break; + for (uint i = 0; i < count; i++) + legacyTokens.Add(buf[i]); + } + _legacyImport.CloseEnum(hEnumLocal); + Debug.Assert(tokens.Count == legacyTokens.Count, $"EnumFields count mismatch for 0x{cl:X}: cDAC={tokens.Count}, DAC={legacyTokens.Count}"); + for (int i = 0; i < Math.Min(tokens.Count, legacyTokens.Count); i++) + Debug.Assert(tokens[i] == legacyTokens[i], $"EnumFields token mismatch at [{i}] for 0x{cl:X}: cDAC=0x{tokens[i]:X}, DAC=0x{legacyTokens[i]:X}"); + } +#endif return hr; } @@ -227,6 +359,7 @@ int IMetaDataImport.EnumCustomAttributes(nint* phEnum, uint tk, uint tkType, uin int IMetaDataImport2.EnumGenericParams(nint* phEnum, uint tk, uint* rGenericParams, uint cMax, uint* pcGenericParams) { int hr = HResults.S_OK; + List? tokens = null; try { if (phEnum is not null && *phEnum != 0) @@ -247,7 +380,7 @@ int IMetaDataImport2.EnumGenericParams(nint* phEnum, uint tk, uint* rGenericPara throw new ArgumentException(null, nameof(tk)); } - List tokens = new(); + tokens = new(); foreach (GenericParameterHandle h in genericParams) tokens.Add((uint)MetadataTokens.GetToken(h)); hr = FillEnum(phEnum, tokens, rGenericParams, cMax, pcGenericParams); @@ -258,6 +391,26 @@ int IMetaDataImport2.EnumGenericParams(nint* phEnum, uint tk, uint* rGenericPara hr = ex.HResult; } +#if DEBUG + if (tokens is not null && _legacyImport2 is not null) + { + nint hEnumLocal = 0; + List legacyTokens = new(); + uint* buf = stackalloc uint[64]; + while (true) + { + uint count; + int hrLegacy = _legacyImport2.EnumGenericParams(&hEnumLocal, tk, buf, 64, &count); + if (hrLegacy < 0 || count == 0) break; + for (uint i = 0; i < count; i++) + legacyTokens.Add(buf[i]); + } + _legacyImport2.CloseEnum(hEnumLocal); + Debug.Assert(tokens.Count == legacyTokens.Count, $"EnumGenericParams count mismatch for 0x{tk:X}: cDAC={tokens.Count}, DAC={legacyTokens.Count}"); + for (int i = 0; i < Math.Min(tokens.Count, legacyTokens.Count); i++) + Debug.Assert(tokens[i] == legacyTokens[i], $"EnumGenericParams token mismatch at [{i}] for 0x{tk:X}: cDAC=0x{tokens[i]:X}, DAC=0x{legacyTokens[i]:X}"); + } +#endif return hr; } @@ -266,7 +419,7 @@ int IMetaDataImport.GetTypeDefProps(uint td, char* szTypeDef, uint cchTypeDef, u int hr = HResults.S_OK; try { - TypeDefinitionHandle typeHandle = MetadataTokens.TypeDefinitionHandle((int)(td & 0x00FFFFFF)); + TypeDefinitionHandle typeHandle = MetadataTokens.TypeDefinitionHandle(GetRID(td)); TypeDefinition typeDef = _reader.GetTypeDefinition(typeHandle); string fullName = GetTypeDefFullName(typeDef); @@ -292,7 +445,8 @@ int IMetaDataImport.GetTypeDefProps(uint td, char* szTypeDef, uint cchTypeDef, u if (_legacyImport is not null) { uint flagsLocal = 0, extendsLocal = 0, pchLocal = 0; - int hrLegacy = _legacyImport.GetTypeDefProps(td, null, 0, &pchLocal, &flagsLocal, &extendsLocal); + char* szLocal = stackalloc char[(int)cchTypeDef]; + int hrLegacy = _legacyImport.GetTypeDefProps(td, szLocal, cchTypeDef, &pchLocal, &flagsLocal, &extendsLocal); Debug.ValidateHResult(hr, hrLegacy); if (hr >= 0 && hrLegacy >= 0) { @@ -302,6 +456,12 @@ int IMetaDataImport.GetTypeDefProps(uint td, char* szTypeDef, uint cchTypeDef, u Debug.Assert(*ptkExtends == extendsLocal, $"Extends mismatch: cDAC=0x{*ptkExtends:X}, DAC=0x{extendsLocal:X}"); if (pchTypeDef is not null) Debug.Assert(*pchTypeDef == pchLocal, $"Name length mismatch: cDAC={*pchTypeDef}, DAC={pchLocal}"); + if (szTypeDef is not null && cchTypeDef > 0) + { + string cdacName = new string(szTypeDef); + string dacName = new string(szLocal); + Debug.Assert(cdacName == dacName, $"TypeDef name mismatch: cDAC='{cdacName}', DAC='{dacName}'"); + } } } #endif @@ -313,7 +473,7 @@ int IMetaDataImport.GetTypeRefProps(uint tr, uint* ptkResolutionScope, char* szN int hr = HResults.S_OK; try { - TypeReferenceHandle refHandle = MetadataTokens.TypeReferenceHandle((int)(tr & 0x00FFFFFF)); + TypeReferenceHandle refHandle = MetadataTokens.TypeReferenceHandle(GetRID(tr)); TypeReference typeRef = _reader.GetTypeReference(refHandle); string fullName = GetTypeRefFullName(typeRef); @@ -336,7 +496,8 @@ int IMetaDataImport.GetTypeRefProps(uint tr, uint* ptkResolutionScope, char* szN if (_legacyImport is not null) { uint scopeLocal = 0, pchLocal = 0; - int hrLegacy = _legacyImport.GetTypeRefProps(tr, &scopeLocal, null, 0, &pchLocal); + char* szLocal = stackalloc char[(int)cchName]; + int hrLegacy = _legacyImport.GetTypeRefProps(tr, &scopeLocal, szLocal, cchName, &pchLocal); Debug.ValidateHResult(hr, hrLegacy); if (hr >= 0 && hrLegacy >= 0) { @@ -344,6 +505,12 @@ int IMetaDataImport.GetTypeRefProps(uint tr, uint* ptkResolutionScope, char* szN Debug.Assert(*ptkResolutionScope == scopeLocal, $"ResolutionScope mismatch: cDAC=0x{*ptkResolutionScope:X}, DAC=0x{scopeLocal:X}"); if (pchName is not null) Debug.Assert(*pchName == pchLocal, $"Name length mismatch: cDAC={*pchName}, DAC={pchLocal}"); + if (szName is not null && cchName > 0) + { + string cdacName = new string(szName); + string dacName = new string(szLocal); + Debug.Assert(cdacName == dacName, $"TypeRef name mismatch: cDAC='{cdacName}', DAC='{dacName}'"); + } } } #endif @@ -356,7 +523,7 @@ int IMetaDataImport.GetMethodProps(uint mb, uint* pClass, char* szMethod, uint c int hr = HResults.S_OK; try { - MethodDefinitionHandle methodHandle = MetadataTokens.MethodDefinitionHandle((int)(mb & 0x00FFFFFF)); + MethodDefinitionHandle methodHandle = MetadataTokens.MethodDefinitionHandle(GetRID(mb)); MethodDefinition methodDef = _reader.GetMethodDefinition(methodHandle); string name = _reader.GetString(methodDef.Name); @@ -396,7 +563,8 @@ int IMetaDataImport.GetMethodProps(uint mb, uint* pClass, char* szMethod, uint c { uint classLocal = 0, attrLocal = 0, rvaLocal = 0, implLocal = 0, pchLocal = 0, cbSigLocal = 0; byte* sigLocal = null; - int hrLegacy = _legacyImport.GetMethodProps(mb, &classLocal, null, 0, &pchLocal, &attrLocal, &sigLocal, &cbSigLocal, &rvaLocal, &implLocal); + char* szLocal = stackalloc char[(int)cchMethod]; + int hrLegacy = _legacyImport.GetMethodProps(mb, &classLocal, szLocal, cchMethod, &pchLocal, &attrLocal, &sigLocal, &cbSigLocal, &rvaLocal, &implLocal); Debug.ValidateHResult(hr, hrLegacy); if (hr >= 0 && hrLegacy >= 0) { @@ -406,6 +574,12 @@ int IMetaDataImport.GetMethodProps(uint mb, uint* pClass, char* szMethod, uint c Debug.Assert(*pdwAttr == attrLocal, $"Attr mismatch: cDAC=0x{*pdwAttr:X}, DAC=0x{attrLocal:X}"); if (pchMethod is not null) Debug.Assert(*pchMethod == pchLocal, $"Name length mismatch: cDAC={*pchMethod}, DAC={pchLocal}"); + if (szMethod is not null && cchMethod > 0) + { + string cdacName = new string(szMethod); + string dacName = new string(szLocal); + Debug.Assert(cdacName == dacName, $"Method name mismatch: cDAC='{cdacName}', DAC='{dacName}'"); + } if (pulCodeRVA is not null) Debug.Assert(*pulCodeRVA == rvaLocal, $"RVA mismatch: cDAC=0x{*pulCodeRVA:X}, DAC=0x{rvaLocal:X}"); if (pdwImplFlags is not null) @@ -427,7 +601,7 @@ int IMetaDataImport.GetFieldProps(uint mb, uint* pClass, char* szField, uint cch int hr = HResults.S_OK; try { - FieldDefinitionHandle fieldHandle = MetadataTokens.FieldDefinitionHandle((int)(mb & 0x00FFFFFF)); + FieldDefinitionHandle fieldHandle = MetadataTokens.FieldDefinitionHandle(GetRID(mb)); FieldDefinition fieldDef = _reader.GetFieldDefinition(fieldHandle); string name = _reader.GetString(fieldDef.Name); @@ -485,7 +659,8 @@ int IMetaDataImport.GetFieldProps(uint mb, uint* pClass, char* szField, uint cch uint classLocal = 0, attrLocal = 0, pchLocal = 0, cbSigLocal = 0, cpTypeLocal = 0, cchValueLocal = 0; byte* sigLocal = null; void* valueLocal = null; - int hrLegacy = _legacyImport.GetFieldProps(mb, &classLocal, null, 0, &pchLocal, &attrLocal, &sigLocal, &cbSigLocal, &cpTypeLocal, &valueLocal, &cchValueLocal); + char* szLocal = stackalloc char[(int)cchField]; + int hrLegacy = _legacyImport.GetFieldProps(mb, &classLocal, szLocal, cchField, &pchLocal, &attrLocal, &sigLocal, &cbSigLocal, &cpTypeLocal, &valueLocal, &cchValueLocal); Debug.ValidateHResult(hr, hrLegacy); if (hr >= 0 && hrLegacy >= 0) { @@ -495,6 +670,12 @@ int IMetaDataImport.GetFieldProps(uint mb, uint* pClass, char* szField, uint cch Debug.Assert(*pdwAttr == attrLocal, $"Attr mismatch: cDAC=0x{*pdwAttr:X}, DAC=0x{attrLocal:X}"); if (pchField is not null) Debug.Assert(*pchField == pchLocal, $"Name length mismatch: cDAC={*pchField}, DAC={pchLocal}"); + if (szField is not null && cchField > 0) + { + string cdacName = new string(szField); + string dacName = new string(szLocal); + Debug.Assert(cdacName == dacName, $"Field name mismatch: cDAC='{cdacName}', DAC='{dacName}'"); + } if (pdwCPlusTypeFlag is not null) Debug.Assert(*pdwCPlusTypeFlag == cpTypeLocal, $"CPlusTypeFlag mismatch: cDAC=0x{*pdwCPlusTypeFlag:X}, DAC=0x{cpTypeLocal:X}"); if (ppvSigBlob is not null) @@ -546,13 +727,13 @@ int IMetaDataImport.GetInterfaceImplProps(uint iiImpl, uint* pClass, uint* ptkIf int hr = HResults.S_OK; try { - InterfaceImplementationHandle implHandle = MetadataTokens.InterfaceImplementationHandle((int)(iiImpl & 0x00FFFFFF)); + InterfaceImplementationHandle implHandle = MetadataTokens.InterfaceImplementationHandle(GetRID(iiImpl)); InterfaceImplementation impl = _reader.GetInterfaceImplementation(implHandle); if (pClass is not null) { _interfaceImplToTypeDef ??= BuildInterfaceImplLookup(); - *pClass = _interfaceImplToTypeDef.TryGetValue((int)(iiImpl & 0x00FFFFFF), out uint ownerToken) + *pClass = _interfaceImplToTypeDef.TryGetValue(GetRID(iiImpl), out uint ownerToken) ? ownerToken : 0; } @@ -589,7 +770,7 @@ int IMetaDataImport.GetNestedClassProps(uint tdNestedClass, uint* ptdEnclosingCl int hr = HResults.S_OK; try { - TypeDefinitionHandle typeHandle = MetadataTokens.TypeDefinitionHandle((int)(tdNestedClass & 0x00FFFFFF)); + TypeDefinitionHandle typeHandle = MetadataTokens.TypeDefinitionHandle(GetRID(tdNestedClass)); TypeDefinition typeDef = _reader.GetTypeDefinition(typeHandle); TypeDefinitionHandle declaringType = typeDef.GetDeclaringType(); @@ -622,7 +803,7 @@ int IMetaDataImport2.GetGenericParamProps(uint gp, uint* pulParamSeq, uint* pdwP int hr = HResults.S_OK; try { - GenericParameterHandle gpHandle = MetadataTokens.GenericParameterHandle((int)(gp & 0x00FFFFFF)); + GenericParameterHandle gpHandle = MetadataTokens.GenericParameterHandle(GetRID(gp)); GenericParameter genericParam = _reader.GetGenericParameter(gpHandle); if (pulParamSeq is not null) @@ -650,8 +831,9 @@ int IMetaDataImport2.GetGenericParamProps(uint gp, uint* pulParamSeq, uint* pdwP #if DEBUG if (_legacyImport2 is not null) { - uint seqLocal = 0, flagsLocal = 0, ownerLocal = 0; - int hrLegacy = _legacyImport2.GetGenericParamProps(gp, &seqLocal, &flagsLocal, &ownerLocal, null, null, 0, null); + uint seqLocal = 0, flagsLocal = 0, ownerLocal = 0, pchLocal = 0; + char* szLocal = stackalloc char[(int)cchName]; + int hrLegacy = _legacyImport2.GetGenericParamProps(gp, &seqLocal, &flagsLocal, &ownerLocal, null, szLocal, cchName, &pchLocal); Debug.ValidateHResult(hr, hrLegacy); if (hr >= 0 && hrLegacy >= 0) { @@ -661,6 +843,14 @@ int IMetaDataImport2.GetGenericParamProps(uint gp, uint* pulParamSeq, uint* pdwP Debug.Assert(*pdwParamFlags == flagsLocal, $"ParamFlags mismatch: cDAC=0x{*pdwParamFlags:X}, DAC=0x{flagsLocal:X}"); if (ptOwner is not null) Debug.Assert(*ptOwner == ownerLocal, $"Owner mismatch: cDAC=0x{*ptOwner:X}, DAC=0x{ownerLocal:X}"); + if (pchName is not null) + Debug.Assert(*pchName == pchLocal, $"Name length mismatch: cDAC={*pchName}, DAC={pchLocal}"); + if (wzname is not null && cchName > 0) + { + string cdacName = new string(wzname); + string dacName = new string(szLocal); + Debug.Assert(cdacName == dacName, $"GenericParam name mismatch: cDAC='{cdacName}', DAC='{dacName}'"); + } } } #endif @@ -675,7 +865,7 @@ int IMetaDataImport.GetRVA(uint tk, uint* pulCodeRVA, uint* pdwImplFlags) uint tableIndex = tk >> 24; if (tableIndex == 0x06) // MethodDef { - MethodDefinitionHandle methodHandle = MetadataTokens.MethodDefinitionHandle((int)(tk & 0x00FFFFFF)); + MethodDefinitionHandle methodHandle = MetadataTokens.MethodDefinitionHandle(GetRID(tk)); MethodDefinition methodDef = _reader.GetMethodDefinition(methodHandle); if (pulCodeRVA is not null) *pulCodeRVA = (uint)methodDef.RelativeVirtualAddress; @@ -684,7 +874,7 @@ int IMetaDataImport.GetRVA(uint tk, uint* pulCodeRVA, uint* pdwImplFlags) } else if (tableIndex == 0x04) // FieldDef { - FieldDefinitionHandle fieldHandle = MetadataTokens.FieldDefinitionHandle((int)(tk & 0x00FFFFFF)); + FieldDefinitionHandle fieldHandle = MetadataTokens.FieldDefinitionHandle(GetRID(tk)); FieldDefinition fieldDef = _reader.GetFieldDefinition(fieldHandle); if (pulCodeRVA is not null) *pulCodeRVA = (uint)fieldDef.GetRelativeVirtualAddress(); @@ -724,7 +914,7 @@ int IMetaDataImport.GetSigFromToken(uint mdSig, byte** ppvSig, uint* pcbSig) int hr = HResults.S_OK; try { - StandaloneSignatureHandle sigHandle = MetadataTokens.StandaloneSignatureHandle((int)(mdSig & 0x00FFFFFF)); + StandaloneSignatureHandle sigHandle = MetadataTokens.StandaloneSignatureHandle(GetRID(mdSig)); StandaloneSignature sig = _reader.GetStandaloneSignature(sigHandle); BlobReader blobReader = _reader.GetBlobReader(sig.Signature); @@ -818,7 +1008,7 @@ int IMetaDataImport.GetCustomAttributeByName(uint tkObj, char* szName, void** pp int IMetaDataImport.IsValidToken(uint tk) { - int rid = (int)(tk & 0x00FFFFFF); + int rid = GetRID(tk); int tokenType = (int)(tk >> 24); if (rid == 0) @@ -966,7 +1156,7 @@ int IMetaDataImport.GetMemberRefProps(uint mr, uint* ptk, char* szMember, uint c int hr = HResults.S_OK; try { - MemberReferenceHandle refHandle = MetadataTokens.MemberReferenceHandle((int)(mr & 0x00FFFFFF)); + MemberReferenceHandle refHandle = MetadataTokens.MemberReferenceHandle(GetRID(mr)); MemberReference memberRef = _reader.GetMemberReference(refHandle); string name = _reader.GetString(memberRef.Name); @@ -996,7 +1186,8 @@ int IMetaDataImport.GetMemberRefProps(uint mr, uint* ptk, char* szMember, uint c { uint tkLocal = 0, pchLocal = 0, cbSigLocal = 0; byte* sigLocal = null; - int hrLegacy = _legacyImport.GetMemberRefProps(mr, &tkLocal, null, 0, &pchLocal, &sigLocal, &cbSigLocal); + char* szLocal = stackalloc char[(int)cchMember]; + int hrLegacy = _legacyImport.GetMemberRefProps(mr, &tkLocal, szLocal, cchMember, &pchLocal, &sigLocal, &cbSigLocal); Debug.ValidateHResult(hr, hrLegacy); if (hr >= 0 && hrLegacy >= 0) { @@ -1004,6 +1195,12 @@ int IMetaDataImport.GetMemberRefProps(uint mr, uint* ptk, char* szMember, uint c Debug.Assert(*ptk == tkLocal, $"Parent mismatch: cDAC=0x{*ptk:X}, DAC=0x{tkLocal:X}"); if (pchMember is not null) Debug.Assert(*pchMember == pchLocal, $"Name length mismatch: cDAC={*pchMember}, DAC={pchLocal}"); + if (szMember is not null && cchMember > 0) + { + string cdacName = new string(szMember); + string dacName = new string(szLocal); + Debug.Assert(cdacName == dacName, $"MemberRef name mismatch: cDAC='{cdacName}', DAC='{dacName}'"); + } if (ppvSigBlob is not null) ValidateBlobsEqual(*ppvSigBlob, pbSig is not null ? *pbSig : cbSigLocal, sigLocal, cbSigLocal, "MemberRefSig"); else if (pbSig is not null) @@ -1036,7 +1233,7 @@ int IMetaDataImport.GetClassLayout(uint td, uint* pdwPackSize, void* rFieldOffse int hr = HResults.S_OK; try { - TypeDefinitionHandle typeHandle = MetadataTokens.TypeDefinitionHandle((int)(td & 0x00FFFFFF)); + TypeDefinitionHandle typeHandle = MetadataTokens.TypeDefinitionHandle(GetRID(td)); TypeDefinition typeDef = _reader.GetTypeDefinition(typeHandle); TypeLayout layout = typeDef.GetLayout(); @@ -1110,7 +1307,7 @@ int IMetaDataImport.GetModuleRefProps(uint mur, char* szName, uint cchName, uint int hr = HResults.S_OK; try { - ModuleReferenceHandle modRefHandle = MetadataTokens.ModuleReferenceHandle((int)(mur & 0x00FFFFFF)); + ModuleReferenceHandle modRefHandle = MetadataTokens.ModuleReferenceHandle(GetRID(mur)); ModuleReference modRef = _reader.GetModuleReference(modRefHandle); string name = _reader.GetString(modRef.Name); @@ -1127,10 +1324,20 @@ int IMetaDataImport.GetModuleRefProps(uint mur, char* szName, uint cchName, uint if (_legacyImport is not null) { uint pchLocal = 0; - int hrLegacy = _legacyImport.GetModuleRefProps(mur, null, 0, &pchLocal); + char* szLocal = stackalloc char[(int)cchName]; + int hrLegacy = _legacyImport.GetModuleRefProps(mur, szLocal, cchName, &pchLocal); Debug.ValidateHResult(hr, hrLegacy); - if (hr >= 0 && hrLegacy >= 0 && pchName is not null) - Debug.Assert(*pchName == pchLocal, $"Name length mismatch: cDAC={*pchName}, DAC={pchLocal}"); + if (hr >= 0 && hrLegacy >= 0) + { + if (pchName is not null) + Debug.Assert(*pchName == pchLocal, $"Name length mismatch: cDAC={*pchName}, DAC={pchLocal}"); + if (szName is not null && cchName > 0) + { + string cdacName = new string(szName); + string dacName = new string(szLocal); + Debug.Assert(cdacName == dacName, $"ModuleRef name mismatch: cDAC='{cdacName}', DAC='{dacName}'"); + } + } } #endif return hr; @@ -1144,7 +1351,7 @@ int IMetaDataImport.GetTypeSpecFromToken(uint typespec, byte** ppvSig, uint* pcb int hr = HResults.S_OK; try { - TypeSpecificationHandle tsHandle = MetadataTokens.TypeSpecificationHandle((int)(typespec & 0x00FFFFFF)); + TypeSpecificationHandle tsHandle = MetadataTokens.TypeSpecificationHandle(GetRID(typespec)); TypeSpecification typeSpec = _reader.GetTypeSpecification(tsHandle); BlobReader blobReader = _reader.GetBlobReader(typeSpec.Signature); @@ -1195,7 +1402,7 @@ int IMetaDataImport.GetUserString(uint stk, char* szString, uint cchString, uint // Using raw bytes avoids potential discrepancies with MetadataReader.GetUserString(). int heapMetadataOffset = _reader.GetHeapMetadataOffset(HeapIndex.UserString); int heapSize = _reader.GetHeapSize(HeapIndex.UserString); - int handleOffset = (int)(stk & 0x00FFFFFF); + int handleOffset = GetRID(stk); byte* heapBase = _reader.MetadataPointer + heapMetadataOffset; int remaining = heapSize - handleOffset; @@ -1240,10 +1447,20 @@ int IMetaDataImport.GetUserString(uint stk, char* szString, uint cchString, uint if (_legacyImport is not null) { uint pchLocal = 0; - int hrLegacy = _legacyImport.GetUserString(stk, null, 0, &pchLocal); + char* szLocal = stackalloc char[(int)cchString]; + int hrLegacy = _legacyImport.GetUserString(stk, szLocal, cchString, &pchLocal); Debug.ValidateHResult(hr, hrLegacy); - if (hr >= 0 && hrLegacy >= 0 && pchString is not null) - Debug.Assert(*pchString == pchLocal, $"String length mismatch: cDAC={*pchString}, DAC={pchLocal}"); + if (hr >= 0 && hrLegacy >= 0) + { + if (pchString is not null) + Debug.Assert(*pchString == pchLocal, $"String length mismatch: cDAC={*pchString}, DAC={pchLocal}"); + if (szString is not null && cchString > 0) + { + string cdacStr = new string(szString); + string dacStr = new string(szLocal); + Debug.Assert(cdacStr == dacStr, $"UserString content mismatch: cDAC='{cdacStr}', DAC='{dacStr}'"); + } + } } #endif return hr; @@ -1270,7 +1487,7 @@ int IMetaDataImport.GetParamForMethodIndex(uint md, uint ulParamSeq, uint* ppd) if (ppd is not null) *ppd = 0; - MethodDefinitionHandle methodHandle = MetadataTokens.MethodDefinitionHandle((int)(md & 0x00FFFFFF)); + MethodDefinitionHandle methodHandle = MetadataTokens.MethodDefinitionHandle(GetRID(md)); MethodDefinition methodDef = _reader.GetMethodDefinition(methodHandle); bool found = false; @@ -1325,7 +1542,7 @@ int IMetaDataImport.GetParamProps(uint tk, uint* pmd, uint* pulSequence, char* s int hr = HResults.S_OK; try { - ParameterHandle paramHandle = MetadataTokens.ParameterHandle((int)(tk & 0x00FFFFFF)); + ParameterHandle paramHandle = MetadataTokens.ParameterHandle(GetRID(tk)); Parameter param = _reader.GetParameter(paramHandle); string name = _reader.GetString(param.Name); @@ -1376,8 +1593,9 @@ int IMetaDataImport.GetParamProps(uint tk, uint* pmd, uint* pulSequence, char* s #if DEBUG if (_legacyImport is not null) { - uint mdLocal = 0, seqLocal = 0, attrLocal = 0; - int hrLegacy = _legacyImport.GetParamProps(tk, &mdLocal, &seqLocal, null, 0, null, &attrLocal, null, null, null); + uint mdLocal = 0, seqLocal = 0, attrLocal = 0, pchLocal = 0; + char* szLocal = stackalloc char[(int)cchName]; + int hrLegacy = _legacyImport.GetParamProps(tk, &mdLocal, &seqLocal, szLocal, cchName, &pchLocal, &attrLocal, null, null, null); Debug.ValidateHResult(hr, hrLegacy); if (hr >= 0 && hrLegacy >= 0) { @@ -1387,6 +1605,14 @@ int IMetaDataImport.GetParamProps(uint tk, uint* pmd, uint* pulSequence, char* s Debug.Assert(*pulSequence == seqLocal, $"Sequence mismatch: cDAC={*pulSequence}, DAC={seqLocal}"); if (pdwAttr is not null) Debug.Assert(*pdwAttr == attrLocal, $"Attr mismatch: cDAC=0x{*pdwAttr:X}, DAC=0x{attrLocal:X}"); + if (pchName is not null) + Debug.Assert(*pchName == pchLocal, $"Name length mismatch: cDAC={*pchName}, DAC={pchLocal}"); + if (szName is not null && cchName > 0) + { + string cdacName = new string(szName); + string dacName = new string(szLocal); + Debug.Assert(cdacName == dacName, $"Param name mismatch: cDAC='{cdacName}', DAC='{dacName}'"); + } } } #endif @@ -1494,12 +1720,19 @@ int IMetaDataAssemblyImport.GetAssemblyProps(uint mda, byte** ppbPublicKey, uint uint pchLocal = 0, hashAlgLocal = 0, flagsLocal = 0, cbPublicKeyLocal = 0; byte* publicKeyLocal = null; ASSEMBLYMETADATA metaLocal = default; - int hrLegacy = _legacyAssemblyImport.GetAssemblyProps(mda, &publicKeyLocal, &cbPublicKeyLocal, &hashAlgLocal, null, 0, &pchLocal, &metaLocal, &flagsLocal); + char* szLocal = stackalloc char[(int)cchName]; + int hrLegacy = _legacyAssemblyImport.GetAssemblyProps(mda, &publicKeyLocal, &cbPublicKeyLocal, &hashAlgLocal, szLocal, cchName, &pchLocal, &metaLocal, &flagsLocal); Debug.ValidateHResult(hr, hrLegacy); if (hr >= 0 && hrLegacy >= 0) { if (pchName is not null) Debug.Assert(*pchName == pchLocal, $"Name length mismatch: cDAC={*pchName}, DAC={pchLocal}"); + if (szName is not null && cchName > 0) + { + string cdacName = new string(szName); + string dacName = new string(szLocal); + Debug.Assert(cdacName == dacName, $"Assembly name mismatch: cDAC='{cdacName}', DAC='{dacName}'"); + } if (pulHashAlgId is not null) Debug.Assert(*pulHashAlgId == hashAlgLocal, $"HashAlgId mismatch: cDAC=0x{*pulHashAlgId:X}, DAC=0x{hashAlgLocal:X}"); if (pdwAssemblyFlags is not null) @@ -1528,7 +1761,7 @@ int IMetaDataAssemblyImport.GetAssemblyRefProps(uint mdar, byte** ppbPublicKeyOr int hr = HResults.S_OK; try { - AssemblyReferenceHandle refHandle = MetadataTokens.AssemblyReferenceHandle((int)(mdar & 0x00FFFFFF)); + AssemblyReferenceHandle refHandle = MetadataTokens.AssemblyReferenceHandle(GetRID(mdar)); AssemblyReference assemblyRef = _reader.GetAssemblyReference(refHandle); string name = _reader.GetString(assemblyRef.Name); @@ -1598,12 +1831,19 @@ int IMetaDataAssemblyImport.GetAssemblyRefProps(uint mdar, byte** ppbPublicKeyOr uint pchLocal = 0, flagsLocal = 0, cbPublicKeyLocal = 0, cbHashLocal = 0; byte* publicKeyLocal = null, hashLocal = null; ASSEMBLYMETADATA metaLocal = default; - int hrLegacy = _legacyAssemblyImport.GetAssemblyRefProps(mdar, &publicKeyLocal, &cbPublicKeyLocal, null, 0, &pchLocal, &metaLocal, &hashLocal, &cbHashLocal, &flagsLocal); + char* szLocal = stackalloc char[(int)cchName]; + int hrLegacy = _legacyAssemblyImport.GetAssemblyRefProps(mdar, &publicKeyLocal, &cbPublicKeyLocal, szLocal, cchName, &pchLocal, &metaLocal, &hashLocal, &cbHashLocal, &flagsLocal); Debug.ValidateHResult(hr, hrLegacy); if (hr >= 0 && hrLegacy >= 0) { if (pchName is not null) Debug.Assert(*pchName == pchLocal, $"Name length mismatch: cDAC={*pchName}, DAC={pchLocal}"); + if (szName is not null && cchName > 0) + { + string cdacName = new string(szName); + string dacName = new string(szLocal); + Debug.Assert(cdacName == dacName, $"AssemblyRef name mismatch: cDAC='{cdacName}', DAC='{dacName}'"); + } if (pdwAssemblyRefFlags is not null) Debug.Assert(*pdwAssemblyRefFlags == flagsLocal, $"Flags mismatch: cDAC=0x{*pdwAssemblyRefFlags:X}, DAC=0x{flagsLocal:X}"); if (ppbPublicKeyOrToken is not null) @@ -1633,7 +1873,69 @@ int IMetaDataAssemblyImport.GetFileProps(uint mdf, char* szName, uint cchName, u int IMetaDataAssemblyImport.GetExportedTypeProps(uint mdct, char* szName, uint cchName, uint* pchName, uint* ptkImplementation, uint* ptkTypeDef, uint* pdwExportedTypeFlags) - => _legacyAssemblyImport is not null ? _legacyAssemblyImport.GetExportedTypeProps(mdct, szName, cchName, pchName, ptkImplementation, ptkTypeDef, pdwExportedTypeFlags) : HResults.E_NOTIMPL; + { + int hr = HResults.S_OK; + try + { + ExportedTypeHandle handle = MetadataTokens.ExportedTypeHandle(GetRID(mdct)); + ExportedType exportedType = _reader.GetExportedType(handle); + + string name = _reader.GetString(exportedType.Name); + string ns = _reader.GetString(exportedType.Namespace); + string fullName = string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; + OutputBufferHelpers.CopyStringToBuffer(szName, cchName, pchName, fullName, out bool truncated); + + if (ptkImplementation is not null) + { + EntityHandle impl = exportedType.Implementation; + *ptkImplementation = impl.IsNil ? 0 : (uint)MetadataTokens.GetToken(impl); + } + + if (ptkTypeDef is not null) + *ptkTypeDef = (uint)exportedType.GetTypeDefinitionId(); + + if (pdwExportedTypeFlags is not null) + *pdwExportedTypeFlags = (uint)exportedType.Attributes; + + hr = truncated ? CldbHResults.CLDB_S_TRUNCATION : HResults.S_OK; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyAssemblyImport is not null) + { + char* szNameLocal = stackalloc char[(int)cchName]; + uint pchNameLocal = 0; + uint tkImplementationLocal = 0; + uint tkTypeDefLocal = 0; + uint dwExportedTypeFlagsLocal = 0; + int hrLegacy = _legacyAssemblyImport.GetExportedTypeProps(mdct, szNameLocal, cchName, &pchNameLocal, + &tkImplementationLocal, &tkTypeDefLocal, &dwExportedTypeFlagsLocal); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0) + { + if (szName is not null && szNameLocal is not null && cchName > 0) + { + string cdacName = new string(szName); + string dacName = new string(szNameLocal); + Debug.Assert(cdacName == dacName, $"ExportedType name mismatch: cDAC='{cdacName}', DAC='{dacName}'"); + } + if (pchName is not null) + Debug.Assert(*pchName == pchNameLocal, $"ExportedType name length mismatch: cDAC={*pchName}, DAC={pchNameLocal}"); + if (ptkImplementation is not null) + Debug.Assert(*ptkImplementation == tkImplementationLocal, $"ExportedType implementation mismatch: cDAC=0x{*ptkImplementation:X}, DAC=0x{tkImplementationLocal:X}"); + if (ptkTypeDef is not null) + Debug.Assert(*ptkTypeDef == tkTypeDefLocal, $"ExportedType typeDef mismatch: cDAC=0x{*ptkTypeDef:X}, DAC=0x{tkTypeDefLocal:X}"); + if (pdwExportedTypeFlags is not null) + Debug.Assert(*pdwExportedTypeFlags == dwExportedTypeFlagsLocal, $"ExportedType flags mismatch: cDAC=0x{*pdwExportedTypeFlags:X}, DAC=0x{dwExportedTypeFlagsLocal:X}"); + } + } +#endif + return hr; + } int IMetaDataAssemblyImport.GetManifestResourceProps(uint mdmr, char* szName, uint cchName, uint* pchName, uint* ptkImplementation, uint* pdwOffset, uint* pdwResourceFlags) @@ -1671,7 +1973,60 @@ int IMetaDataAssemblyImport.GetAssemblyFromScope(uint* ptkAssembly) } int IMetaDataAssemblyImport.FindExportedTypeByName(char* szName, uint mdtExportedType, uint* ptkExportedType) - => _legacyAssemblyImport is not null ? _legacyAssemblyImport.FindExportedTypeByName(szName, mdtExportedType, ptkExportedType) : HResults.E_NOTIMPL; + { + int hr = HResults.S_OK; + try + { + if (ptkExportedType is not null) + *ptkExportedType = 0; + + string targetName = new string(szName); + + bool found = false; + foreach (ExportedTypeHandle eth in _reader.ExportedTypes) + { + ExportedType exportedType = _reader.GetExportedType(eth); + string name = _reader.GetString(exportedType.Name); + string ns = _reader.GetString(exportedType.Namespace); + string fullName = string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; + + if (!string.Equals(fullName, targetName, StringComparison.Ordinal)) + continue; + + if (mdtExportedType != 0) + { + EntityHandle impl = exportedType.Implementation; + if (impl.IsNil || (uint)MetadataTokens.GetToken(impl) != mdtExportedType) + continue; + } + + if (ptkExportedType is not null) + *ptkExportedType = (uint)MetadataTokens.GetToken(eth); + + found = true; + break; + } + + if (!found) + hr = CldbHResults.CLDB_E_RECORD_NOTFOUND; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyAssemblyImport is not null) + { + uint tkExportedTypeLocal = 0; + int hrLegacy = _legacyAssemblyImport.FindExportedTypeByName(szName, mdtExportedType, &tkExportedTypeLocal); + Debug.ValidateHResult(hr, hrLegacy); + if (hr >= 0 && hrLegacy >= 0 && ptkExportedType is not null) + Debug.Assert(*ptkExportedType == tkExportedTypeLocal, $"ExportedType mismatch: cDAC=0x{*ptkExportedType:X}, DAC=0x{tkExportedTypeLocal:X}"); + } +#endif + return hr; + } int IMetaDataAssemblyImport.FindManifestResourceByName(char* szName, uint* ptkManifestResource) => _legacyAssemblyImport is not null ? _legacyAssemblyImport.FindManifestResourceByName(szName, ptkManifestResource) : HResults.E_NOTIMPL; diff --git a/src/native/managed/cdac/tests/DumpTests/MetaDataImportDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/MetaDataImportDumpTests.cs index 715f13c1a6069e..f7092cb537d4e9 100644 --- a/src/native/managed/cdac/tests/DumpTests/MetaDataImportDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/MetaDataImportDumpTests.cs @@ -2,6 +2,7 @@ // 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 Microsoft.Diagnostics.DataContractReader.Contracts; @@ -143,4 +144,83 @@ public unsafe void GetUserString_ReturnsCharCountWithoutNull(TestConfiguration c // Native returns character count WITHOUT null terminator Assert.Equal((uint)expectedCharCount, pchString); } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10")] + public unsafe void EnumTypeDefs_MatchesMetadataReader(TestConfiguration config) + { + InitializeDumpTest(config); + var (reader, mdi) = GetRootModuleImport(); + + List enumTokens = new(); + nint hEnum = 0; + uint* rTokens = stackalloc uint[16]; + + while (true) + { + uint count; + int hr = mdi.EnumTypeDefs(&hEnum, rTokens, 16, &count); + Assert.True(hr >= 0); + if (count == 0) + break; + for (uint i = 0; i < count; i++) + enumTokens.Add(rTokens[i]); + } + + List readerTokens = new(); + foreach (TypeDefinitionHandle h in reader.TypeDefinitions) + readerTokens.Add((uint)MetadataTokens.GetToken(h)); + + Assert.Equal(readerTokens, enumTokens); + mdi.CloseEnum(hEnum); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "Assembly type does not include IsDynamic/IsLoaded fields in .NET 10")] + public unsafe void EnumMethods_MatchesMetadataReader(TestConfiguration config) + { + InitializeDumpTest(config); + var (reader, mdi) = GetRootModuleImport(); + + // Find a type with methods (skip ) + uint classToken = 0; + TypeDefinitionHandle targetType = default; + foreach (TypeDefinitionHandle tdh in reader.TypeDefinitions) + { + if (MetadataTokens.GetRowNumber(tdh) == 1) continue; + TypeDefinition td = reader.GetTypeDefinition(tdh); + if (td.GetMethods().Count > 0) + { + classToken = (uint)MetadataTokens.GetToken(tdh); + targetType = tdh; + break; + } + } + + Assert.True(classToken != 0, "Expected at least one non-global type with methods"); + + List enumTokens = new(); + nint hEnum = 0; + uint* rMethods = stackalloc uint[16]; + + while (true) + { + uint count; + int hr = mdi.EnumMethods(&hEnum, classToken, rMethods, 16, &count); + Assert.True(hr >= 0); + if (count == 0) + break; + for (uint i = 0; i < count; i++) + enumTokens.Add(rMethods[i]); + } + + List readerTokens = new(); + foreach (MethodDefinitionHandle mdh in reader.GetTypeDefinition(targetType).GetMethods()) + readerTokens.Add((uint)MetadataTokens.GetToken(mdh)); + + Assert.Equal(readerTokens, enumTokens); + mdi.CloseEnum(hEnum); + } } diff --git a/src/native/managed/cdac/tests/MetaDataImportImplTests.cs b/src/native/managed/cdac/tests/MetaDataImportImplTests.cs index 9983512542c64f..1db47d31236ed6 100644 --- a/src/native/managed/cdac/tests/MetaDataImportImplTests.cs +++ b/src/native/managed/cdac/tests/MetaDataImportImplTests.cs @@ -146,6 +146,15 @@ private static (MetadataReader reader, MetadataReaderProvider provider) CreateTe mb.AddCustomAttribute(testClassHandle, obsoleteCtor, mb.GetOrAddBlob(new byte[] { 0x01, 0x00, 0x0C, 0x74, 0x65, 0x73, 0x74, 0x20, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x00, 0x00 })); + // Exported types (for GetExportedTypeProps and FindExportedTypeByName testing) + ExportedTypeHandle exportedType1 = mb.AddExportedType( + TypeAttributes.Public, mb.GetOrAddString("TestNamespace"), mb.GetOrAddString("ExportedClass"), + mscorlibRef, 0x02000002); + + mb.AddExportedType( + TypeAttributes.NestedPublic, default, mb.GetOrAddString("NestedExported"), + exportedType1, 0); + // Serialize BlobBuilder metadataBlob = new(); MetadataRootBuilder root = new(mb); @@ -448,7 +457,6 @@ public void NotImplementedMethods_ReturnENotImpl() Assert.Equal(HResults.E_NOTIMPL, wrapper.GetScopeProps(null, 0, null, null)); Assert.Equal(HResults.E_NOTIMPL, wrapper.ResolveTypeRef(0, null, null, null)); - Assert.Equal(HResults.E_NOTIMPL, wrapper.EnumTypeDefs(null, null, 0, null)); Assert.Equal(HResults.E_NOTIMPL, wrapper.EnumTypeRefs(null, null, 0, null)); } @@ -773,7 +781,7 @@ public void GetAssemblyProps_SmallBuffer_Truncates() uint nameLen; int hr = assemblyImport.GetAssemblyProps(0x20000001, null, null, null, nameBuf, 5, &nameLen, null, null); - Assert.Equal(0x00131106, hr); // CLDB_S_TRUNCATION + Assert.Equal(CldbHResults.CLDB_S_TRUNCATION, hr); // Full name is "TestAssembly" (12 chars + null = 13) Assert.Equal(13u, nameLen); // Buffer should contain "Test\0" @@ -806,7 +814,7 @@ public void GetAssemblyProps_InvalidToken_ReturnsRecordNotFound() // Pass an invalid assembly token (wrong RID) int hr = assemblyImport.GetAssemblyProps(0x20000002, null, null, null, null, 0, null, null, null); - Assert.Equal(unchecked((int)0x80131130), hr); // CLDB_E_RECORD_NOTFOUND + Assert.Equal(CldbHResults.CLDB_E_RECORD_NOTFOUND, hr); } } @@ -1015,7 +1023,7 @@ public void QueryInterfaceForIMetaDataImport_ReturnsIMetaDataImport2VtableWithEx var fn = (delegate* unmanaged[Stdcall])enumGenericParams; hr = fn(pImportAgain, &hEnum, 0x02000002, null, 0, &count); // Should succeed (or return no results) without AV - Assert.True(hr >= 0 || hr == unchecked((int)0x80131130)); // S_OK or CLDB_E_RECORD_NOTFOUND + Assert.True(hr >= 0 || hr == CldbHResults.CLDB_E_RECORD_NOTFOUND); } finally { @@ -1099,4 +1107,220 @@ public void ResetEnum_NullHandle_ReturnsOk() int hr = wrapper.ResetEnum(0, 0); Assert.Equal(HResults.S_OK, hr); } + + [Fact] + public void EnumTypeDefs_ReturnsAllTypes() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + // Enumerate all type defs at once (metadata has 4 TypeDefs: , TestClass, NestedType, LayoutClass) + nint hEnum = 0; + uint* tokens = stackalloc uint[10]; + uint count; + + int hr = wrapper.EnumTypeDefs(&hEnum, tokens, 10, &count); + Assert.True(hr >= 0); + Assert.Equal(4u, count); + + // Verify first token is (TypeDef RID 1 = 0x02000001) + Assert.Equal(0x02000001u, tokens[0]); + + wrapper.CloseEnum(hEnum); + } + + [Fact] + public void EnumTypeDefs_Pagination() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + nint hEnum = 0; + uint token; + uint count; + int totalCount = 0; + + // Enumerate one at a time + while (true) + { + int hr = wrapper.EnumTypeDefs(&hEnum, &token, 1, &count); + Assert.True(hr >= 0); + if (count == 0) + break; + totalCount++; + } + + Assert.Equal(4, totalCount); + wrapper.CloseEnum(hEnum); + } + + [Fact] + public void EnumMethods_ReturnsMethodsForType() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + // TestClass (TypeDef RID 2 = 0x02000002) has 1 method: DoWork + nint hEnum = 0; + uint token; + uint count; + + int hr = wrapper.EnumMethods(&hEnum, 0x02000002, &token, 1, &count); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(1u, count); + // DoWork is method row 2 = 0x06000002 + Assert.Equal(0x06000002u, token); + + // No more methods + hr = wrapper.EnumMethods(&hEnum, 0x02000002, &token, 1, &count); + Assert.True(hr >= 0); + Assert.Equal(0u, count); + + wrapper.CloseEnum(hEnum); + } + + [Fact] + public void EnumMethods_ModuleType_ReturnsGlobalMethod() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + // (TypeDef RID 1 = 0x02000001) has 1 method: GlobalHelper + nint hEnum = 0; + uint token; + uint count; + + int hr = wrapper.EnumMethods(&hEnum, 0x02000001, &token, 1, &count); + Assert.Equal(HResults.S_OK, hr); + Assert.Equal(1u, count); + // GlobalHelper is method row 1 = 0x06000001 + Assert.Equal(0x06000001u, token); + + wrapper.CloseEnum(hEnum); + } + + [Fact] + public void EnumMethods_NoMethods_ReturnsFalse() + { + IMetaDataImport2 wrapper = CreateWrapper(); + + // NestedType (TypeDef RID 3 = 0x02000003) has no methods + nint hEnum = 0; + uint token; + uint count; + + int hr = wrapper.EnumMethods(&hEnum, 0x02000003, &token, 1, &count); + Assert.True(hr >= 0); + Assert.Equal(0u, count); + + wrapper.CloseEnum(hEnum); + } + + [Fact] + public void GetExportedTypeProps_ReturnsCorrectProperties() + { + IMetaDataImport2 wrapper = CreateWrapper(); + IMetaDataAssemblyImport assemblyImport = (IMetaDataAssemblyImport)wrapper; + + // ExportedType row 1 = 0x27000001 (TestNamespace.ExportedClass) + char* szName = stackalloc char[256]; + uint pchName; + uint tkImplementation; + uint tkTypeDef; + uint dwFlags; + + int hr = assemblyImport.GetExportedTypeProps(0x27000001, szName, 256, &pchName, + &tkImplementation, &tkTypeDef, &dwFlags); + Assert.True(hr >= 0); + + string name = new string(szName); + Assert.Equal("TestNamespace.ExportedClass", name); + Assert.Equal((uint)TypeAttributes.Public, dwFlags); + Assert.Equal(0x02000002u, tkTypeDef); + // Implementation should be the mscorlib AssemblyRef + Assert.NotEqual(0u, tkImplementation); + } + + [Fact] + public void GetExportedTypeProps_NestedType() + { + IMetaDataImport2 wrapper = CreateWrapper(); + IMetaDataAssemblyImport assemblyImport = (IMetaDataAssemblyImport)wrapper; + + // ExportedType row 2 = 0x27000002 (NestedExported, nested in ExportedClass) + char* szName = stackalloc char[256]; + uint pchName; + uint tkImplementation; + uint tkTypeDef; + uint dwFlags; + + int hr = assemblyImport.GetExportedTypeProps(0x27000002, szName, 256, &pchName, + &tkImplementation, &tkTypeDef, &dwFlags); + Assert.True(hr >= 0); + + string name = new string(szName); + Assert.Equal("NestedExported", name); + Assert.Equal((uint)TypeAttributes.NestedPublic, dwFlags); + // Implementation should point to the parent ExportedType (0x27000001) + Assert.Equal(0x27000001u, tkImplementation); + } + + [Fact] + public void GetExportedTypeProps_Truncation() + { + IMetaDataImport2 wrapper = CreateWrapper(); + IMetaDataAssemblyImport assemblyImport = (IMetaDataAssemblyImport)wrapper; + + // Use a small buffer to trigger truncation + char* szName = stackalloc char[5]; + uint pchName; + uint tkImplementation; + uint tkTypeDef; + uint dwFlags; + + int hr = assemblyImport.GetExportedTypeProps(0x27000001, szName, 5, &pchName, + &tkImplementation, &tkTypeDef, &dwFlags); + Assert.Equal(CldbHResults.CLDB_S_TRUNCATION, hr); + } + + [Fact] + public void FindExportedTypeByName_FindsType() + { + IMetaDataImport2 wrapper = CreateWrapper(); + IMetaDataAssemblyImport assemblyImport = (IMetaDataAssemblyImport)wrapper; + + uint tkExportedType; + fixed (char* szName = "TestNamespace.ExportedClass") + { + int hr = assemblyImport.FindExportedTypeByName(szName, 0, &tkExportedType); + Assert.True(hr >= 0); + Assert.Equal(0x27000001u, tkExportedType); + } + } + + [Fact] + public void FindExportedTypeByName_NestedType() + { + IMetaDataImport2 wrapper = CreateWrapper(); + IMetaDataAssemblyImport assemblyImport = (IMetaDataAssemblyImport)wrapper; + + uint tkExportedType; + fixed (char* szName = "NestedExported") + { + // Find nested type with parent = ExportedType row 1 + int hr = assemblyImport.FindExportedTypeByName(szName, 0x27000001, &tkExportedType); + Assert.True(hr >= 0); + Assert.Equal(0x27000002u, tkExportedType); + } + } + + [Fact] + public void FindExportedTypeByName_NotFound() + { + IMetaDataImport2 wrapper = CreateWrapper(); + IMetaDataAssemblyImport assemblyImport = (IMetaDataAssemblyImport)wrapper; + + uint tkExportedType; + fixed (char* szName = "NonExistent.Type") + { + int hr = assemblyImport.FindExportedTypeByName(szName, 0, &tkExportedType); + Assert.True(hr < 0); // CLDB_E_RECORD_NOTFOUND + } + } } From 713491c02f93c48ba1378d6033b482c42923aa2f Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Apr 2026 13:13:34 -0700 Subject: [PATCH 023/115] [cDAC] Implement EnumerateAssembliesInAppDomain for cDAC (#127540) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Replaces the legacy-delegation stub in `DacDbiImpl.EnumerateAssembliesInAppDomain` with a managed implementation using the `ILoader` contract, mirroring the native C++ logic in `dacdbiimpl.cpp:4412–4449`. ### Changes - **`IDacDbiInterface.cs`**: Changed `fpCallback` parameter type from `nint` to `delegate* unmanaged` (consistent with `EnumerateThreads`) - **`DacDbiImpl.cs`**: Full implementation that: - Returns `S_OK` early on null `vmAppDomain` - Enumerates via `ILoader.GetModuleHandles` with `IncludeLoading | IncludeLoaded | IncludeExecution` - Resolves each `ModuleHandle` to an assembly pointer via `ILoader.GetAssembly` - Calls `fpCallback(assembly, pUserData)` per entry - `#if DEBUG` validation against legacy DAC (same pattern as `EnumerateThreads`) - DacDbiImplTests.cs - Add 5 tests covering zero AppDomain, null callback, single/multiple/empty assembly enumeration with mocked ILoade --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: barosiak <76071368+barosiak@users.noreply.github.com> Co-authored-by: Barbara Rosiak Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Dbi/DacDbiImpl.cs | 62 +++++++- .../Dbi/IDacDbiInterface.cs | 2 +- .../managed/cdac/tests/DacDbiImplTests.cs | 144 ++++++++++++++++++ 3 files changed, 205 insertions(+), 3 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs index 032640bdd23ab7..dc3f6b7d93cdb9 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs @@ -395,8 +395,66 @@ public int SetCompilerFlags(ulong vmAssembly, Interop.BOOL fAllowJitOpts, Intero return hr; } - public int EnumerateAssembliesInAppDomain(ulong vmAppDomain, nint fpCallback, nint pUserData) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.EnumerateAssembliesInAppDomain(vmAppDomain, fpCallback, pUserData) : HResults.E_NOTIMPL; + public int EnumerateAssembliesInAppDomain(ulong vmAppDomain, delegate* unmanaged fpCallback, nint pUserData) + { + int hr = HResults.S_OK; +#if DEBUG + List? cdacAssemblies = _legacy is not null ? new() : null; +#endif + try + { + if (fpCallback == null) + { + throw new ArgumentNullException(nameof(fpCallback)); + } + + if (vmAppDomain == 0) + { + return hr; + } + + Contracts.ILoader loader = _target.Contracts.Loader; + foreach (Contracts.ModuleHandle handle in loader.GetModuleHandles( + new TargetPointer(vmAppDomain), + AssemblyIterationFlags.IncludeLoading | AssemblyIterationFlags.IncludeLoaded | AssemblyIterationFlags.IncludeExecution)) + { + TargetPointer assembly = loader.GetAssembly(handle); + fpCallback(assembly.Value, pUserData); +#if DEBUG + cdacAssemblies?.Add(assembly.Value); +#endif + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacy is not null && fpCallback != null) + { + List dacAssemblies = new(); + GCHandle dacHandle = GCHandle.Alloc(dacAssemblies); + try + { + int hrLocal = _legacy.EnumerateAssembliesInAppDomain(vmAppDomain, &CollectEnumerationCallback, GCHandle.ToIntPtr(dacHandle)); + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + { + Debug.Assert( + cdacAssemblies!.SequenceEqual(dacAssemblies), + $"Assembly enumeration mismatch - " + + $"cDAC: [{string.Join(",", cdacAssemblies!.Select(a => $"0x{a:x}"))}], " + + $"DAC: [{string.Join(",", dacAssemblies.Select(a => $"0x{a:x}"))}]"); + } + } + finally + { + dacHandle.Free(); + } + } +#endif + return hr; + } public int EnumerateModulesInAssembly(ulong vmAssembly, nint fpCallback, nint pUserData) => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.EnumerateModulesInAssembly(vmAssembly, fpCallback, pUserData) : HResults.E_NOTIMPL; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs index 28970787a37c94..77aa05e7fdacbb 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs @@ -206,7 +206,7 @@ public unsafe partial interface IDacDbiInterface int SetCompilerFlags(ulong vmAssembly, Interop.BOOL fAllowJitOpts, Interop.BOOL fEnableEnC); [PreserveSig] - int EnumerateAssembliesInAppDomain(ulong vmAppDomain, nint fpCallback, nint pUserData); + int EnumerateAssembliesInAppDomain(ulong vmAppDomain, delegate* unmanaged fpCallback, nint pUserData); [PreserveSig] int EnumerateModulesInAssembly(ulong vmAssembly, nint fpCallback, nint pUserData); diff --git a/src/native/managed/cdac/tests/DacDbiImplTests.cs b/src/native/managed/cdac/tests/DacDbiImplTests.cs index 08d6b89b10f318..0f9268acbeb255 100644 --- a/src/native/managed/cdac/tests/DacDbiImplTests.cs +++ b/src/native/managed/cdac/tests/DacDbiImplTests.cs @@ -3,8 +3,10 @@ using System; using System.Collections.Generic; +using System.Runtime.InteropServices; using Microsoft.Diagnostics.DataContractReader.Contracts; using Microsoft.Diagnostics.DataContractReader.Legacy; +using Moq; using Xunit; namespace Microsoft.Diagnostics.DataContractReader.Tests; @@ -253,4 +255,146 @@ public void SetCompilerFlags_EnCBlocked_NotificationProfiler(MockTarget.Architec uint rawFlags = target.Read(moduleAddr + (ulong)flagsOffset); Assert.Equal(0u, rawFlags & IsEditAndContinue); } + + private static DacDbiImpl CreateDacDbiWithMockLoader( + MockTarget.Architecture arch, + Mock mockLoader) + { + var target = new TestPlaceholderTarget.Builder(arch) + .UseReader((_, _) => -1) + .AddMockContract(mockLoader) + .Build(); + return new DacDbiImpl(target, legacyObj: null); + } + + [UnmanagedCallersOnly] + private static unsafe void CollectAssemblyCallback(ulong value, nint pUserData) + { + GCHandle handle = GCHandle.FromIntPtr(pUserData); + ((List)handle.Target!).Add(value); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void EnumerateAssembliesInAppDomain_ZeroAppDomain(MockTarget.Architecture arch) + { + var mockLoader = new Mock(); + DacDbiImpl dacDbi = CreateDacDbiWithMockLoader(arch, mockLoader); + + List assemblies = new(); + GCHandle gcHandle = GCHandle.Alloc(assemblies); + int hr = dacDbi.EnumerateAssembliesInAppDomain(0, &CollectAssemblyCallback, GCHandle.ToIntPtr(gcHandle)); + gcHandle.Free(); + + Assert.Equal(System.HResults.S_OK, hr); + Assert.Empty(assemblies); + mockLoader.Verify( + l => l.GetModuleHandles(It.IsAny(), It.IsAny()), + Times.Never); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void EnumerateAssembliesInAppDomain_NullCallback(MockTarget.Architecture arch) + { + var mockLoader = new Mock(); + DacDbiImpl dacDbi = CreateDacDbiWithMockLoader(arch, mockLoader); + + int hr = dacDbi.EnumerateAssembliesInAppDomain(0x1000, null, nint.Zero); + + Assert.NotEqual(System.HResults.S_OK, hr); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void EnumerateAssembliesInAppDomain_SingleAssembly_CallsCallback(MockTarget.Architecture arch) + { + ulong appDomainAddr = 0x1000; + ulong assemblyAddr = 0x2000; + TargetPointer moduleAddr = new(0x3000); + + var mockLoader = new Mock(); + mockLoader + .Setup(l => l.GetModuleHandles( + new TargetPointer(appDomainAddr), + AssemblyIterationFlags.IncludeLoading | AssemblyIterationFlags.IncludeLoaded | AssemblyIterationFlags.IncludeExecution)) + .Returns(new[] { new Contracts.ModuleHandle(moduleAddr) }); + mockLoader + .Setup(l => l.GetAssembly(It.Is(h => h.Address == moduleAddr))) + .Returns(new TargetPointer(assemblyAddr)); + + DacDbiImpl dacDbi = CreateDacDbiWithMockLoader(arch, mockLoader); + + List assemblies = new(); + GCHandle gcHandle = GCHandle.Alloc(assemblies); + int hr = dacDbi.EnumerateAssembliesInAppDomain(appDomainAddr, &CollectAssemblyCallback, GCHandle.ToIntPtr(gcHandle)); + gcHandle.Free(); + + Assert.Equal(System.HResults.S_OK, hr); + Assert.Single(assemblies); + Assert.Equal(assemblyAddr, assemblies[0]); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void EnumerateAssembliesInAppDomain_MultipleAssemblies(MockTarget.Architecture arch) + { + ulong appDomainAddr = 0x1000; + ulong[] expectedAssemblies = [0x2000, 0x3000, 0x4000]; + TargetPointer[] moduleAddrs = [new(0x5000), new(0x6000), new(0x7000)]; + + var mockLoader = new Mock(); + mockLoader + .Setup(l => l.GetModuleHandles( + new TargetPointer(appDomainAddr), + AssemblyIterationFlags.IncludeLoading | AssemblyIterationFlags.IncludeLoaded | AssemblyIterationFlags.IncludeExecution)) + .Returns(new[] + { + new Contracts.ModuleHandle(moduleAddrs[0]), + new Contracts.ModuleHandle(moduleAddrs[1]), + new Contracts.ModuleHandle(moduleAddrs[2]), + }); + + for (int i = 0; i < 3; i++) + { + int index = i; + mockLoader + .Setup(l => l.GetAssembly(It.Is(h => h.Address == moduleAddrs[index]))) + .Returns(new TargetPointer(expectedAssemblies[index])); + } + + DacDbiImpl dacDbi = CreateDacDbiWithMockLoader(arch, mockLoader); + + List assemblies = new(); + GCHandle gcHandle = GCHandle.Alloc(assemblies); + int hr = dacDbi.EnumerateAssembliesInAppDomain(appDomainAddr, &CollectAssemblyCallback, GCHandle.ToIntPtr(gcHandle)); + gcHandle.Free(); + + Assert.Equal(System.HResults.S_OK, hr); + Assert.Equal(expectedAssemblies, assemblies.ToArray()); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void EnumerateAssembliesInAppDomain_NoAssemblies(MockTarget.Architecture arch) + { + ulong appDomainAddr = 0x1000; + + var mockLoader = new Mock(); + mockLoader + .Setup(l => l.GetModuleHandles( + new TargetPointer(appDomainAddr), + AssemblyIterationFlags.IncludeLoading | AssemblyIterationFlags.IncludeLoaded | AssemblyIterationFlags.IncludeExecution)) + .Returns(Array.Empty()); + + DacDbiImpl dacDbi = CreateDacDbiWithMockLoader(arch, mockLoader); + + List assemblies = new(); + GCHandle gcHandle = GCHandle.Alloc(assemblies); + int hr = dacDbi.EnumerateAssembliesInAppDomain(appDomainAddr, &CollectAssemblyCallback, GCHandle.ToIntPtr(gcHandle)); + gcHandle.Free(); + + Assert.Equal(System.HResults.S_OK, hr); + Assert.Empty(assemblies); + } } From 0f01d22be46fc2daa2cee0c86b467a60d654afc6 Mon Sep 17 00:00:00 2001 From: Elinor Fung Date: Wed, 29 Apr 2026 13:45:04 -0700 Subject: [PATCH 024/115] Fix sporadic AccessViolation in compressed single-file apps on Apple Silicon (#127355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem On macOS Apple Silicon (observed on macOS 15), compressed self-contained single-file apps intermittently hit `AccessViolationException` crashes. With the repro provided in the issue, the failure rate was roughly 75% in a local run. `FlatImageLayout::LoadImageByCopyingParts` allocates the image region with `MEM_RESERVE_EXECUTABLE` (which maps to `MAP_JIT` on macOS) as `PAGE_READWRITE`, copies in all section bytes, then promotes executable sections to `PAGE_EXECUTE_READWRITE` via a second `mprotect`. On Apple Silicon, this `RW → RWX` transition on MAP_JIT memory has been observed to intermittently succeed at the API level but leave pages non-executable at the kernel level, producing the sporadic AVs. ## Fix On Apple Silicon (`HOST_OSX && HOST_ARM64`), avoid the unreliable transition: reserve the whole image region as `PAGE_NOACCESS` up front, then commit each section directly with its final runtime protection (`PAGE_EXECUTE_READWRITE` for exec, `PAGE_READWRITE` otherwise), copying executable content under `PAL_JitWriteProtect`. The `PROT_NONE → RWX` direction via a fresh commit is reliable; the problematic `RW → RWX` transition no longer occurs. Read-only sections are still downgraded from `PAGE_READWRITE` to `PAGE_READONLY` after the copy, which is a W-removing transition and is not implicated in the original bug. The non-Apple-Silicon code path is unchanged. ## Testing New test: `AppHost.Bundle.Tests.AppLaunch.SelfContained_Compressed_SpawnsChildren` (OSX-only). Adds a `launch_self` option to the `HelloWorld` test asset that spawns 5 copies of itself in parallel, and asserts the parent runs to completion. Local validation on Apple Silicon: - Without fix: parent crash with `AccessViolationException` in ~74% of trials at N=5 children (rises to ~98% at N≥12). - With fix: 0 failures across 50+ trials at N=5. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/peimagelayout.cpp | 76 ++++++++++++++++--- .../tests/AppHost.Bundle.Tests/AppLaunch.cs | 18 +++++ .../Assets/Projects/HelloWorld/Program.cs | 21 +++++ 3 files changed, 105 insertions(+), 10 deletions(-) diff --git a/src/coreclr/vm/peimagelayout.cpp b/src/coreclr/vm/peimagelayout.cpp index 3c87f1b1d06ab3..464565ecb3eb2a 100644 --- a/src/coreclr/vm/peimagelayout.cpp +++ b/src/coreclr/vm/peimagelayout.cpp @@ -893,17 +893,29 @@ void* FlatImageLayout::LoadImageByCopyingParts(SIZE_T* m_imageParts) const #endif // FEATURE_ENABLE_NO_ADDRESS_SPACE_RANDOMIZATION DWORD allocationType = MEM_RESERVE | MEM_COMMIT; + DWORD initialProtection = PAGE_READWRITE; #if defined(HOST_UNIX) && defined(FEATURE_DYNAMIC_CODE_COMPILED) // Tell PAL to use the executable memory allocator to satisfy this request for virtual memory. // This is required on MacOS and otherwise will allow us to place native R2R code close to the // coreclr library and thus improve performance by avoiding jump stubs in managed code. allocationType |= MEM_RESERVE_EXECUTABLE; #endif +#if defined(HOST_OSX) && defined(HOST_ARM64) + // On Apple Silicon, allocating the image region as plain PAGE_READWRITE and then promoting + // executable sections to PAGE_EXECUTE_READ via mprotect has been observed to + // intermittently leave pages non-executable at the kernel level. This manifests as sporadic + // AccessViolationException crashes. + // + // Avoid the unreliable transition - reserve the whole region as PAGE_NOACCESS first, then + // commit each section directly with its final protection. + allocationType &= ~MEM_COMMIT; + initialProtection = PAGE_NOACCESS; +#endif COUNT_T allocSize = ALIGN_UP(this->GetVirtualSize(), g_SystemInfo.dwAllocationGranularity); - LPVOID base = ClrVirtualAlloc(preferredBase, allocSize, allocationType, PAGE_READWRITE); + LPVOID base = ClrVirtualAlloc(preferredBase, allocSize, allocationType, initialProtection); if (base == NULL && preferredBase != NULL) - base = ClrVirtualAlloc(NULL, allocSize, allocationType, PAGE_READWRITE); + base = ClrVirtualAlloc(NULL, allocSize, allocationType, initialProtection); if (base == NULL) ThrowLastError(); @@ -911,6 +923,56 @@ void* FlatImageLayout::LoadImageByCopyingParts(SIZE_T* m_imageParts) const // when loading by copying we have only one part to free. m_imageParts[0] = AllocatedPart(base); + IMAGE_SECTION_HEADER* sectionStart = IMAGE_FIRST_SECTION(FindNTHeaders()); + IMAGE_SECTION_HEADER* sectionEnd = sectionStart + VAL16(FindNTHeaders()->FileHeader.NumberOfSections); + +#if defined(HOST_OSX) && defined(HOST_ARM64) + _ASSERTE((allocationType & MEM_COMMIT) == 0); + _ASSERTE(initialProtection != PAGE_READWRITE); + + DWORD sizeOfHeaders = VAL32(FindNTHeaders()->OptionalHeader.SizeOfHeaders); + + // Commit and copy headers, then apply read-only protection. + if (ClrVirtualAlloc(base, sizeOfHeaders, MEM_COMMIT, PAGE_READWRITE) == NULL) + ThrowLastError(); + + CopyMemory(base, (void*)GetBase(), sizeOfHeaders); + + DWORD oldProtection; // PAL layer doesn't properly set the previous protection, so we don't try to validate it here. + if (!ClrVirtualProtect((void*)base, sizeOfHeaders, PAGE_READONLY, &oldProtection)) + ThrowLastError(); + + // Commit and copy each section with its desired protection. + for (IMAGE_SECTION_HEADER* section = sectionStart; section < sectionEnd; section++) + { + DWORD virtualSize = VAL32(section->Misc.VirtualSize); + if (virtualSize == 0) + continue; + + bool isExec = (section->Characteristics & IMAGE_SCN_MEM_EXECUTE) != 0; + BYTE* sectionBase = (BYTE*)base + VAL32(section->VirtualAddress); + DWORD copySize = min(VAL32(section->SizeOfRawData), virtualSize); + + if (ClrVirtualAlloc(sectionBase, virtualSize, MEM_COMMIT, isExec ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE) == NULL) + ThrowLastError(); + + if (isExec) + PAL_JitWriteProtect(true); + + CopyMemory(sectionBase, (BYTE*)GetBase() + VAL32(section->PointerToRawData), copySize); + + if (isExec) + { + PAL_JitWriteProtect(false); + } + else if ((section->Characteristics & IMAGE_SCN_MEM_WRITE) == 0) + { + DWORD oldProt; + if (!ClrVirtualProtect(sectionBase, virtualSize, PAGE_READONLY, &oldProt)) + ThrowLastError(); + } + } +#else // We're going to copy everything first, and write protect what we need to later. // First, copy headers @@ -918,9 +980,6 @@ void* FlatImageLayout::LoadImageByCopyingParts(SIZE_T* m_imageParts) const // Now, copy all sections to appropriate virtual address - IMAGE_SECTION_HEADER* sectionStart = IMAGE_FIRST_SECTION(FindNTHeaders()); - IMAGE_SECTION_HEADER* sectionEnd = sectionStart + VAL16(FindNTHeaders()->FileHeader.NumberOfSections); - IMAGE_SECTION_HEADER* section = sectionStart; while (section < sectionEnd) { @@ -944,13 +1003,9 @@ void* FlatImageLayout::LoadImageByCopyingParts(SIZE_T* m_imageParts) const // Finally, apply proper protection to copied sections for (section = sectionStart; section < sectionEnd; section++) { - DWORD executableProtection = PAGE_EXECUTE_READ; -#if defined(HOST_OSX) && defined(HOST_ARM64) - executableProtection = PAGE_EXECUTE_READWRITE; -#endif // Add appropriate page protection. DWORD newProtection = section->Characteristics & IMAGE_SCN_MEM_EXECUTE ? - executableProtection : + PAGE_EXECUTE_READ : section->Characteristics & IMAGE_SCN_MEM_WRITE ? PAGE_READWRITE : PAGE_READONLY; @@ -962,6 +1017,7 @@ void* FlatImageLayout::LoadImageByCopyingParts(SIZE_T* m_imageParts) const ThrowLastError(); } } +#endif // defined(HOST_OSX) && defined(HOST_ARM64) return base; } diff --git a/src/installer/tests/AppHost.Bundle.Tests/AppLaunch.cs b/src/installer/tests/AppHost.Bundle.Tests/AppLaunch.cs index 17d79ede5fa217..7f3e26b9ac56f6 100644 --- a/src/installer/tests/AppHost.Bundle.Tests/AppLaunch.cs +++ b/src/installer/tests/AppHost.Bundle.Tests/AppLaunch.cs @@ -110,6 +110,24 @@ private void NonAsciiCharacterSelfContainedApp() } } + // Regression test: on macOS Apple Silicon, compressed self-contained single-file + // apps intermittently hit an AccessViolationException based on how we load images. + // This was observed via concurrent child process launches, so this test launches + // multiple child processes in parallel as a regression test. + [Fact] + [PlatformSpecific(TestPlatforms.OSX)] + public void SelfContained_Compressed_SpawnsChildren() + { + string singleFile = sharedTestState.SelfContainedApp.Bundle(BundleOptions.EnableCompression); + + Command.Create(singleFile, "launch_self") + .CaptureStdErr() + .CaptureStdOut() + .Execute() + .Should().Pass() + .And.HaveStdOutContaining("Hello World!"); + } + [Theory( SkipType = typeof(Binaries.CetCompat), SkipUnless = nameof(Binaries.CetCompat.IsSupported), diff --git a/src/installer/tests/Assets/Projects/HelloWorld/Program.cs b/src/installer/tests/Assets/Projects/HelloWorld/Program.cs index 60bb05c8b641cf..7fd61b604c4c04 100644 --- a/src/installer/tests/Assets/Projects/HelloWorld/Program.cs +++ b/src/installer/tests/Assets/Projects/HelloWorld/Program.cs @@ -2,9 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics; +using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Loader; +using System.Threading.Tasks; namespace HelloWorld { @@ -53,6 +56,24 @@ public static void Main(string[] args) // Disable core dumps - test is intentionally crashing Utilities.CoreDump.Disable(); throw new Exception("Goodbye World!"); + case "launch_self": + // Launch copies of this app in parallel. + // When run via dotnet , GetCommandLineArgs[0] is the managed + // dll - forward it so the child knows what to launch. + string fileName = Environment.ProcessPath!; + string entry = Environment.GetCommandLineArgs()[0]; + bool forwardEntry = entry != Environment.ProcessPath; + + var tasks = Enumerable.Range(0, 5).Select(_ => Task.Run(() => + { + var startInfo = new ProcessStartInfo { FileName = fileName }; + if (forwardEntry) + startInfo.ArgumentList.Add(entry); + using var process = Process.Start(startInfo)!; + process.WaitForExit(); + })).ToArray(); + Task.WaitAll(tasks); + break; default: break; } From 7fe7b6f9ee530920febbabdfdc9c2cadbab3348e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 06:19:00 +0900 Subject: [PATCH 025/115] Add missing custom attribute marking to ILTrim TokenBased nodes (#127369) Several `TokenBasedNode` subclasses in ILTrim were not calling `CustomAttributeNode.AddDependenciesDueToCustomAttributes`, causing custom attributes on those metadata tables to be silently dropped during trimming. --- .../GenericParameterConstraintNode.cs | 8 ++++- .../TokenBased/GenericParameterNode.cs | 10 +++++- .../TokenBased/ManifestResourceNode.cs | 34 +++++++++---------- .../TokenBased/ParameterNode.cs | 10 +++++- .../ILTrim.Tests/ILTrimExpectedFailures.txt | 25 +++----------- 5 files changed, 47 insertions(+), 40 deletions(-) diff --git a/src/coreclr/tools/ILTrim.Core/DependencyAnalysis/TokenBased/GenericParameterConstraintNode.cs b/src/coreclr/tools/ILTrim.Core/DependencyAnalysis/TokenBased/GenericParameterConstraintNode.cs index e3979602375dfc..b5722f70a252d8 100644 --- a/src/coreclr/tools/ILTrim.Core/DependencyAnalysis/TokenBased/GenericParameterConstraintNode.cs +++ b/src/coreclr/tools/ILTrim.Core/DependencyAnalysis/TokenBased/GenericParameterConstraintNode.cs @@ -29,7 +29,13 @@ public GenericParameterConstraintNode(EcmaModule module, GenericParameterConstra public override IEnumerable GetStaticDependencies(NodeFactory factory) { GenericParameterConstraint genericParamConstraint = _module.MetadataReader.GetGenericParameterConstraint(Handle); - yield return new DependencyListEntry(factory.GetNodeForTypeToken(_module, genericParamConstraint.Type), "Parameter constrained to type"); + + DependencyList dependencies = new DependencyList(); + dependencies.Add(factory.GetNodeForTypeToken(_module, genericParamConstraint.Type), "Parameter constrained to type"); + + CustomAttributeNode.AddDependenciesDueToCustomAttributes(ref dependencies, factory, _module, genericParamConstraint.GetCustomAttributes()); + + return dependencies; } protected override EntityHandle WriteInternal(ModuleWritingContext writeContext) diff --git a/src/coreclr/tools/ILTrim.Core/DependencyAnalysis/TokenBased/GenericParameterNode.cs b/src/coreclr/tools/ILTrim.Core/DependencyAnalysis/TokenBased/GenericParameterNode.cs index 12a4e6e2db3fb3..74dcb9d7c262f8 100644 --- a/src/coreclr/tools/ILTrim.Core/DependencyAnalysis/TokenBased/GenericParameterNode.cs +++ b/src/coreclr/tools/ILTrim.Core/DependencyAnalysis/TokenBased/GenericParameterNode.cs @@ -28,10 +28,18 @@ public GenericParameterNode(EcmaModule module, GenericParameterHandle handle) public override IEnumerable GetStaticDependencies(NodeFactory factory) { GenericParameter genericParam = _module.MetadataReader.GetGenericParameter(Handle); + + DependencyList dependencies = null; + foreach (var genericParamConstrain in genericParam.GetConstraints()) { - yield return new DependencyListEntry(factory.GenericParameterConstraint(_module, genericParamConstrain), "Generic Parameter Constraint of Generic Parameter"); + dependencies ??= new DependencyList(); + dependencies.Add(factory.GenericParameterConstraint(_module, genericParamConstrain), "Generic Parameter Constraint of Generic Parameter"); } + + CustomAttributeNode.AddDependenciesDueToCustomAttributes(ref dependencies, factory, _module, genericParam.GetCustomAttributes()); + + return dependencies; } protected override EntityHandle WriteInternal(ModuleWritingContext writeContext) diff --git a/src/coreclr/tools/ILTrim.Core/DependencyAnalysis/TokenBased/ManifestResourceNode.cs b/src/coreclr/tools/ILTrim.Core/DependencyAnalysis/TokenBased/ManifestResourceNode.cs index b7f6293f7b4376..3f55462eea6f3c 100644 --- a/src/coreclr/tools/ILTrim.Core/DependencyAnalysis/TokenBased/ManifestResourceNode.cs +++ b/src/coreclr/tools/ILTrim.Core/DependencyAnalysis/TokenBased/ManifestResourceNode.cs @@ -32,6 +32,8 @@ public override IEnumerable GetStaticDependencies(NodeFacto _skipWritingResource = false; + DependencyList dependencies = null; + if (resource.Implementation.IsNil) { string resourceName = _module.MetadataReader.GetString(resource.Name); @@ -40,29 +42,25 @@ public override IEnumerable GetStaticDependencies(NodeFacto string assemblyName = _module.Assembly.GetName().Name; _skipWritingResource = factory.Settings.Optimizations.IsEnabled(CodeOptimizations.RemoveDescriptors, assemblyName); - if (factory.Settings.IgnoreDescriptors) - return null; + if (!factory.Settings.IgnoreDescriptors) + { + PEMemoryBlock resourceDirectory = _module.PEReader.GetSectionData(_module.PEReader.PEHeaders.CorHeader.ResourcesDirectory.RelativeVirtualAddress); + BlobReader reader = resourceDirectory.GetReader((int)resource.Offset, resourceDirectory.Length - (int)resource.Offset); + int length = (int)reader.ReadUInt32(); - PEMemoryBlock resourceDirectory = _module.PEReader.GetSectionData(_module.PEReader.PEHeaders.CorHeader.ResourcesDirectory.RelativeVirtualAddress); - BlobReader reader = resourceDirectory.GetReader((int)resource.Offset, resourceDirectory.Length - (int)resource.Offset); - int length = (int)reader.ReadUInt32(); + UnmanagedMemoryStream ms; + unsafe + { + ms = new UnmanagedMemoryStream(reader.CurrentPointer, length); + } - UnmanagedMemoryStream ms; - unsafe - { - ms = new UnmanagedMemoryStream(reader.CurrentPointer, length); + dependencies = DescriptorMarker.GetDependencies(factory.Logger, factory, ms, resource, _module, "resource " + resourceName + " in " + _module.ToString(), factory.Settings.FeatureSettings); } - - return DescriptorMarker.GetDependencies(factory.Logger, factory, ms, resource, _module, "resource " + resourceName + " in " + _module.ToString(), factory.Settings.FeatureSettings); - } - else - { - return null; } } else { - DependencyList dependencies = new(); + dependencies = new(); switch (resource.Implementation.Kind) { case HandleKind.AssemblyReference: @@ -73,8 +71,10 @@ public override IEnumerable GetStaticDependencies(NodeFacto // TODO: Handle AssemblyFile throw new InvalidOperationException(resource.Implementation.Kind.ToString()); } - return dependencies; } + + CustomAttributeNode.AddDependenciesDueToCustomAttributes(ref dependencies, factory, _module, resource.GetCustomAttributes()); + return dependencies; } public override void BuildTokens(TokenMap.Builder builder) diff --git a/src/coreclr/tools/ILTrim.Core/DependencyAnalysis/TokenBased/ParameterNode.cs b/src/coreclr/tools/ILTrim.Core/DependencyAnalysis/TokenBased/ParameterNode.cs index 9cecda6e8137fb..915e88954a167c 100644 --- a/src/coreclr/tools/ILTrim.Core/DependencyAnalysis/TokenBased/ParameterNode.cs +++ b/src/coreclr/tools/ILTrim.Core/DependencyAnalysis/TokenBased/ParameterNode.cs @@ -20,7 +20,15 @@ public ParameterNode(EcmaModule module, ParameterHandle handle) private ParameterHandle Handle => (ParameterHandle)_handle; - public override IEnumerable GetStaticDependencies(NodeFactory context) => null; + public override IEnumerable GetStaticDependencies(NodeFactory context) + { + DependencyList dependencies = null; + + Parameter parameter = _module.MetadataReader.GetParameter(Handle); + CustomAttributeNode.AddDependenciesDueToCustomAttributes(ref dependencies, context, _module, parameter.GetCustomAttributes()); + + return dependencies; + } public override string ToString() { diff --git a/src/coreclr/tools/ILTrim.Tests/ILTrimExpectedFailures.txt b/src/coreclr/tools/ILTrim.Tests/ILTrimExpectedFailures.txt index 359cb3545127a9..4f03cadcadc2be 100644 --- a/src/coreclr/tools/ILTrim.Tests/ILTrimExpectedFailures.txt +++ b/src/coreclr/tools/ILTrim.Tests/ILTrimExpectedFailures.txt @@ -1,7 +1,6 @@ Advanced.TypeCheckRemoval Attributes.AssemblyAttributeAccessesMembers Attributes.AssemblyAttributeIsRemovedIfOnlyTypesUsedInAssembly -Attributes.AttributeOnParameterInUsedMethodIsKept Attributes.AttributeOnPreservedTypeWithTypeUsedInConstructorIsKept Attributes.AttributeOnPreservedTypeWithTypeUsedInDifferentNamespaceIsKept Attributes.AttributeOnPreservedTypeWithTypeUsedInFieldIsKept @@ -54,16 +53,20 @@ Attributes.OnlyKeepUsed.AttributeUsedByAttributeIsKept Attributes.OnlyKeepUsed.CanLinkCoreLibrariesWithOnlyKeepUsedAttributes Attributes.OnlyKeepUsed.CoreLibraryUnusedAssemblyAttributesAreRemoved Attributes.OnlyKeepUsed.CoreLibraryUsedAssemblyAttributesAreKept +Attributes.OnlyKeepUsed.MethodWithUnmanagedConstraint Attributes.OnlyKeepUsed.NullableOnConstraintsKept Attributes.OnlyKeepUsed.NullableOnConstraintsRemoved +Attributes.OnlyKeepUsed.UnusedAttributeOnGenericParameterIsRemoved +Attributes.OnlyKeepUsed.UnusedAttributeOnReturnTypeIsRemoved Attributes.OnlyKeepUsed.UnusedAttributeTypeOnAssemblyIsRemoved Attributes.OnlyKeepUsed.UnusedAttributeTypeOnEventIsRemoved Attributes.OnlyKeepUsed.UnusedAttributeTypeOnMethodIsRemoved Attributes.OnlyKeepUsed.UnusedAttributeTypeOnModuleIsRemoved +Attributes.OnlyKeepUsed.UnusedAttributeTypeOnParameterIsRemoved Attributes.OnlyKeepUsed.UnusedAttributeTypeOnPropertyIsRemoved Attributes.OnlyKeepUsed.UnusedAttributeTypeOnTypeIsRemoved +Attributes.OnlyKeepUsed.UnusedAttributeWithTypeForwarderIsRemoved Attributes.OnlyKeepUsed.UnusedDerivedAttributeType -Attributes.OnlyKeepUsed.UsedAttributeTypeOnParameterIsKept Attributes.SecurityAttributesOnUsedMethodAreKept Attributes.SecurityAttributesOnUsedTypeAreKept Attributes.StructLayout.ExplicitClass @@ -95,9 +98,7 @@ DataFlow.AnnotatedMembersAccessedViaUnsafeAccessor DataFlow.ApplyTypeAnnotations DataFlow.AttributeConstructorDataflow DataFlow.AttributeFieldDataflow -DataFlow.AttributePrimaryConstructorDataflow DataFlow.AttributePropertyDataflow -DataFlow.ByRefDataflow DataFlow.CompilerGeneratedCodeAccessedViaReflection DataFlow.CompilerGeneratedCodeDataflow DataFlow.CompilerGeneratedTypes @@ -113,7 +114,6 @@ DataFlow.ExtensionMembersDataFlow DataFlow.FeatureCheckDataFlow DataFlow.FeatureGuardAttributeDataFlow DataFlow.FieldDataFlow -DataFlow.FieldKeyword DataFlow.GenericParameterDataFlow DataFlow.GenericParameterDataFlowMarking DataFlow.GenericParameterWarningLocation @@ -124,7 +124,6 @@ DataFlow.MemberTypes DataFlow.MemberTypesAllOnCopyAssembly DataFlow.MethodParametersDataFlow DataFlow.ModifierDataFlow -DataFlow.NullableAnnotations DataFlow.ObjectGetTypeDataflow DataFlow.PropertyDataFlow DataFlow.RuntimeAsyncMethods @@ -316,7 +315,6 @@ LinkAttributes.LinkAttributeErrorCases LinkAttributes.LinkerAttributeRemoval LinkAttributes.LinkerAttributeRemovalAndPreserveAssembly LinkAttributes.LinkerAttributeRemovalConditional -LinkAttributes.OverrideAttributeRemoval LinkAttributes.TypedArguments LinkAttributes.TypedArgumentsErrors LinkXml.CanPreserveAnExportedType @@ -369,22 +367,13 @@ References.MissingReferenceInUsedCodePath References.ReferenceWithEntryPoint Reflection.ActivatorCreateInstance Reflection.AssemblyImportedViaReflectionWithDerivedType -Reflection.ConstructorsUsedViaReflection -Reflection.ConstructorUsedViaReflection Reflection.CoreLibMessages Reflection.EventHanderTypeGetInvokeMethod Reflection.EventsUsedViaReflection Reflection.ExpressionCallString -Reflection.ExpressionFieldString -Reflection.ExpressionPropertyString -Reflection.FieldsUsedViaReflection Reflection.IsAssignableFrom -Reflection.MembersUsedViaReflection -Reflection.MemberUsedViaReflection Reflection.MethodsUsedViaReflection Reflection.MethodUsedViaReflection -Reflection.NestedTypesUsedViaReflection -Reflection.NestedTypeUsedViaReflection Reflection.ObjectGetType Reflection.ObjectGetTypeLibraryMode Reflection.ParametersUsedViaReflection @@ -392,10 +381,8 @@ Reflection.PropertiesUsedViaReflection Reflection.PropertyUsedViaReflection Reflection.RunClassConstructor Reflection.RuntimeReflectionExtensionsCalls -Reflection.TypeDelegator Reflection.TypeHierarchyLibraryModeSuppressions Reflection.TypeHierarchyReflectionWarnings -Reflection.TypeHierarchySuppressions Reflection.TypeMap Reflection.TypeMapEntryAssembly Reflection.TypeUsedViaReflection @@ -458,8 +445,6 @@ Symbols.ReferenceWithPortablePdbCopyActionAndSymbolLinkingEnabled TestFramework.VerifyAttributesInAssemblyWorks TestFramework.VerifyAttributesInAssemblyWorksWithStrings TestFramework.VerifyLocalsAreChanged -TopLevelStatements.BasicDataFlow -TopLevelStatements.InvalidAnnotations TypeForwarding.AttributeArgumentForwarded TypeForwarding.AttributeArgumentForwardedWithCopyAction TypeForwarding.AttributeEnumArgumentForwardedCopyUsedWithSweptForwarder From f2a0ea7a8ea8967f90884128d8c4c8f0ae0b4185 Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Wed, 29 Apr 2026 19:10:22 -0400 Subject: [PATCH 026/115] Adjust breaking change workflow based on issues seen (#126607) 1. Agent is sometimes ignoring the result of the version calculation script - make it more clear how to handle results. 2. Comment was duplicating issue body. Remove this. 3. Tag the PR's assignees to get attention. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Build-IssueComment.ps1 | 52 ++++++++----------- .github/skills/breaking-change-doc/SKILL.md | 30 ++++++++--- 2 files changed, 46 insertions(+), 36 deletions(-) diff --git a/.github/skills/breaking-change-doc/Build-IssueComment.ps1 b/.github/skills/breaking-change-doc/Build-IssueComment.ps1 index 4848f2eaf50e78..6fb00936c92ab8 100644 --- a/.github/skills/breaking-change-doc/Build-IssueComment.ps1 +++ b/.github/skills/breaking-change-doc/Build-IssueComment.ps1 @@ -1,8 +1,8 @@ # Build-IssueComment.ps1 # Reads a breaking change issue draft markdown file, URL-encodes the title, -# body, and labels, and produces a PR comment markdown file containing: -# - A header -# - The full draft for inline review +# body, and labels, and produces a brief PR comment markdown file containing: +# - A short header with instructions +# - @mentions of the PR assignees so they get notified # - A clickable link that pre-fills a new issue in dotnet/docs # - An email reminder # @@ -10,6 +10,7 @@ # pwsh .github/skills/breaking-change-doc/Build-IssueComment.ps1 ` # -IssueDraftPath issue-draft.md ` # -Title "[Breaking change]: Something changed" ` +# -Assignees "@user1 @user2" ` # -OutputPath pr-comment.md # # The issue draft file should contain only the issue body markdown (no title). @@ -21,6 +22,8 @@ param( [Parameter(Mandatory = $true)] [string]$Title, + [string]$Assignees = "", + [string]$OutputPath = "pr-comment.md", [string]$Labels = "breaking-change,Pri1,doc-idea", @@ -46,43 +49,34 @@ $encodedEmailSubject = [Uri]::EscapeDataString("[Breaking Change] $Title") $issueUrl = "https://github.com/$DocsRepo/issues/new?title=$encodedTitle&body=$encodedBody&labels=$encodedLabels" $notificationEmailUrl = "mailto:dotnetbcn@microsoft.com?subject=$encodedEmailSubject" +# Build a brief assignee mention line (if any) +$mentionLine = "" +$trimmedAssignees = "" +if (-not [string]::IsNullOrWhiteSpace($Assignees)) { + $trimmedAssignees = $Assignees.Trim() + $mentionLine = "`n`n/cc $trimmedAssignees" +} + $comment = @" ## Breaking Change Documentation -$issueBody +A breaking change draft has been prepared for this PR. ---- +:point_right: **[Click here to create the issue in $DocsRepo]($issueUrl)** + +After creating the issue, please email a link to it to +[.NET Breaking Change Notifications]($notificationEmailUrl).$mentionLine > [!NOTE] > This documentation was generated with AI assistance from Copilot. - -:point_right: **[Click here to create the issue in dotnet/docs]($issueUrl)** - -After creating the issue, please email a link to it to -[.NET Breaking Change Notifications]($notificationEmailUrl). "@ -# GitHub comment body limit is 65536 characters. If the comment exceeds this, -# replace the inline draft with a short summary pointing at the file. +# GitHub comment body limit is 65536 characters. The comment is now brief, +# but the URL itself can be very long. Warn if the comment exceeds the +# configured comment-length threshold; the URL length is checked separately below. $maxCommentLength = 65000 if ($comment.Length -gt $maxCommentLength) { - Write-Warning "Comment body ($($comment.Length) chars) exceeds GitHub limit. Truncating inline draft." - $comment = @" -## Breaking Change Documentation - -The full draft is too large to display inline. See ``issue-draft.md`` in the -workflow artifacts for the complete content. - ---- - -> [!NOTE] -> This documentation was generated with AI assistance from Copilot. - -:point_right: **[Click here to create the issue in dotnet/docs]($issueUrl)** - -After creating the issue, please email a link to it to -[.NET Breaking Change Notifications]($notificationEmailUrl). -"@ + Write-Warning "Comment body ($($comment.Length) chars) exceeds GitHub limit." } $comment | Out-File -FilePath $OutputPath -Encoding UTF8 -NoNewline diff --git a/.github/skills/breaking-change-doc/SKILL.md b/.github/skills/breaking-change-doc/SKILL.md index 5a363add915ba3..ced83b253d7e50 100644 --- a/.github/skills/breaking-change-doc/SKILL.md +++ b/.github/skills/breaking-change-doc/SKILL.md @@ -64,7 +64,7 @@ Extract the PR number. The source repository is always `dotnet/runtime`. Use GitHub tools to read comprehensive PR data. Collect **all** of the following: -1. **PR metadata**: title, author, base branch, merge commit SHA, merged-at date, labels, state. +1. **PR metadata**: title, author, assignees, base branch, merge commit SHA, merged-at date, labels, state. 2. **PR body**: the full description. 3. **Changed files**: list of file paths modified. 4. **PR comments and reviews**: read all comments and review comments for context about the change's impact. @@ -105,17 +105,22 @@ Run the helper script to determine the .NET version context: pwsh .github/skills/breaking-change-doc/Get-VersionInfo.ps1 -PrNumber ``` -The script outputs JSON with: +**You MUST display the complete script output.** The script outputs JSON that includes: - `LastTagBeforeMerge` — the closest release tag before the merge commit - `FirstTagWithChange` — the first tag that contains this commit (or "Not yet released") - `EstimatedVersion` — human-readable version string like ".NET 11 Preview 3" - `MergeCommit` — the merge commit SHA - `MergedAt` — when the PR was merged +- `BaseRef` — the PR base branch used to determine version context -Use `EstimatedVersion` as the version for the breaking change issue. +After running the script, **print the full JSON output** so it is visible in the +workflow log. Then check the JSON: -If the script returns an error, fall back to reading the PR's base branch and recent -tags manually to estimate the version. +- If the JSON contains an `Error` field, report the error and fall back to + reading the PR's base branch and recent tags manually to estimate the version. +- If the JSON does **not** contain an `Error` field, use `EstimatedVersion` as + the version for the breaking change issue. **Do not fall back to manual + detection when the script succeeds.** --- @@ -229,21 +234,32 @@ mkdir -p artifacts/docs/breakingChanges ### Build the PR comment file -Run the helper script to produce a PR comment with a URL-encoded issue link: +Run the helper script to produce a PR comment with a URL-encoded issue link. +Pass the PR assignees so they are `@`-mentioned in the comment and receive a +GitHub notification: ```bash pwsh .github/skills/breaking-change-doc/Build-IssueComment.ps1 \ -IssueDraftPath artifacts/docs/breakingChanges/issue-draft.md \ -Title "" \ + -Assignees "@user1 @user2" \ -OutputPath artifacts/docs/breakingChanges/pr-comment.md ``` +Build the `-Assignees` value from the PR's assignee list (from Step 1 metadata), +prefixing each GitHub username with `@` and separating with spaces. + The script: - URL-encodes the title, body, and labels using `[Uri]::EscapeDataString` - Builds a clickable `https://github.com/dotnet/docs/issues/new?...` link -- Writes `pr-comment.md` containing the full draft, the link, and an email reminder +- Writes a **brief** `pr-comment.md` with instructions, the link, an email + reminder, and `/cc` mentions of the assignees - Warns if the URL exceeds browser length limits +The comment intentionally does **not** duplicate the full issue draft. The draft +is available in the `issue-draft.md` workflow artifact and is pre-filled in the +issue creation link. + ### Post the comment When running interactively (not in dry-run mode), post the contents of From 0b9b47d80e43da04bd6056470627887289aafe2e Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Thu, 30 Apr 2026 02:45:43 +0200 Subject: [PATCH 027/115] Remove unsafe code from DCS utilities (UniqueId, XmlBufferReader) (#127539) More code can be replaced with safe in this assembly, but that requires work in the jit side. Current change produces improvements: [diffs](https://github.com/MihuBot/runtime-utils/issues/1870) --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/Xml/UniqueId.cs | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/libraries/System.Private.DataContractSerialization/src/System/Xml/UniqueId.cs b/src/libraries/System.Private.DataContractSerialization/src/System/Xml/UniqueId.cs index 6cfffa4b867403..4377e2d448655b 100644 --- a/src/libraries/System.Private.DataContractSerialization/src/System/Xml/UniqueId.cs +++ b/src/libraries/System.Private.DataContractSerialization/src/System/Xml/UniqueId.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers.Binary; using System.Diagnostics.CodeAnalysis; namespace System.Xml @@ -62,7 +63,7 @@ public UniqueId(byte[] guid) : this(guid, 0) { } - public unsafe UniqueId(byte[] guid, int offset) + public UniqueId(byte[] guid, int offset) { ArgumentNullException.ThrowIfNull(guid); @@ -71,11 +72,10 @@ public unsafe UniqueId(byte[] guid, int offset) throw new ArgumentOutOfRangeException(nameof(offset), SR.Format(SR.OffsetExceedsBufferSize, guid.Length)); if (guidLength > guid.Length - offset) throw new ArgumentException(SR.Format(SR.XmlArrayTooSmallInput, guidLength), nameof(guid)); - fixed (byte* pb = &guid[offset]) - { - _idLow = UnsafeGetInt64(pb); - _idHigh = UnsafeGetInt64(&pb[8]); - } + + ReadOnlySpan source = guid.AsSpan(offset, guidLength); + _idLow = BinaryPrimitives.ReadInt64LittleEndian(source); + _idHigh = BinaryPrimitives.ReadInt64LittleEndian(source.Slice(8)); } public unsafe UniqueId(string value) @@ -274,7 +274,7 @@ public bool TryGetGuid(out Guid guid) return true; } - public unsafe bool TryGetGuid(byte[] buffer, int offset) + public bool TryGetGuid(byte[] buffer, int offset) { if (!IsGuid) return false; @@ -288,11 +288,9 @@ public unsafe bool TryGetGuid(byte[] buffer, int offset) if (guidLength > buffer.Length - offset) throw new ArgumentOutOfRangeException(nameof(buffer), SR.Format(SR.XmlArrayTooSmallOutput, guidLength)); - fixed (byte* pb = &buffer[offset]) - { - UnsafeSetInt64(_idLow, pb); - UnsafeSetInt64(_idHigh, &pb[8]); - } + Span destination = buffer.AsSpan(offset, guidLength); + BinaryPrimitives.WriteInt64LittleEndian(destination, _idLow); + BinaryPrimitives.WriteInt64LittleEndian(destination.Slice(8), _idHigh); return true; } From eb89df07b50fd90b50a0d7df5f5cb2936e32f19a Mon Sep 17 00:00:00 2001 From: Adam Perlin Date: Wed, 29 Apr 2026 18:16:28 -0700 Subject: [PATCH 028/115] [Wasm RyuJIT]: Fix LIR Semantics in Stackifier Output (#127412) This is a fix for an issue that came up in #126778, and is probably easiest to explain with a motivating example. Consider the following case, where NOMOVE is a gentree operation we aren't allowed to move. ``` t2 = ... NOMOVE OP t3 = ... OP t0 = ... NOMOVE OP t1 = ... OP * t3 (arg1) * t2 (arg2) * t1 (arg3) * t0 (target) CALL ``` The stackifier will first introduce a store to put `t0` after `t1`: ``` t2 = ... NOMOVE OP t3 = ... OP t0 = ... OP +** STORE_LCL_VAR tmp0 t1 = ... OP t0 = LCL_VAR tmp0 * t3 (arg1) * t2 (arg2) * t1 (arg3) * t0 (call target) CALL ``` And then recursively stackify the new STORE to tmp0, since it is a dataflow root. *The stackifier then marks tmp0 as free here, since it IS free in linear data flow order*. Then, when the next operands to the call are stackified, the stackifier introduces a temporary again, but reuses t0 because we freed it. ``` t2 = ... OP +** STORE_LCL_VAR tmp0 t3 = ... OP t2 = LCL_VAR tmp0 t0 = ... OP +** STORE_LCL_VAR tmp0 t1 = ... OP t0 = LCL_VAR tmp0 * t3 * t2 * t1 * t0 (target) CALL ``` This produces invalid LIR; there is a store to tmp0 before one of its reads (t2) is consumed. The simplest fix is to not release temporaries for reuse until all operands of a root tree have been processed. This PR adds a free list of temps which is recycled after each root gen tree has been processed, so we won't end up with any interference between temporaries while processing gentree ops which share a parent node. --------- Co-authored-by: SingleAccretion <62474226+SingleAccretion@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/coreclr/jit/lowerwasm.cpp | 52 +++++++++++++++++------------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/coreclr/jit/lowerwasm.cpp b/src/coreclr/jit/lowerwasm.cpp index ab9c56164fa1a8..7c8659fad3c28a 100644 --- a/src/coreclr/jit/lowerwasm.cpp +++ b/src/coreclr/jit/lowerwasm.cpp @@ -531,7 +531,7 @@ void Lowering::AfterLowerBlocks() ArrayStack m_stack; unsigned m_minimumTempLclNum; Temporary* m_availableTemps[TYP_COUNT] = {}; - Temporary* m_unusedTempNodes = nullptr; + Temporary* m_inUseTemps[TYP_COUNT] = {}; bool m_anyChanges = false; public: @@ -552,12 +552,14 @@ void Lowering::AfterLowerBlocks() { assert(IsDataFlowRoot(node)); node = StackifyTree(node); + // We don't track liveness of temporaries more precisely since introducing earlier uses + // may interfere with later (by that point already inserted and stackified) stores. + ReleaseTemporaries(); } m_lower->m_block = nullptr; JITDUMP(FMT_BB ": %s\n", block->bbNum, m_anyChanges ? "stackified with some changes" : "already in WASM value stack order"); - assert((m_unusedTempNodes == nullptr) && "Some temporaries were not released"); } GenTree* StackifyTree(GenTree* root) @@ -567,7 +569,6 @@ void Lowering::AfterLowerBlocks() // Simple greedy algorithm working backwards. The invariant is that the stack top must be placed right next // to (in normal linear order - before) the node we last stackified. m_stack.Push(&root); - ReleaseTemporariesDefinedBy(root); GenTree* lastStackified = root->gtNext; while (m_stack.Height() != initialDepth) @@ -690,47 +691,46 @@ void Lowering::AfterLowerBlocks() if (local != nullptr) { lclNum = local->LclNum; - Append(&m_unusedTempNodes, local); // Free the node for later recycling. assert(m_compiler->lvaGetDesc(lclNum)->TypeGet() == genActualType(type)); } else { - lclNum = m_compiler->lvaGrabTemp(true DEBUGARG("Stackifier temporary")); + lclNum = m_compiler->lvaGrabTemp(true DEBUGARG("Stackifier temporary")); + assert(lclNum >= m_minimumTempLclNum); LclVarDsc* varDsc = m_compiler->lvaGetDesc(lclNum); varDsc->lvType = genActualType(type); - assert(lclNum >= m_minimumTempLclNum); + + // Allocate a new temporary to describe this local + local = new (m_compiler, CMK_Lower) Temporary(); + local->LclNum = lclNum; } + Append(&m_inUseTemps[genActualType(type)], local); + JITDUMP("Temporary V%02u is now in use\n", lclNum); return lclNum; } - void ReleaseTemporariesDefinedBy(GenTree* node) + void ReleaseTemporaries() { - // We rely in this function on the lifetime of temporaries beginning (recall this is backwards traversal) - // at exactly "node"'s position, and not shrinking or extending after this call. This is currently true - // because we never move dataflow roots, and we only begin processing them after all subsequent nodes - // have already been stackified and thus won't move either. - assert(IsDataFlowRoot(node)); - if (!node->OperIs(GT_STORE_LCL_VAR)) + if (m_minimumTempLclNum == m_compiler->lvaCount) { + // No temporaries were created return; } + assert(m_minimumTempLclNum < m_compiler->lvaCount); - unsigned lclNum = node->AsLclVar()->GetLclNum(); - if (lclNum < m_minimumTempLclNum) + JITDUMP("Releasing stackifier temporaries:\n"); + // Reclaim all in-use temporaries + for (int i = 0; i < TYP_COUNT; i++) { - return; - } - - Temporary* local = Remove(&m_unusedTempNodes); // See if we have any free nodes in the pool. - if (local == nullptr) - { - local = new (m_compiler, CMK_Lower) Temporary(); + while (m_inUseTemps[i] != nullptr) + { + Temporary* temp = Remove(&m_inUseTemps[i]); + assert(temp->LclNum >= m_minimumTempLclNum); + Append(&m_availableTemps[i], temp); + JITDUMP("Temporary V%02u is now available\n", temp->LclNum); + } } - local->LclNum = lclNum; - - JITDUMP("Temporary V%02u is now free and can be re-used\n", lclNum); - Append(&m_availableTemps[genActualType(node->TypeGet())], local); } Temporary* Remove(Temporary** pTemps) From 8f854afd60f520eed7b267f31cf39996c062213c Mon Sep 17 00:00:00 2001 From: Adam Perlin Date: Wed, 29 Apr 2026 18:18:12 -0700 Subject: [PATCH 029/115] [Wasm RyuJit] Call Codegen Fixes for R2R (#126778) Initially cherry-picked commit from #126662; This PR: - Adds explicit handling in lowering for managed calls using the PEP calling convention to pass the additional PEP parameter. - Removes some dead code around handling of `WellKnownArg::R2RIndireictionCell` from wasm code gen. - Adds fixes for helper call codegen to account for the extra level of indirection needed to load a helper call target from a PEP (Portable Entrypoint). ## Lowering Transformation Details The lowering transformation introduces a temp local (call it `pep`) to hold the PEP, which we then add to a managed call as a final parameter with associated ABI info. We then rewrite the control expr of the call to be `*pep`. This ensures that, conceptually, any call with an entrypoint of access type `PVALUE` will be lowered to something roughly like the following, in C notation: ``` void* pep = *(void**)(); (*(void**)pep)(arg0, arg1, ..., argN, pep) ``` --------- Co-authored-by: David Wrighton Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/coreclr/jit/codegenwasm.cpp | 68 +++++++++++++-------------------- src/coreclr/jit/gentree.h | 2 +- src/coreclr/jit/lower.cpp | 9 +++++ src/coreclr/jit/lower.h | 7 +++- src/coreclr/jit/lowerwasm.cpp | 65 +++++++++++++++++++++++++++++++ src/coreclr/jit/morph.cpp | 21 +++++----- 6 files changed, 118 insertions(+), 54 deletions(-) diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index a9d5308d63ee41..dc6b2946c22ebe 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -2515,6 +2515,9 @@ void CodeGen::genCallInstruction(GenTreeCall* call) params.wasmSignature = m_compiler->info.compCompHnd->getWasmTypeSymbol(typeStack.Data(), typeStack.Height()); + // A non-null target expression always indicates an indirect call on Wasm, + // as currently the only possible result of the target expression would be a + // table index which must be used via call_indirect if (target != nullptr) { // Codegen should have already evaluated our target node (last) and pushed it onto the stack, @@ -2526,52 +2529,28 @@ void CodeGen::genCallInstruction(GenTreeCall* call) } else { - // If we have no target and this is a call with indirection cell then - // we do an optimization where we load the call address directly from - // the indirection cell instead of duplicating the tree. In BuildCall - // we ensure that get an extra register for the purpose. Note that for - // CFG the call might have changed to - // CORINFO_HELP_DISPATCH_INDIRECT_CALL in which case we still have the - // indirection cell but we should not try to optimize. - WellKnownArg indirectionCellArgKind = WellKnownArg::None; - if (!call->IsHelperCall(CORINFO_HELP_DISPATCH_INDIRECT_CALL)) - { - indirectionCellArgKind = call->GetIndirectionCellArgKind(); - } + // Generate a direct call to a non-virtual user defined or helper method + assert(call->IsHelperCall() || (call->gtCallType == CT_USER_FUNC)); - if (indirectionCellArgKind != WellKnownArg::None) - { - assert(call->IsR2ROrVirtualStubRelativeIndir()); + assert(call->gtEntryPoint.addr == NULL); - params.callType = EC_INDIR_R; - // params.ireg = targetAddrReg; - genEmitCallWithCurrentGC(params); + if (call->IsHelperCall()) + { + assert(!call->IsFastTailCall()); + CorInfoHelpFunc helperNum = m_compiler->eeGetHelperNum(params.methHnd); + noway_assert(helperNum != CORINFO_HELP_UNDEF); + CORINFO_CONST_LOOKUP helperLookup = m_compiler->compGetHelperFtn(helperNum); + assert(helperLookup.accessType == IAT_VALUE); + params.addr = helperLookup.addr; } else { - // Generate a direct call to a non-virtual user defined or helper method - assert(call->IsHelperCall() || (call->gtCallType == CT_USER_FUNC)); - - assert(call->gtEntryPoint.addr == NULL); - - if (call->IsHelperCall()) - { - assert(!call->IsFastTailCall()); - CorInfoHelpFunc helperNum = m_compiler->eeGetHelperNum(params.methHnd); - noway_assert(helperNum != CORINFO_HELP_UNDEF); - CORINFO_CONST_LOOKUP helperLookup = m_compiler->compGetHelperFtn(helperNum); - assert(helperLookup.accessType == IAT_VALUE); - params.addr = helperLookup.addr; - } - else - { - // Direct call to a non-virtual user function. - params.addr = call->gtDirectCallAddress; - } - - params.callType = EC_FUNC_TOKEN; - genEmitCallWithCurrentGC(params); + // Direct call to a non-virtual user function. + params.addr = call->gtDirectCallAddress; } + + params.callType = EC_FUNC_TOKEN; + genEmitCallWithCurrentGC(params); } } @@ -2676,16 +2655,21 @@ void CodeGen::genEmitHelperCall(unsigned helper, int argSize, emitAttr retSize, if (helperIsManaged) { // Push PEP onto the stack because we are calling a managed helper that expects it as the last parameter. + // The helper function address is the address of an indirection cell, so we load from the cell to get the PEP + // address to push. assert(helperFunction.accessType == IAT_PVALUE); GetEmitter()->emitAddressConstant(helperFunction.addr); + GetEmitter()->emitIns_I(INS_I_load, EA_PTRSIZE, 0); } if (params.callType == EC_INDIR_R) { - // Push the call target onto the wasm evaluation stack by dereferencing the PEP. + // Push the call target onto the wasm evaluation stack by dereferencing the indirection cell + // and then the PEP pointed to by the indirection cell. assert(helperFunction.accessType == IAT_PVALUE); GetEmitter()->emitAddressConstant(helperFunction.addr); - GetEmitter()->emitIns_I(INS_i32_load, EA_PTRSIZE, 0); + GetEmitter()->emitIns_I(INS_I_load, EA_PTRSIZE, 0); + GetEmitter()->emitIns_I(INS_I_load, EA_PTRSIZE, 0); } genEmitCallWithCurrentGC(params); diff --git a/src/coreclr/jit/gentree.h b/src/coreclr/jit/gentree.h index 490ea6c08f1d40..c9c965960083ea 100644 --- a/src/coreclr/jit/gentree.h +++ b/src/coreclr/jit/gentree.h @@ -5771,7 +5771,7 @@ struct GenTreeCall final : public GenTree return WellKnownArg::VirtualStubCell; } -#if defined(TARGET_ARMARCH) || defined(TARGET_RISCV64) || defined(TARGET_LOONGARCH64) || defined(TARGET_WASM) +#if defined(TARGET_ARMARCH) || defined(TARGET_RISCV64) || defined(TARGET_LOONGARCH64) // For ARM architectures, we always use an indirection cell for R2R calls. if (IsR2RRelativeIndir() && !IsDelegateInvoke()) { diff --git a/src/coreclr/jit/lower.cpp b/src/coreclr/jit/lower.cpp index c6fcb6cc229502..5f8f8166f57fa9 100644 --- a/src/coreclr/jit/lower.cpp +++ b/src/coreclr/jit/lower.cpp @@ -3028,6 +3028,15 @@ GenTree* Lowering::LowerCall(GenTree* node) } } +#ifdef TARGET_WASM + // For any type of managed call, if we have portable entry points enabled, we need to lower + // the call according to the portable entrypoint abi + if (!call->IsUnmanaged() && m_compiler->opts.jitFlags->IsSet(JitFlags::JIT_FLAG_PORTABLE_ENTRY_POINTS)) + { + LowerPEPCall(call); + } +#endif // TARGET_WASM + if (varTypeIsStruct(call)) { LowerCallStruct(call); diff --git a/src/coreclr/jit/lower.h b/src/coreclr/jit/lower.h index a3e7b8f8356210..b4e8b2dcf03aad 100644 --- a/src/coreclr/jit/lower.h +++ b/src/coreclr/jit/lower.h @@ -153,8 +153,11 @@ class Lowering final : public Phase bool LowerCallMemcmp(GenTreeCall* call, GenTree** next); bool LowerCallMemset(GenTreeCall* call, GenTree** next); void LowerCFGCall(GenTreeCall* call); - void MovePutArgNodesUpToCall(GenTreeCall* call); - void MovePutArgUpToCall(GenTreeCall* call, GenTree* node); +#ifdef TARGET_WASM + void LowerPEPCall(GenTreeCall* call); +#endif + void MovePutArgNodesUpToCall(GenTreeCall* call); + void MovePutArgUpToCall(GenTreeCall* call, GenTree* node); #ifndef TARGET_64BIT GenTree* DecomposeLongCompare(GenTree* cmp); #endif diff --git a/src/coreclr/jit/lowerwasm.cpp b/src/coreclr/jit/lowerwasm.cpp index 7c8659fad3c28a..2d1b1895790753 100644 --- a/src/coreclr/jit/lowerwasm.cpp +++ b/src/coreclr/jit/lowerwasm.cpp @@ -41,6 +41,71 @@ bool Lowering::IsCallTargetInRange(void* addr) return true; } +//--------------------------------------------------------------------------------------------- +// LowerPEPCall: Lower a call node dispatched through a PortableEntryPoint (PEP) +// +// Given a call node with gtControlExpr representing a call target which is the address of a portable entrypoint, +// this function lowers the call to appropriately dispatch through the portable entrypoint using the Portable +// entrypoint calling convention. +// To do this, it: +// 1. Introduces a new local variable to hold the PEP address +// 2. Adds a new well-known argument to the call passing this local +// 3. Rewrites the control expression to indirect through the new local, since for PEP's, the actual call target +// must be loaded from the portable entry point address. +// +// Arguments: +// call - The call node to lower. It is expected that the call node has gtControlExpr set to the original +// call target and that the call does not have a PEP arg already. +// +// Return Value: +// None. The call node is modified in place. +// +void Lowering::LowerPEPCall(GenTreeCall* call) +{ + JITDUMP("Begin lowering PEP call\n"); + DISPTREERANGE(BlockRange(), call); + + // PEP call must always have a control expression + assert(call->gtControlExpr != nullptr); + LIR::Use callTargetUse(BlockRange(), &call->gtControlExpr, call); + + JITDUMP("Creating new local variable for PEP"); + unsigned int callTargetLclNum = callTargetUse.ReplaceWithLclVar(m_compiler); + GenTreeLclVar* callTargetLclForArg = m_compiler->gtNewLclvNode(callTargetLclNum, TYP_I_IMPL); + DISPTREE(call); + + JITDUMP("Add new arg to call arg list corresponding to PEP target"); + NewCallArg pepTargetArg = + NewCallArg::Primitive(callTargetLclForArg).WellKnown(WellKnownArg::WasmPortableEntryPoint); + CallArg* pepArg = call->gtArgs.PushBack(m_compiler, pepTargetArg); + + pepArg->SetEarlyNode(nullptr); + pepArg->SetLateNode(callTargetLclForArg); + call->gtArgs.PushLateBack(pepArg); + + // Set up ABI information for this arg; PEP's should be passed as the last param to a wasm function + unsigned pepIndex = call->gtArgs.CountArgs() - 1; + regNumber pepReg = MakeWasmReg(pepIndex, WasmValueType::I); + pepArg->AbiInfo = + ABIPassingInformation::FromSegmentByValue(m_compiler, + ABIPassingSegment::InRegister(pepReg, 0, TARGET_POINTER_SIZE)); + BlockRange().InsertBefore(call, callTargetLclForArg); + + // Lower the new PEP arg now that the call abi info is updated and lcl var is inserted + LowerArg(call, pepArg); + DISPTREE(call); + + JITDUMP("Rewrite PEP call's control expression to indirect through the new local variable\n"); + // Rewrite the call's control expression to have an additional load from the PEP local + GenTree* controlExpr = call->gtControlExpr; + GenTree* target = Ind(controlExpr); + BlockRange().InsertAfter(controlExpr, target); + call->gtControlExpr = target; + + JITDUMP("Finished lowering PEP call\n"); + DISPTREERANGE(BlockRange(), call); +} + //------------------------------------------------------------------------ // IsContainableImmed: Is an immediate encodable in-place? // diff --git a/src/coreclr/jit/morph.cpp b/src/coreclr/jit/morph.cpp index 7caba07734be6d..fe982668bce51c 100644 --- a/src/coreclr/jit/morph.cpp +++ b/src/coreclr/jit/morph.cpp @@ -1756,19 +1756,27 @@ void CallArgs::AddFinalArgsAndDetermineABIInfo(Compiler* comp, GenTreeCall* call // That's ok; after making something a tailcall, we will invalidate this information // and reconstruct it if necessary. The tailcalling decision does not change since // this is a non-standard arg in a register. - bool needsIndirectionCell = call->IsR2RRelativeIndir() && !call->IsDelegateInvoke(); +#ifndef TARGET_WASM + bool needsIndirectionCellArg = call->IsR2RRelativeIndir() && !call->IsDelegateInvoke(); + #if defined(TARGET_XARCH) - needsIndirectionCell &= call->IsFastTailCall(); + needsIndirectionCellArg &= call->IsFastTailCall(); +#endif + +#else + // TARGET_WASM does not use an explicit indirection cell arg for the R2R calling convention, + // the address of the indirection cell is recoverable from the portable entrypoint which + // we pass as part of the Wasm managed calling convention (See LowerPEPCall). + bool needsIndirectionCellArg = false; #endif - if (needsIndirectionCell) + if (needsIndirectionCellArg) { assert(call->gtEntryPoint.addr != nullptr); size_t addrValue = (size_t)call->gtEntryPoint.addr; GenTree* indirectCellAddress = comp->gtNewIconHandleNode(addrValue, GTF_ICON_FTN_ADDR); INDEBUG(indirectCellAddress->AsIntCon()->gtTargetHandle = (size_t)call->gtCallMethHnd); - #ifdef TARGET_ARM // TODO-ARM: We currently do not properly kill this register in LSRA // (see getKillSetForCall which does so only for VSD calls). @@ -1781,12 +1789,7 @@ void CallArgs::AddFinalArgsAndDetermineABIInfo(Compiler* comp, GenTreeCall* call // Push the stub address onto the list of arguments. NewCallArg indirCellAddrArg = NewCallArg::Primitive(indirectCellAddress).WellKnown(WellKnownArg::R2RIndirectionCell); -#ifdef TARGET_WASM - // On wasm we need to ensure we put the indirection cell address last in LIR, after the SP and formal args. - PushBack(comp, indirCellAddrArg); -#else InsertAfterThisOrFirst(comp, indirCellAddrArg); -#endif // TARGET_WASM } #endif // defined(FEATURE_READYTORUN) From 8b7fda5eb3e6849a380afbded2a2a978f3e98876 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 07:21:50 +0000 Subject: [PATCH 030/115] Add Process.ReadAllLines synchronous API (#127106) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> Co-authored-by: Adam Sitnik Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../ref/System.Diagnostics.Process.cs | 1 + .../Diagnostics/Process.Multiplexing.Unix.cs | 300 ++++++++-- .../Process.Multiplexing.Windows.cs | 193 +++++++ .../Diagnostics/Process.Multiplexing.cs | 258 ++++++++- .../tests/ProcessStreamingTests.cs | 546 ++++++++++++++---- 5 files changed, 1128 insertions(+), 170 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs index 0907c2dc7b69f1..85682d07a03c30 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs @@ -170,6 +170,7 @@ public static void LeaveDebugMode() { } protected void OnExited() { } public (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; } public System.Threading.Tasks.Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public System.Collections.Generic.IEnumerable ReadAllLines(System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; } public System.Collections.Generic.IAsyncEnumerable ReadAllLinesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public (string StandardOutput, string StandardError) ReadAllText(System.TimeSpan? timeout = default(System.TimeSpan?)) { throw null; } public System.Threading.Tasks.Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.Unix.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.Unix.cs index 27393f1d50a8f2..d48cc914a766b6 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.Unix.cs @@ -1,10 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; +using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.IO.Pipes; -using System.Runtime.InteropServices; +using System.Text; using Microsoft.Win32.SafeHandles; namespace System.Diagnostics @@ -13,6 +15,248 @@ public partial class Process { private static SafePipeHandle GetSafeHandleFromStreamReader(StreamReader reader) => ((AnonymousPipeClientStream)reader.BaseStream).SafePipeHandle; + /// + /// Reads from both standard output and standard error pipes as lines of text using Unix + /// poll-based multiplexing with non-blocking reads. + /// Buffers are rented from the pool and returned when enumeration completes. + /// + private IEnumerable ReadPipesToLines( + int timeoutMs, + Encoding outputEncoding, + Encoding errorEncoding) + { + SafePipeHandle outputHandle = GetSafeHandleFromStreamReader(_standardOutput!); + SafePipeHandle errorHandle = GetSafeHandleFromStreamReader(_standardError!); + + byte[] outputByteBuffer = ArrayPool.Shared.Rent(InitialReadAllBufferSize); + byte[] errorByteBuffer = ArrayPool.Shared.Rent(InitialReadAllBufferSize); + char[] outputCharBuffer = ArrayPool.Shared.Rent(InitialReadAllBufferSize); + char[] errorCharBuffer = ArrayPool.Shared.Rent(InitialReadAllBufferSize); + bool outputRefAdded = false, errorRefAdded = false; + + try + { + outputHandle.DangerousAddRef(ref outputRefAdded); + errorHandle.DangerousAddRef(ref errorRefAdded); + + int outputFd = outputHandle.DangerousGetHandle().ToInt32(); + int errorFd = errorHandle.DangerousGetHandle().ToInt32(); + + if (Interop.Sys.Fcntl.DangerousSetIsNonBlocking(outputFd, 1) != 0 + || Interop.Sys.Fcntl.DangerousSetIsNonBlocking(errorFd, 1) != 0) + { + throw new Win32Exception(); + } + + // Cannot use stackalloc in an iterator method; use a regular array. + Interop.PollEvent[] pollFds = new Interop.PollEvent[2]; + + long deadline = timeoutMs >= 0 ? Environment.TickCount64 + timeoutMs : long.MaxValue; + + Decoder outputDecoder = outputEncoding.GetDecoder(); + Decoder errorDecoder = errorEncoding.GetDecoder(); + int outputCharStart = 0, outputCharEnd = 0; + int errorCharStart = 0, errorCharEnd = 0; + int unconsumedOutputBytesCount = 0, unconsumedErrorBytesCount = 0; + bool outputDone = false, errorDone = false; + bool outputPreambleChecked = false, errorPreambleChecked = false; + + List lines = new(); + + while (!outputDone || !errorDone) + { + int numFds = PollForPipeActivity(pollFds, errorFd, outputFd, errorDone, outputDone, deadline, timeoutMs, out int errorIndex, out int outputIndex); + + // Process error pipe first (lower index) when both have data available. + for (int i = 0; i < numFds; i++) + { + if (pollFds[i].TriggeredEvents == Interop.PollEvents.POLLNONE) + { + continue; + } + + bool isError = i == errorIndex; + SafePipeHandle currentHandle = isError ? errorHandle : outputHandle; + + // Use explicit branching to avoid ref locals across yield points. + if (isError) + { + HandlePipeLineRead(currentHandle, ref errorDecoder, ref errorEncoding, + errorByteBuffer, ref unconsumedErrorBytesCount, + ref errorCharBuffer, ref errorCharStart, ref errorCharEnd, + ref errorPreambleChecked, ref errorDone, isError, lines); + } + else + { + HandlePipeLineRead(currentHandle, ref outputDecoder, ref outputEncoding, + outputByteBuffer, ref unconsumedOutputBytesCount, + ref outputCharBuffer, ref outputCharStart, ref outputCharEnd, + ref outputPreambleChecked, ref outputDone, isError, lines); + } + } + + // Yield parsed lines outside of any ref-local scope. + foreach (ProcessOutputLine line in lines) + { + yield return line; + } + + lines.Clear(); + } + } + finally + { + if (outputRefAdded) + { + outputHandle.DangerousRelease(); + } + + if (errorRefAdded) + { + errorHandle.DangerousRelease(); + } + + ArrayPool.Shared.Return(outputByteBuffer); + ArrayPool.Shared.Return(errorByteBuffer); + ArrayPool.Shared.Return(outputCharBuffer); + ArrayPool.Shared.Return(errorCharBuffer); + } + } + + /// + /// Populates the poll fd array with the active pipe file descriptors. + /// Error is added first so it gets serviced first when both have data. + /// Returns the number of active file descriptors. + /// + private static int PreparePollFds( + Span pollFds, + int errorFd, int outputFd, + bool errorDone, bool outputDone, + out int errorIndex, out int outputIndex) + { + int numFds = 0; + errorIndex = -1; + outputIndex = -1; + + if (!errorDone) + { + errorIndex = numFds; + pollFds[numFds].FileDescriptor = errorFd; + pollFds[numFds].Events = Interop.PollEvents.POLLIN; + pollFds[numFds].TriggeredEvents = Interop.PollEvents.POLLNONE; + numFds++; + } + + if (!outputDone) + { + outputIndex = numFds; + pollFds[numFds].FileDescriptor = outputFd; + pollFds[numFds].Events = Interop.PollEvents.POLLIN; + pollFds[numFds].TriggeredEvents = Interop.PollEvents.POLLNONE; + numFds++; + } + + return numFds; + } + + /// + /// Prepares the poll fd array, checks the remaining timeout, calls poll(2), and handles + /// errors. Returns the number of polled fds, or 0 if poll was interrupted (EINTR) and + /// the caller should retry. + /// + private static int PollForPipeActivity( + Span pollFds, + int errorFd, int outputFd, + bool errorDone, bool outputDone, + long deadline, int timeoutMs, + out int errorIndex, out int outputIndex) + { + int numFds = PreparePollFds(pollFds, errorFd, outputFd, errorDone, outputDone, out errorIndex, out outputIndex); + + if (!TryGetRemainingTimeout(deadline, timeoutMs, out int pollTimeout)) + { + throw new TimeoutException(); + } + + uint triggered = 0; + Interop.Error pollError; + unsafe + { + fixed (Interop.PollEvent* pPollFds = pollFds) + { + pollError = Interop.Sys.Poll(pPollFds, (uint)numFds, pollTimeout, &triggered); + } + } + + if (pollError != Interop.Error.SUCCESS) + { + if (pollError == Interop.Error.EINTR) + { + return 0; + } + + throw new Win32Exception(Interop.Sys.ConvertErrorPalToPlatform(pollError)); + } + + if (triggered == 0) + { + throw new TimeoutException(); + } + + return numFds; + } + + /// + /// Handles a poll notification for a single pipe: reads bytes, decodes to chars, + /// strips BOM on first decode, parses lines, compacts the char buffer, and sets + /// to on EOF. + /// + private static void HandlePipeLineRead( + SafePipeHandle handle, + ref Decoder decoder, + ref Encoding encoding, + byte[] byteBuffer, + ref int unconsumedBytesCount, + ref char[] charBuffer, + ref int charStart, + ref int charEnd, + ref bool preambleChecked, + ref bool done, + bool standardError, + List lines) + { + int bytesRead = ReadNonBlocking(handle, byteBuffer, offset: unconsumedBytesCount); + if (bytesRead > 0) + { + ReadOnlySpan bytes = byteBuffer.AsSpan(0, unconsumedBytesCount + bytesRead); + + if (!preambleChecked) + { + if (bytes.Length >= MaxEncodingBytesLength) + { + bytes = bytes.Slice(SkipPreambleOrDetectEncoding(bytes, ref encoding, ref decoder)); + preambleChecked = true; + unconsumedBytesCount = 0; + } + else + { + unconsumedBytesCount += bytesRead; + } + } + + if (preambleChecked) + { + DecodeBytesAndParseLines(decoder, bytes, ref charBuffer, ref charStart, ref charEnd, standardError, lines); + } + } + else if (bytesRead == 0) + { + done = FlushDecoderAndEmitRemainingChars(preambleChecked, encoding, decoder, byteBuffer.AsSpan(0, unconsumedBytesCount), + ref charBuffer, ref charStart, ref charEnd, standardError, lines); + } + // bytesRead < 0 means EAGAIN — nothing available yet, let poll retry. + } + /// /// Reads from both standard output and standard error pipes using Unix poll-based multiplexing /// with non-blocking reads. @@ -43,59 +287,7 @@ private static void ReadPipes( bool outputDone = false, errorDone = false; while (!outputDone || !errorDone) { - int numFds = 0; - - int outputIndex = -1; - int errorIndex = -1; - - if (!outputDone) - { - outputIndex = numFds; - pollFds[numFds].FileDescriptor = outputFd; - pollFds[numFds].Events = Interop.PollEvents.POLLIN; - pollFds[numFds].TriggeredEvents = Interop.PollEvents.POLLNONE; - numFds++; - } - - if (!errorDone) - { - errorIndex = numFds; - pollFds[numFds].FileDescriptor = errorFd; - pollFds[numFds].Events = Interop.PollEvents.POLLIN; - pollFds[numFds].TriggeredEvents = Interop.PollEvents.POLLNONE; - numFds++; - } - - int pollTimeout; - if (!TryGetRemainingTimeout(deadline, timeoutMs, out pollTimeout)) - { - throw new TimeoutException(); - } - - unsafe - { - uint triggered; - fixed (Interop.PollEvent* pPollFds = pollFds) - { - Interop.Error error = Interop.Sys.Poll(pPollFds, (uint)numFds, pollTimeout, &triggered); - if (error != Interop.Error.SUCCESS) - { - if (error == Interop.Error.EINTR) - { - // We don't re-issue the poll immediately because we need to check - // if we've already exceeded the overall timeout. - continue; - } - - throw new Win32Exception(Interop.Sys.ConvertErrorPalToPlatform(error)); - } - - if (triggered == 0) - { - throw new TimeoutException(); - } - } - } + int numFds = PollForPipeActivity(pollFds, errorFd, outputFd, errorDone, outputDone, deadline, timeoutMs, out int errorIndex, out int outputIndex); for (int i = 0; i < numFds; i++) { diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.Windows.cs index fb30cc07fb254c..4a134a7a77b501 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.Windows.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.Windows.cs @@ -1,9 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; +using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Runtime.InteropServices; +using System.Text; using System.Threading; using Microsoft.Win32.SafeHandles; @@ -13,6 +16,196 @@ public partial class Process { private static SafeFileHandle GetSafeHandleFromStreamReader(StreamReader reader) => ((FileStream)reader.BaseStream).SafeFileHandle; + /// + /// Reads from both standard output and standard error pipes as lines of text using Windows + /// overlapped IO with wait handles for single-threaded synchronous multiplexing. + /// Buffers are rented from the pool and returned when enumeration completes. + /// + private IEnumerable ReadPipesToLines( + int timeoutMs, + Encoding outputEncoding, + Encoding errorEncoding) + { + SafeFileHandle outputHandle = GetSafeHandleFromStreamReader(_standardOutput!); + SafeFileHandle errorHandle = GetSafeHandleFromStreamReader(_standardError!); + + byte[] outputByteBuffer = ArrayPool.Shared.Rent(InitialReadAllBufferSize); + byte[] errorByteBuffer = ArrayPool.Shared.Rent(InitialReadAllBufferSize); + char[] outputCharBuffer = ArrayPool.Shared.Rent(InitialReadAllBufferSize); + char[] errorCharBuffer = ArrayPool.Shared.Rent(InitialReadAllBufferSize); + PinnedGCHandle outputPin = default, errorPin = default; + // NativeOverlapped* can't be used as iterator state machine fields (pointers aren't + // allowed in managed types). Store as nint and cast back inside scoped unsafe blocks. + nint outputOverlappedNint = 0, errorOverlappedNint = 0; + EventWaitHandle? outputEvent = null, errorEvent = null; + bool outputDone = true, errorDone = true; + + try + { + outputPin = new PinnedGCHandle(outputByteBuffer); + errorPin = new PinnedGCHandle(errorByteBuffer); + + outputEvent = new EventWaitHandle(initialState: false, EventResetMode.ManualReset); + errorEvent = new EventWaitHandle(initialState: false, EventResetMode.ManualReset); + + unsafe + { + outputOverlappedNint = (nint)AllocateOverlapped(outputEvent); + errorOverlappedNint = (nint)AllocateOverlapped(errorEvent); + } + + // Error output gets index 0 so WaitAny services it first when both are signaled. + WaitHandle[] waitHandles = [errorEvent, outputEvent]; + + Decoder outputDecoder = outputEncoding.GetDecoder(); + Decoder errorDecoder = errorEncoding.GetDecoder(); + int outputCharStart = 0, outputCharEnd = 0; + int errorCharStart = 0, errorCharEnd = 0; + int unconsumedOutputBytesCount = 0, unconsumedErrorBytesCount = 0; + bool outputPreambleChecked = false, errorPreambleChecked = false; + + unsafe + { + outputDone = !QueueRead(outputHandle, outputPin.GetAddressOfArrayData(), + outputByteBuffer.Length, (NativeOverlapped*)outputOverlappedNint, outputEvent); + errorDone = !QueueRead(errorHandle, errorPin.GetAddressOfArrayData(), + errorByteBuffer.Length, (NativeOverlapped*)errorOverlappedNint, errorEvent); + } + + long deadline = timeoutMs >= 0 ? Environment.TickCount64 + timeoutMs : long.MaxValue; + List lines = new(); + + while (!outputDone || !errorDone) + { + int waitResult = TryGetRemainingTimeout(deadline, timeoutMs, out int remainingMilliseconds) + ? WaitHandle.WaitAny(waitHandles, remainingMilliseconds) + : WaitHandle.WaitTimeout; + + if (waitResult == WaitHandle.WaitTimeout) + { + throw new TimeoutException(); + } + + bool isError = waitResult == 0; + nint currentOverlappedNint = isError ? errorOverlappedNint : outputOverlappedNint; + SafeFileHandle currentHandle = isError ? errorHandle : outputHandle; + EventWaitHandle currentEvent = isError ? errorEvent! : outputEvent!; + + int bytesRead; + unsafe + { + bytesRead = GetOverlappedResultForPipe(currentHandle, (NativeOverlapped*)currentOverlappedNint); + } + + if (bytesRead > 0) + { + ReadOnlySpan bytes = new ReadOnlySpan( + isError ? errorByteBuffer : outputByteBuffer, + 0, + (isError ? unconsumedErrorBytesCount : unconsumedOutputBytesCount) + bytesRead); + + ref bool preambleChecked = ref (isError ? ref errorPreambleChecked : ref outputPreambleChecked); + ref Encoding currentEncoding = ref (isError ? ref errorEncoding : ref outputEncoding); + ref Decoder currentDecoder = ref (isError ? ref errorDecoder : ref outputDecoder); + ref int unconsumedBytesCount = ref (isError ? ref unconsumedErrorBytesCount : ref unconsumedOutputBytesCount); + + if (!preambleChecked) + { + if (bytes.Length >= MaxEncodingBytesLength) + { + bytes = bytes.Slice(SkipPreambleOrDetectEncoding(bytes, ref currentEncoding, ref currentDecoder)); + preambleChecked = true; + unconsumedBytesCount = 0; + } + else + { + unconsumedBytesCount += bytesRead; + } + } + + if (preambleChecked) + { + DecodeBytesAndParseLines(currentDecoder, bytes, + ref isError ? ref errorCharBuffer : ref outputCharBuffer, + ref isError ? ref errorCharStart : ref outputCharStart, + ref isError ? ref errorCharEnd : ref outputCharEnd, + isError, lines); + } + + unsafe + { + ResetOverlapped(currentEvent, (NativeOverlapped*)currentOverlappedNint); + + byte* pinPointer = isError + ? (errorPin.GetAddressOfArrayData() + unconsumedErrorBytesCount) + : (outputPin.GetAddressOfArrayData() + unconsumedOutputBytesCount); + int currentByteLength = isError + ? errorByteBuffer.Length - unconsumedErrorBytesCount + : outputByteBuffer.Length - unconsumedOutputBytesCount; + + if (!QueueRead(currentHandle, pinPointer, + currentByteLength, + (NativeOverlapped*)currentOverlappedNint, currentEvent)) + { + bytesRead = 0; // EOF during QueueRead + } + } + } + + if (bytesRead == 0) // EOF + { + if (isError) + { + errorDone = FlushDecoderAndEmitRemainingChars(errorPreambleChecked, errorEncoding, errorDecoder, errorByteBuffer.AsSpan(0, unconsumedErrorBytesCount), + ref errorCharBuffer, ref errorCharStart, ref errorCharEnd, isError, lines); + } + else + { + outputDone = FlushDecoderAndEmitRemainingChars(outputPreambleChecked, outputEncoding, outputDecoder, outputByteBuffer.AsSpan(0, unconsumedOutputBytesCount), + ref outputCharBuffer, ref outputCharStart, ref outputCharEnd, isError, lines); + } + + currentEvent.Reset(); + } + + // Yield parsed lines outside of any unsafe or ref-local scope. + foreach (ProcessOutputLine line in lines) + { + yield return line; + } + + lines.Clear(); + } + } + finally + { + unsafe + { + if (outputOverlappedNint != 0) + { + CancelPendingIOIfNeeded(outputHandle, outputDone, (NativeOverlapped*)outputOverlappedNint); + NativeMemory.Free((void*)outputOverlappedNint); + } + + if (errorOverlappedNint != 0) + { + CancelPendingIOIfNeeded(errorHandle, errorDone, (NativeOverlapped*)errorOverlappedNint); + NativeMemory.Free((void*)errorOverlappedNint); + } + } + + outputEvent?.Dispose(); + errorEvent?.Dispose(); + outputPin.Dispose(); + errorPin.Dispose(); + + ArrayPool.Shared.Return(outputByteBuffer); + ArrayPool.Shared.Return(errorByteBuffer); + ArrayPool.Shared.Return(outputCharBuffer); + ArrayPool.Shared.Return(errorCharBuffer); + } + } + /// /// Reads from both standard output and standard error pipes using Windows overlapped IO /// with wait handles for single-threaded synchronous multiplexing. diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.cs index 5b11679d82c451..6223af6cae0cb6 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Multiplexing.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Buffers.Binary; using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; @@ -17,6 +18,7 @@ public partial class Process { /// Initial buffer size for reading process output. private const int InitialReadAllBufferSize = 4096; + private const int MaxEncodingBytesLength = 4; /// /// Reads all standard output and standard error of the process as text. @@ -113,6 +115,250 @@ public partial class Process } } + /// + /// Reads all standard output and standard error of the process as lines of text, + /// interleaving them as they become available. + /// + /// + /// The maximum amount of time to wait for the streams to be fully read. + /// When , waits indefinitely. + /// + /// + /// An enumerable of instances representing the lines + /// read from standard output and standard error. + /// + /// + /// Lines from standard output and standard error are yielded as they become available. + /// When data is available in both standard output and standard error, standard error + /// is processed first. + /// + /// + /// Standard output or standard error has not been redirected. + /// -or- + /// A redirected stream has already been used for synchronous or asynchronous reading. + /// + /// + /// The operation did not complete within the specified . + /// + /// + /// The process has been disposed. + /// + public IEnumerable ReadAllLines(TimeSpan? timeout = default) + { + ValidateReadAllState(); + + int timeoutMs = timeout.HasValue + ? ProcessUtils.ToTimeoutMilliseconds(timeout.Value) + : Timeout.Infinite; + + Encoding outputEncoding = _startInfo?.StandardOutputEncoding ?? GetStandardOutputEncoding(); + Encoding errorEncoding = _startInfo?.StandardErrorEncoding ?? GetStandardOutputEncoding(); + + return ReadPipesToLines(timeoutMs, outputEncoding, errorEncoding); + } + + /// + /// Decodes bytes from the byte buffer using the and appends the + /// resulting characters to the char buffer, growing it if necessary. + /// To flush the decoder at EOF, pass an empty byte array with set to + /// . + /// + private static void DecodeAndAppendChars( + Decoder decoder, + ReadOnlySpan byteBuffer, + bool flush, + ref char[] charBuffer, + ref int charStartIndex, + ref int charEndIndex) + { + int charCount = decoder.GetCharCount(byteBuffer, flush); + + // If there isn't enough room at the end, compact the consumed space at the start first + // so that if growth is still needed, RentLargerBuffer copies only the unconsumed data. + if (charEndIndex + charCount > charBuffer.Length && charStartIndex > 0) + { + int remaining = charEndIndex - charStartIndex; + Array.Copy(charBuffer, charStartIndex, charBuffer, 0, remaining); + charStartIndex = 0; + charEndIndex = remaining; + } + + while (charEndIndex + charCount > charBuffer.Length) + { + RentLargerBuffer(ref charBuffer, charEndIndex); + } + + int decoded = decoder.GetChars(byteBuffer, charBuffer.AsSpan(charEndIndex), flush); + charEndIndex += decoded; + } + + /// + /// Checks for the encoding's preamble or a BOM from a different encoding at the start of + /// the byte buffer, mimicking behavior. + /// If the encoding's own preamble is found, returns the number of bytes to skip. + /// If a different encoding's BOM is detected, updates and + /// and returns the BOM length to skip. + /// + private static int SkipPreambleOrDetectEncoding(ReadOnlySpan byteBuffer, ref Encoding encoding, ref Decoder decoder) + { + // Check for the encoding's own preamble first (like StreamReader.IsPreamble). + ReadOnlySpan preamble = encoding.Preamble; + if (preamble.Length > 0 && byteBuffer.Length >= preamble.Length + && byteBuffer.Slice(0, preamble.Length).SequenceEqual(preamble)) + { + return preamble.Length; + } + + // No preamble match — check for BOM from other encodings (like StreamReader.DetectEncoding). + if (byteBuffer.Length >= 2) + { + ushort firstTwoBytes = BinaryPrimitives.ReadUInt16LittleEndian(byteBuffer); + + if (firstTwoBytes == 0xFFFE) + { + // Big Endian Unicode + encoding = Encoding.BigEndianUnicode; + decoder = encoding.GetDecoder(); + return 2; + } + + if (firstTwoBytes == 0xFEFF) + { + if (byteBuffer.Length >= 4 && byteBuffer[2] == 0 && byteBuffer[3] == 0) + { + encoding = Encoding.UTF32; + decoder = encoding.GetDecoder(); + return 4; + } + + encoding = Encoding.Unicode; + decoder = encoding.GetDecoder(); + return 2; + } + + if (byteBuffer.Length >= 3 && firstTwoBytes == 0xBBEF && byteBuffer[2] == 0xBF) + { + encoding = Encoding.UTF8; + decoder = encoding.GetDecoder(); + return 3; + } + + if (byteBuffer.Length >= 4 && firstTwoBytes == 0 && byteBuffer[2] == 0xFE && byteBuffer[3] == 0xFF) + { + encoding = new UTF32Encoding(bigEndian: true, byteOrderMark: true); + decoder = encoding.GetDecoder(); + return 4; + } + } + + return 0; + } + + /// + /// Scans the char buffer from to for complete + /// lines (delimited by \r, \n, or \r\n), adds each as a + /// to , and advances + /// past the consumed data. + /// This matches behavior used by the async path. + /// + private static void ParseLinesFromCharBuffer( + char[] buffer, + ref int startIndex, + int endIndex, + bool standardError, + List lines) + { + while (startIndex < endIndex) + { + int remaining = endIndex - startIndex; + int lineEnd = buffer.AsSpan(startIndex, remaining).IndexOfAny('\r', '\n'); + if (lineEnd == -1) + { + break; + } + + char terminator = buffer[startIndex + lineEnd]; + + // If we found '\r', we need to check for a following '\n' to treat \r\n as one terminator. + // If '\n' isn't available yet (end of current data), stop and wait for more data. + if (terminator == '\r') + { + if (startIndex + lineEnd + 1 >= endIndex) + { + // The '\r' is at the very end of available data — we can't tell yet + // whether it's a standalone '\r' or part of '\r\n'. Wait for more data. + break; + } + + lines.Add(new ProcessOutputLine( + new string(buffer, startIndex, lineEnd), + standardError)); + + // Skip \r and also \n if it immediately follows. + startIndex += lineEnd + 1; + if (startIndex < endIndex && buffer[startIndex] == '\n') + { + startIndex++; + } + } + else + { + // terminator == '\n' + lines.Add(new ProcessOutputLine( + new string(buffer, startIndex, lineEnd), + standardError)); + + startIndex += lineEnd + 1; + } + } + } + + /// + /// Emits any remaining characters in the buffer as a final line when an EOF is reached. + /// A trailing \r is stripped to match behavior. + /// + private static void EmitRemainingCharsAsLine( + char[] buffer, + ref int startIndex, + ref int endIndex, + bool standardError, + List lines) + { + if (startIndex < endIndex) + { + int length = endIndex - startIndex; + if (length > 0 && buffer[startIndex + length - 1] == '\r') + { + length--; + } + + lines.Add(new ProcessOutputLine( + new string(buffer, startIndex, length), + standardError)); + + startIndex = 0; + endIndex = 0; + } + } + + private static void DecodeBytesAndParseLines(Decoder decoder, ReadOnlySpan byteBuffer, ref char[] charBuffer, ref int charStart, ref int charEnd, bool standardError, List lines) + { + DecodeAndAppendChars(decoder, byteBuffer, flush: false, ref charBuffer, ref charStart, ref charEnd); + ParseLinesFromCharBuffer(charBuffer, ref charStart, charEnd, standardError, lines); + } + + private static bool FlushDecoderAndEmitRemainingChars(bool preambleChecked, Encoding encoding, Decoder decoder, ReadOnlySpan unconsumedBytes, ref char[] charBuffer, ref int charStart, ref int charEnd, bool standardError, List lines) + { + if (!preambleChecked && unconsumedBytes.Length > 0) + { + unconsumedBytes = unconsumedBytes.Slice(SkipPreambleOrDetectEncoding(unconsumedBytes, ref encoding, ref decoder)); + } + + DecodeAndAppendChars(decoder, unconsumedBytes, flush: true, ref charBuffer, ref charStart, ref charEnd); + EmitRemainingCharsAsLine(charBuffer, ref charStart, ref charEnd, standardError, lines); + return true; + } + /// /// Asynchronously reads all standard output and standard error of the process as text. /// @@ -419,17 +665,17 @@ private void ReadPipesToBuffers( } /// - /// Rents a larger buffer from the array pool and copies the existing data to it. + /// Rents a larger buffer from the array pool, copies existing data, and returns the old buffer to the pool. /// - private static void RentLargerBuffer(ref byte[] buffer, int bytesRead) + private static void RentLargerBuffer(ref T[] buffer, int dataLength) { int newSize = (int)Math.Min((long)buffer.Length * 2, Array.MaxLength); newSize = Math.Max(buffer.Length + 1, newSize); - byte[] newBuffer = ArrayPool.Shared.Rent(newSize); - Buffer.BlockCopy(buffer, 0, newBuffer, 0, bytesRead); - byte[] oldBuffer = buffer; + T[] newBuffer = ArrayPool.Shared.Rent(newSize); + Array.Copy(buffer, newBuffer, dataLength); + T[] oldBuffer = buffer; buffer = newBuffer; - ArrayPool.Shared.Return(oldBuffer); + ArrayPool.Shared.Return(oldBuffer); } private static bool TryGetRemainingTimeout(long deadline, int originalTimeout, out int remainingTimeoutMs) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessStreamingTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessStreamingTests.cs index 4b2967e1085611..15a6d65249e197 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessStreamingTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessStreamingTests.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.IO; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.DotNet.RemoteExecutor; @@ -13,65 +15,109 @@ public class ProcessStreamingTests : ProcessTestBase { private const string DontPrintAnything = "DO_NOT_PRINT"; - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public async Task ReadAllLinesAsync_ThrowsAfterDispose() + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true)] + [InlineData(false)] + public async Task ReadAllLines_ThrowsAfterDispose(bool useAsync) { - Process process = CreateProcess(RemotelyInvokable.Dummy); + using Process process = CreateProcess(static () => RemoteExecutor.SuccessExitCode); process.Start(); Assert.True(process.WaitForExit(WaitInMS)); process.Dispose(); - await Assert.ThrowsAsync(async () => + if (useAsync) { - await foreach (ProcessOutputLine line in process.ReadAllLinesAsync()) + await Assert.ThrowsAsync(async () => { - } - }); + await foreach (ProcessOutputLine line in process.ReadAllLinesAsync()) + { + } + }); + } + else + { + Assert.Throws(() => + { + foreach (ProcessOutputLine line in process.ReadAllLines()) + { + } + }); + } } - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public async Task ReadAllLinesAsync_ThrowsWhenNoStreamsRedirected() + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true)] + [InlineData(false)] + public async Task ReadAllLines_ThrowsWhenNoStreamsRedirected(bool useAsync) { - Process process = CreateProcess(RemotelyInvokable.Dummy); + using Process process = CreateProcess(static () => RemoteExecutor.SuccessExitCode); process.Start(); - await Assert.ThrowsAsync(async () => + if (useAsync) { - await foreach (ProcessOutputLine line in process.ReadAllLinesAsync()) + await Assert.ThrowsAsync(async () => { - } - }); + await foreach (ProcessOutputLine line in process.ReadAllLinesAsync()) + { + } + }); + } + else + { + Assert.Throws(() => + { + foreach (ProcessOutputLine line in process.ReadAllLines()) + { + } + }); + } Assert.True(process.WaitForExit(WaitInMS)); } [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - [InlineData(true)] - [InlineData(false)] - public async Task ReadAllLinesAsync_ThrowsWhenOnlyOutputOrErrorIsRedirected(bool standardOutput) + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public async Task ReadAllLines_ThrowsWhenOnlyOutputOrErrorIsRedirected(bool standardOutput, bool useAsync) { - Process process = CreateProcess(RemotelyInvokable.Dummy); + using Process process = CreateProcess(static () => RemoteExecutor.SuccessExitCode); process.StartInfo.RedirectStandardOutput = standardOutput; process.StartInfo.RedirectStandardError = !standardOutput; process.Start(); - await Assert.ThrowsAsync(async () => + if (useAsync) { - await foreach (ProcessOutputLine line in process.ReadAllLinesAsync()) + await Assert.ThrowsAsync(async () => { - } - }); + await foreach (ProcessOutputLine line in process.ReadAllLinesAsync()) + { + } + }); + } + else + { + Assert.Throws(() => + { + foreach (ProcessOutputLine line in process.ReadAllLines()) + { + } + }); + } Assert.True(process.WaitForExit(WaitInMS)); } [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - [InlineData(true)] - [InlineData(false)] - public async Task ReadAllLinesAsync_ThrowsWhenOutputOrErrorIsInSyncMode(bool standardOutput) + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public async Task ReadAllLines_ThrowsWhenOutputOrErrorIsInSyncMode(bool standardOutput, bool useAsync) { - Process process = CreateProcess(RemotelyInvokable.Dummy); + using Process process = CreateProcess(static () => RemoteExecutor.SuccessExitCode); process.StartInfo.RedirectStandardOutput = true; process.StartInfo.RedirectStandardError = true; process.Start(); @@ -79,22 +125,36 @@ public async Task ReadAllLinesAsync_ThrowsWhenOutputOrErrorIsInSyncMode(bool sta // Access the StreamReader property to set the stream to sync mode _ = standardOutput ? process.StandardOutput : process.StandardError; - await Assert.ThrowsAsync(async () => + if (useAsync) { - await foreach (ProcessOutputLine line in process.ReadAllLinesAsync()) + await Assert.ThrowsAsync(async () => { - } - }); + await foreach (ProcessOutputLine line in process.ReadAllLinesAsync()) + { + } + }); + } + else + { + Assert.Throws(() => + { + foreach (ProcessOutputLine line in process.ReadAllLines()) + { + } + }); + } Assert.True(process.WaitForExit(WaitInMS)); } [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - [InlineData(true)] - [InlineData(false)] - public async Task ReadAllLinesAsync_ThrowsWhenOutputOrErrorIsInAsyncMode(bool standardOutput) + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public async Task ReadAllLines_ThrowsWhenOutputOrErrorIsInAsyncMode(bool standardOutput, bool useAsync) { - Process process = CreateProcess(RemotelyInvokable.StreamBody); + using Process process = CreateProcess(static () => RemoteExecutor.SuccessExitCode); process.StartInfo.RedirectStandardOutput = true; process.StartInfo.RedirectStandardError = true; process.Start(); @@ -108,12 +168,24 @@ public async Task ReadAllLinesAsync_ThrowsWhenOutputOrErrorIsInAsyncMode(bool st process.BeginErrorReadLine(); } - await Assert.ThrowsAsync(async () => + if (useAsync) { - await foreach (ProcessOutputLine line in process.ReadAllLinesAsync()) + await Assert.ThrowsAsync(async () => { - } - }); + await foreach (ProcessOutputLine line in process.ReadAllLinesAsync()) + { + } + }); + } + else + { + Assert.Throws(() => + { + foreach (ProcessOutputLine line in process.ReadAllLines()) + { + } + }); + } if (standardOutput) { @@ -128,30 +200,21 @@ await Assert.ThrowsAsync(async () => } [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - [InlineData("hello", "world")] - [InlineData("just output", "")] - [InlineData("", "just error")] - [InlineData("", "")] - public async Task ReadAllLinesAsync_ReadsBothOutputAndError(string standardOutput, string standardError) + [InlineData("hello", "world", true)] + [InlineData("hello", "world", false)] + [InlineData("just output", "", true)] + [InlineData("just output", "", false)] + [InlineData("", "just error", true)] + [InlineData("", "just error", false)] + [InlineData("", "", true)] + [InlineData("", "", false)] + public async Task ReadAllLines_ReadsBothOutputAndError(string standardOutput, string standardError, bool useAsync) { using Process process = StartLinePrintingProcess( string.IsNullOrEmpty(standardOutput) ? DontPrintAnything : standardOutput, string.IsNullOrEmpty(standardError) ? DontPrintAnything : standardError); - List capturedOutput = new(); - List capturedError = new(); - - await foreach (ProcessOutputLine line in process.ReadAllLinesAsync()) - { - if (line.StandardError) - { - capturedError.Add(line.Content); - } - else - { - capturedOutput.Add(line.Content); - } - } + (List capturedOutput, List capturedError) = await EnumerateLines(process, useAsync); if (string.IsNullOrEmpty(standardOutput)) { @@ -174,8 +237,10 @@ public async Task ReadAllLinesAsync_ReadsBothOutputAndError(string standardOutpu Assert.True(process.WaitForExit(WaitInMS)); } - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public async Task ReadAllLinesAsync_ReadsInterleavedOutput() + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true)] + [InlineData(false)] + public async Task ReadAllLines_ReadsInterleavedOutput(bool useAsync) { const int iterations = 100; using Process process = CreateProcess(() => @@ -195,20 +260,7 @@ public async Task ReadAllLinesAsync_ReadsInterleavedOutput() process.StartInfo.RedirectStandardError = true; process.Start(); - List capturedOutput = new(); - List capturedError = new(); - - await foreach (ProcessOutputLine line in process.ReadAllLinesAsync()) - { - if (line.StandardError) - { - capturedError.Add(line.Content); - } - else - { - capturedOutput.Add(line.Content); - } - } + (List capturedOutput, List capturedError) = await EnumerateLines(process, useAsync); List expectedOutput = new(); List expectedError = new(); @@ -224,8 +276,10 @@ public async Task ReadAllLinesAsync_ReadsInterleavedOutput() Assert.True(process.WaitForExit(WaitInMS)); } - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public async Task ReadAllLinesAsync_ReadsLargeOutput() + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true)] + [InlineData(false)] + public async Task ReadAllLines_ReadsLargeOutput(bool useAsync) { const int lineCount = 1000; using Process process = CreateProcess(() => @@ -242,34 +296,62 @@ public async Task ReadAllLinesAsync_ReadsLargeOutput() process.StartInfo.RedirectStandardError = true; process.Start(); - List capturedOutput = new(); - List capturedError = new(); + (List capturedOutput, List capturedError) = await EnumerateLines(process, useAsync); - await foreach (ProcessOutputLine line in process.ReadAllLinesAsync()) + Assert.Equal(lineCount, capturedOutput.Count); + for (int i = 0; i < lineCount; i++) { - if (line.StandardError) - { - capturedError.Add(line.Content); - } - else + Assert.Equal($"line{i}", capturedOutput[i]); + } + + Assert.Empty(capturedError); + Assert.True(process.WaitForExit(WaitInMS)); + } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true)] + [InlineData(false)] + public async Task ReadAllLines_ReadsVeryLongLines(bool useAsync) + { + const int lineLength = 8192; + const int lineCount = 3; + using Process process = CreateProcess(() => + { + for (int i = 0; i < lineCount; i++) { - capturedOutput.Add(line.Content); + Console.Out.WriteLine(new string((char)('A' + i), lineLength)); + Console.Out.Flush(); + Console.Error.WriteLine(new string((char)('a' + i), lineLength)); + Console.Error.Flush(); } - } + + return RemoteExecutor.SuccessExitCode; + }); + + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.Start(); + + (List capturedOutput, List capturedError) = await EnumerateLines(process, useAsync); + + Assert.Equal(lineCount, capturedOutput.Count); + Assert.Equal(lineCount, capturedError.Count); for (int i = 0; i < lineCount; i++) { - Assert.Equal($"line{i}", capturedOutput[i]); + Assert.Equal(new string((char)('A' + i), lineLength), capturedOutput[i]); + Assert.Equal(new string((char)('a' + i), lineLength), capturedError[i]); } - Assert.Empty(capturedError); Assert.True(process.WaitForExit(WaitInMS)); } - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public async Task ReadAllLinesAsync_ThrowsOperationCanceledOnCancellation() + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true)] + [InlineData(false)] + public async Task ReadAllLines_ThrowsOnCancellationOrTimeout(bool useAsync) { - Process process = CreateProcess(RemotelyInvokable.ReadLine); + using Process process = CreateProcess(RemotelyInvokable.ReadLine); process.StartInfo.RedirectStandardOutput = true; process.StartInfo.RedirectStandardError = true; process.StartInfo.RedirectStandardInput = true; @@ -277,14 +359,26 @@ public async Task ReadAllLinesAsync_ThrowsOperationCanceledOnCancellation() try { - using CancellationTokenSource cts = new(TimeSpan.FromMilliseconds(100)); + if (useAsync) + { + using CancellationTokenSource cts = new(TimeSpan.FromMilliseconds(100)); - await Assert.ThrowsAnyAsync(async () => + await Assert.ThrowsAnyAsync(async () => + { + await foreach (ProcessOutputLine line in process.ReadAllLinesAsync(cts.Token)) + { + } + }); + } + else { - await foreach (ProcessOutputLine line in process.ReadAllLinesAsync(cts.Token)) + Assert.Throws(() => { - } - }); + foreach (ProcessOutputLine line in process.ReadAllLines(TimeSpan.FromMilliseconds(100))) + { + } + }); + } } finally { @@ -294,26 +388,25 @@ await Assert.ThrowsAnyAsync(async () => Assert.True(process.WaitForExit(WaitInMS)); } - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public async Task ReadAllLinesAsync_ProcessOutputLineProperties() + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true)] + [InlineData(false)] + public async Task ReadAllLines_ProcessOutputLineProperties(bool useAsync) { using Process process = StartLinePrintingProcess("stdout_line", "stderr_line"); - List allLines = new(); + (List capturedOutput, List capturedError) = await EnumerateLines(process, useAsync); - await foreach (ProcessOutputLine line in process.ReadAllLinesAsync()) - { - allLines.Add(line); - } - - Assert.Single(allLines, line => line.Content == "stdout_line" && !line.StandardError); - Assert.Single(allLines, line => line.Content == "stderr_line" && line.StandardError); + Assert.Single(capturedOutput, line => line == "stdout_line"); + Assert.Single(capturedError, line => line == "stderr_line"); Assert.True(process.WaitForExit(WaitInMS)); } - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public async Task ReadAllLinesAsync_StopsCleanlyWhenConsumerBreaksEarly() + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true)] + [InlineData(false)] + public async Task ReadAllLines_StopsCleanlyWhenConsumerBreaksEarly(bool useAsync) { using Process process = CreateProcess(() => { @@ -333,10 +426,21 @@ public async Task ReadAllLinesAsync_StopsCleanlyWhenConsumerBreaksEarly() ProcessOutputLine? firstLine = null; - await foreach (ProcessOutputLine line in process.ReadAllLinesAsync()) + if (useAsync) { - firstLine = line; - break; // stop after first line + await foreach (ProcessOutputLine line in process.ReadAllLinesAsync()) + { + firstLine = line; + break; + } + } + else + { + foreach (ProcessOutputLine line in process.ReadAllLines()) + { + firstLine = line; + break; + } } Assert.NotNull(firstLine); @@ -345,6 +449,187 @@ public async Task ReadAllLinesAsync_StopsCleanlyWhenConsumerBreaksEarly() Assert.True(process.WaitForExit(WaitInMS)); } + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData("utf-8", true)] + [InlineData("utf-8", false)] + [InlineData("utf-16", true)] + [InlineData("utf-16", false)] + [InlineData("utf-32", true)] + [InlineData("utf-32", false)] + public async Task ReadAllLines_WorksWithNonDefaultEncodings(string encodingName, bool useAsync) + { + Encoding encoding = Encoding.GetEncoding(encodingName); + + using Process process = CreateProcess(static (string encodingArg) => + { + Encoding enc = Encoding.GetEncoding(encodingArg); + using (StreamWriter outputWriter = new(Console.OpenStandardOutput(), enc)) + { + outputWriter.WriteLine("stdout_line"); + } + + using (StreamWriter errorWriter = new(Console.OpenStandardError(), enc)) + { + errorWriter.WriteLine("stderr_line"); + } + + return RemoteExecutor.SuccessExitCode; + }, encodingName); + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.StandardOutputEncoding = encoding; + process.StartInfo.StandardErrorEncoding = encoding; + process.Start(); + + (List capturedOutput, List capturedError) = await EnumerateLines(process, useAsync); + + Assert.Equal(new[] { "stdout_line" }, capturedOutput); + Assert.Equal(new[] { "stderr_line" }, capturedError); + + Assert.True(process.WaitForExit(WaitInMS)); + } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData("utf-8", true)] + [InlineData("utf-8", false)] + [InlineData("utf-16", true)] + [InlineData("utf-16", false)] + [InlineData("utf-32", true)] + [InlineData("utf-32", false)] + public async Task ReadAllLines_WorksWithMultiByteCharacters(string encodingName, bool useAsync) + { + Encoding encoding = Encoding.GetEncoding(encodingName); + + using Process process = CreateProcess(static (string encodingArg) => + { + Encoding enc = Encoding.GetEncoding(encodingArg); + // Write raw encoded bytes split at the midpoint of the byte array so the split + // lands inside a multi-byte character, exercising decoder state across reads. + // CJK chars (U+4E16 U+754C = "世界"): 3 bytes each in UTF-8, 2 in UTF-16, 4 in UTF-32. + byte[] outBytes = enc.GetBytes("hello_\u4e16\u754c_stdout\n"); + int outSplit = outBytes.Length / 2; + Stream stdout = Console.OpenStandardOutput(); + stdout.Write(outBytes, 0, outSplit); + stdout.Flush(); + stdout.Write(outBytes, outSplit, outBytes.Length - outSplit); + stdout.Flush(); + + byte[] errBytes = enc.GetBytes("hello_\u4e16\u754c_stderr\n"); + int errSplit = errBytes.Length / 2; + Stream stderr = Console.OpenStandardError(); + stderr.Write(errBytes, 0, errSplit); + stderr.Flush(); + stderr.Write(errBytes, errSplit, errBytes.Length - errSplit); + stderr.Flush(); + + return RemoteExecutor.SuccessExitCode; + }, encodingName); + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.StandardOutputEncoding = encoding; + process.StartInfo.StandardErrorEncoding = encoding; + process.Start(); + + (List capturedOutput, List capturedError) = await EnumerateLines(process, useAsync); + + Assert.Equal(new[] { "hello_\u4e16\u754c_stdout" }, capturedOutput); + Assert.Equal(new[] { "hello_\u4e16\u754c_stderr" }, capturedError); + + Assert.True(process.WaitForExit(WaitInMS)); + } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true)] + [InlineData(false)] + public async Task ReadAllLines_HandlesMixedLineEndings(bool useAsync) + { + using Process process = CreateProcess(static () => + { + // Write stdout with all three line-terminator styles in one stream: + // \r\n (Windows), \n (Unix), bare \r (classic Mac), and a final chunk with no terminator. + Stream stdout = Console.OpenStandardOutput(); + byte[] data = Encoding.UTF8.GetBytes("lineA\r\nlineB\nlineC\rlineD"); + stdout.Write(data); + stdout.Flush(); + return RemoteExecutor.SuccessExitCode; + }); + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.Start(); + + (List capturedOutput, List capturedError) = await EnumerateLines(process, useAsync); + + Assert.Equal(new[] { "lineA", "lineB", "lineC", "lineD" }, capturedOutput); + Assert.Empty(capturedError); + Assert.True(process.WaitForExit(WaitInMS)); + } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true)] + [InlineData(false)] + public async Task ReadAllLines_HandlesPartialBomAcrossReads(bool useAsync) + { + // Write a UTF-32 LE BOM (FF FE 00 00) as two separate flushed writes so the + // first read can deliver only the first two BOM bytes. Without BOM accumulation, + // FF FE would be misclassified as a UTF-16 LE BOM and the content would be + // decoded with the wrong encoding. + using Process process = CreateProcess(static () => + { + Stream stdout = Console.OpenStandardOutput(); + stdout.Write([0xFF, 0xFE]); // First half of UTF-32 LE BOM + stdout.Flush(); + stdout.Write([0x00, 0x00]); // Second half of BOM + stdout.Write(Encoding.UTF32.GetBytes("hello\n")); // Content (no BOM from GetBytes) + stdout.Flush(); + return RemoteExecutor.SuccessExitCode; + }); + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.StandardOutputEncoding = Encoding.UTF32; + process.Start(); + + (List capturedOutput, List capturedError) = await EnumerateLines(process, useAsync); + + Assert.Equal(new[] { "hello" }, capturedOutput); + Assert.Empty(capturedError); + Assert.True(process.WaitForExit(WaitInMS)); + } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(true)] + [InlineData(false)] + public async Task ReadAllLines_LessThanFourBytes(bool useAsync) + { + using Process process = CreateProcess(static () => + { + Stream stdout = Console.OpenStandardOutput(); + stdout.Write([(byte)'h']); + stdout.Flush(); + stdout.Write([(byte)'i']); + stdout.Flush(); + + Stream error = Console.OpenStandardError(); + error.Write([(byte)'b']); + error.Flush(); + error.Write([(byte)'y']); + error.Flush(); + error.Write([(byte)'e']); + error.Flush(); + + return RemoteExecutor.SuccessExitCode; + }); + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.StandardOutputEncoding = Encoding.UTF8; + process.Start(); + + (List capturedOutput, List capturedError) = await EnumerateLines(process, useAsync); + + Assert.Equal(new[] { "hi" }, capturedOutput); + Assert.Equal(new[] { "bye" }, capturedError); + Assert.True(process.WaitForExit(WaitInMS)); + } + private Process StartLinePrintingProcess(string stdOutText, string stdErrText) { Process process = CreateProcess((stdOut, stdErr) => @@ -368,5 +653,46 @@ private Process StartLinePrintingProcess(string stdOutText, string stdErrText) return process; } + + /// + /// Helper that wraps both the sync and async line-reading APIs and returns + /// the captured output and error lines. + /// + private static async Task<(List capturedOutput, List capturedError)> EnumerateLines(Process process, bool useAsync) + { + List capturedOutput = new(); + List capturedError = new(); + + if (useAsync) + { + await foreach (ProcessOutputLine line in process.ReadAllLinesAsync()) + { + if (line.StandardError) + { + capturedError.Add(line.Content); + } + else + { + capturedOutput.Add(line.Content); + } + } + } + else + { + foreach (ProcessOutputLine line in process.ReadAllLines()) + { + if (line.StandardError) + { + capturedError.Add(line.Content); + } + else + { + capturedOutput.Add(line.Content); + } + } + } + + return (capturedOutput, capturedError); + } } } From c0d836dbe2315b310e7e099afcb50475dce1a521 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 30 Apr 2026 02:37:38 -0700 Subject: [PATCH 031/115] Add managed ilasm round-trip testing support (#127096) --- src/tests/Common/CLRTest.Jit.targets | 104 +++++++++++------- src/tests/Common/testenvironment.proj | 8 +- src/tests/Interop/IJW/Directory.Build.props | 2 + src/tests/JIT/jit64/opt/cse/hugeexpr1.csproj | 2 + .../MethodImpl/InternalMethodImplTest.csproj | 5 + .../dev10_403582/dev10_403582.ilproj | 4 +- src/tests/run.cmd | 6 + src/tests/run.py | 15 +++ 8 files changed, 102 insertions(+), 44 deletions(-) diff --git a/src/tests/Common/CLRTest.Jit.targets b/src/tests/Common/CLRTest.Jit.targets index 729d588e5337eb..745d3e96c9d3a4 100644 --- a/src/tests/Common/CLRTest.Jit.targets +++ b/src/tests/Common/CLRTest.Jit.targets @@ -337,6 +337,11 @@ def is_managed_exe_assembly(file): print("") print("ILASM RoundTrips") +if os.environ.get("IlasmRoundTripUseManagedIlasm") == "1": + ilasm_path = os.path.join(os.environ["CORE_ROOT"], "managed-ilasm", "ilasm") +else: + ilasm_path = os.path.join(os.environ["CORE_ROOT"], "ilasm") + if not os.path.exists("IL-RT"): os.mkdir("IL-RT") @@ -367,7 +372,11 @@ for inputAssemblyName in glob.glob("*.dll"): proc.kill() sys.exit(1) - ilasm_args = f'{os.environ["CORE_ROOT"]}/ilasm -output={inputAssemblyName} {ilasmSwitches} {disassemblyName}' + if proc.returncode != 0: + print(f"ILDASM failed with exit code {proc.returncode}") + sys.exit(1) + + ilasm_args = f'{ilasm_path} -output={inputAssemblyName} {ilasmSwitches} {disassemblyName}' print(ilasm_args) proc = subprocess.Popen(ilasm_args, shell=True) @@ -377,63 +386,76 @@ for inputAssemblyName in glob.glob("*.dll"): proc.kill() sys.exit(1) - test_det = proc.returncode == 0 + if proc.returncode != 0: + print(f"ILASM failed with exit code {proc.returncode}") + sys.exit(1) # Test determinism - if test_det: - hash = hash_file(inputAssemblyName) + hash = hash_file(inputAssemblyName) - ilasm_args = f'{os.environ["CORE_ROOT"]}/ilasm -output={inputAssemblyName} {ilasmSwitches} {disassemblyName}' - print(ilasm_args) - proc = subprocess.Popen(ilasm_args, shell=True) + ilasm_args = f'{ilasm_path} -output={inputAssemblyName} {ilasmSwitches} {disassemblyName}' + print(ilasm_args) + proc = subprocess.Popen(ilasm_args, shell=True) - try: - proc.communicate() - except: - proc.kill() - sys.exit(1) + try: + proc.communicate() + except: + proc.kill() + sys.exit(1) - if hash != hash_file(inputAssemblyName): - print("ILASM determinism failed") + if proc.returncode != 0: + print(f"ILASM failed with exit code {proc.returncode}") sys.exit(1) - else: - print("ILASM determinism succeeded") - # Test PDB determinism + if hash != hash_file(inputAssemblyName): + print("ILASM determinism failed") + sys.exit(1) + else: + print("ILASM determinism succeeded") - if not is_managed_debug_assembly(inputAssemblyName): - ilasmSwitches = ilasmSwitches + " -DEBUG" + # Test PDB determinism - pdbName = inputAssemblyName.replace('.dll', '.pdb') + if not is_managed_debug_assembly(inputAssemblyName): + ilasmSwitches = ilasmSwitches + " -DEBUG" + + pdbName = inputAssemblyName.replace('.dll', '.pdb') + + ilasm_args = f'{ilasm_path} -output={pdbName} {ilasmSwitches} {disassemblyName}' + print(ilasm_args) + proc = subprocess.Popen(ilasm_args, shell=True) - ilasm_args = f'{os.environ["CORE_ROOT"]}/ilasm -output={pdbName} {ilasmSwitches} {disassemblyName}' - print(ilasm_args) - proc = subprocess.Popen(ilasm_args, shell=True) + try: + proc.communicate() + except: + proc.kill() + sys.exit(1) - try: - proc.communicate() - except: - proc.kill() - sys.exit(1) + if proc.returncode != 0: + print(f"ILASM failed with exit code {proc.returncode}") + sys.exit(1) - hash = hash_file(pdbName) + hash = hash_file(pdbName) - ilasm_args = f'{os.environ["CORE_ROOT"]}/ilasm -output={pdbName} {ilasmSwitches} {disassemblyName}' - print(ilasm_args) - proc = subprocess.Popen(ilasm_args, shell=True) + ilasm_args = f'{ilasm_path} -output={pdbName} {ilasmSwitches} {disassemblyName}' + print(ilasm_args) + proc = subprocess.Popen(ilasm_args, shell=True) - try: - proc.communicate() - except: - proc.kill() - sys.exit(1) + try: + proc.communicate() + except: + proc.kill() + sys.exit(1) - if hash != hash_file(pdbName): - print("ILASM PDB determinism failed") + if proc.returncode != 0: + print(f"ILASM failed with exit code {proc.returncode}") sys.exit(1) - else: - print("ILASM PDB determinism succeeded") + + if hash != hash_file(pdbName): + print("ILASM PDB determinism failed") + sys.exit(1) + else: + print("ILASM PDB determinism succeeded") print("") diff --git a/src/tests/Common/testenvironment.proj b/src/tests/Common/testenvironment.proj index eaab9898e1d34a..5b1e224c4fa6a7 100644 --- a/src/tests/Common/testenvironment.proj +++ b/src/tests/Common/testenvironment.proj @@ -7,8 +7,10 @@ - @@ -66,6 +68,7 @@ DOTNET_JitForceControlFlowGuard; DOTNET_JitCFGUseDispatcher; RunningIlasmRoundTrip; + IlasmRoundTripUseManagedIlasm; DOTNET_JitSynthesizeCounts; DOTNET_JitCheckSynthesizedCounts; DOTNET_JitRLCSEGreedy; @@ -191,6 +194,7 @@ + diff --git a/src/tests/Interop/IJW/Directory.Build.props b/src/tests/Interop/IJW/Directory.Build.props index 6faba48234f19a..1b150028e2807b 100644 --- a/src/tests/Interop/IJW/Directory.Build.props +++ b/src/tests/Interop/IJW/Directory.Build.props @@ -11,6 +11,8 @@ true true + + true diff --git a/src/tests/JIT/jit64/opt/cse/hugeexpr1.csproj b/src/tests/JIT/jit64/opt/cse/hugeexpr1.csproj index 5bca68ddc4bcfd..3def7a12428f17 100644 --- a/src/tests/JIT/jit64/opt/cse/hugeexpr1.csproj +++ b/src/tests/JIT/jit64/opt/cse/hugeexpr1.csproj @@ -5,6 +5,8 @@ true true + + true Full diff --git a/src/tests/Loader/classloader/MethodImpl/InternalMethodImplTest.csproj b/src/tests/Loader/classloader/MethodImpl/InternalMethodImplTest.csproj index c17f6a7f838561..e09dc84afd9fa2 100644 --- a/src/tests/Loader/classloader/MethodImpl/InternalMethodImplTest.csproj +++ b/src/tests/Loader/classloader/MethodImpl/InternalMethodImplTest.csproj @@ -1,4 +1,9 @@ + + + true + true + diff --git a/src/tests/Loader/classloader/regressions/dev10_403582/dev10_403582.ilproj b/src/tests/Loader/classloader/regressions/dev10_403582/dev10_403582.ilproj index 579e65d4199b66..7340d29ea37a26 100644 --- a/src/tests/Loader/classloader/regressions/dev10_403582/dev10_403582.ilproj +++ b/src/tests/Loader/classloader/regressions/dev10_403582/dev10_403582.ilproj @@ -1,10 +1,12 @@ - + true 1 true + + true diff --git a/src/tests/run.cmd b/src/tests/run.cmd index f8aeda3a9ea99a..a1b60649ecf102 100644 --- a/src/tests/run.cmd +++ b/src/tests/run.cmd @@ -28,6 +28,7 @@ set __msbuildExtraArgs= set __LongGCTests= set __GCSimulatorTests= set __IlasmRoundTrip= +set __UseManagedIlasm= set __PrintLastResultsOnly= set LogsDirArg= set RunInUnloadableContext= @@ -73,6 +74,7 @@ if /i "%1" == "gcstresslevel" (set DOTNET_GCStress=%2& if /i "%1" == "gcsimulator" (set __GCSimulatorTests=1&shift&goto Arg_Loop) if /i "%1" == "longgc" (set __LongGCTests=1&shift&goto Arg_Loop) if /i "%1" == "ilasmroundtrip" (set __IlasmRoundTrip=1&shift&goto Arg_Loop) +if /i "%1" == "usemanagedilasm" (set __IlasmRoundTrip=1&set __UseManagedIlasm=1&shift&goto Arg_Loop) if /i "%1" == "timeout" (set __TestTimeout=%2&shift&shift&goto Arg_Loop) if /i "%1" == "runincontext" (set RunInUnloadableContext=1&shift&goto Arg_Loop) if /i "%1" == "tieringtest" (set TieringTest=1&shift&goto Arg_Loop) @@ -152,6 +154,10 @@ if defined __IlasmRoundTrip ( set __RuntestPyArgs=%__RuntestPyArgs% --ilasmroundtrip ) +if defined __UseManagedIlasm ( + set __RuntestPyArgs=%__RuntestPyArgs% --use_managed_ilasm +) + if defined __TestEnv ( set __RuntestPyArgs=%__RuntestPyArgs% -test_env %__TestEnv% ) diff --git a/src/tests/run.py b/src/tests/run.py index 1cfc74d4c6ec51..7fb32639f11a45 100755 --- a/src/tests/run.py +++ b/src/tests/run.py @@ -101,6 +101,7 @@ parser.add_argument("--long_gc", dest="long_gc", action="store_true", default=False) parser.add_argument("--gcsimulator", dest="gcsimulator", action="store_true", default=False) parser.add_argument("--ilasmroundtrip", dest="ilasmroundtrip", action="store_true", default=False) +parser.add_argument("--use_managed_ilasm", dest="use_managed_ilasm", action="store_true", default=False) parser.add_argument("--run_crossgen2_tests", dest="run_crossgen2_tests", action="store_true", default=False) parser.add_argument("--large_version_bubble", dest="large_version_bubble", action="store_true", default=False) parser.add_argument("--synthesize_pgo", dest="synthesize_pgo", action="store_true", default=False) @@ -829,6 +830,15 @@ def run_tests(args, print("Setting RunningIlasmRoundTrip=1") os.environ["RunningIlasmRoundTrip"] = "1" + if args.use_managed_ilasm: + if not args.ilasmroundtrip: + print("--use_managed_ilasm implies --ilasmroundtrip; enabling ilasm round trip.") + print("Setting RunningIlasmRoundTrip=1") + os.environ["RunningIlasmRoundTrip"] = "1" + print("Using managed ILasm for round trip.") + print("Setting IlasmRoundTripUseManagedIlasm=1") + os.environ["IlasmRoundTripUseManagedIlasm"] = "1" + if args.run_crossgen2_tests: print("Running tests R2R (Crossgen2)") print("Setting RunCrossGen2=1") @@ -978,6 +988,11 @@ def setup_args(args): lambda arg: True, "Error setting ilasmroundtrip") + coreclr_setup_args.verify(args, + "use_managed_ilasm", + lambda arg: True, + "Error setting use_managed_ilasm") + coreclr_setup_args.verify(args, "large_version_bubble", lambda arg: True, From 3204491cb2e327fac41701d0bf919148e49879f7 Mon Sep 17 00:00:00 2001 From: Tom McDonald Date: Thu, 30 Apr 2026 07:33:07 -0400 Subject: [PATCH 032/115] Fix interpreter breakpoint to pass fIsVEH=FALSE to FirstChanceNativeException (#127592) Fix a Windows crash with x64 debugging with Interpreter breakpoints on CET-enabled hardware. Interpreter breakpoints are synthetic software callbacks, not vectored exception handler callbacks. We were passing in Passing `fIsVEH=TRUE` (the default) but this caused `SendSetThreadContextNeeded` to fire unconditionally for every interpreter breakpoint, resulting in a crash. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/interpexec.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/coreclr/vm/interpexec.cpp b/src/coreclr/vm/interpexec.cpp index 7fe0a4b3f00955..f6e8947984a19d 100644 --- a/src/coreclr/vm/interpexec.cpp +++ b/src/coreclr/vm/interpexec.cpp @@ -782,11 +782,14 @@ static void InterpBreakpoint(const int32_t *ip, const InterpMethodContextFrame * (void*)GetSP(&ctx), (void*)GetFP(&ctx))); + // Pass fIsVEH=FALSE: interpreter breakpoints are synthetic software callbacks, + // not vectored exception handler callbacks. if (g_pDebugInterface->FirstChanceNativeException( &exceptionRecord, &ctx, STATUS_BREAKPOINT, - pThread)) + pThread, + FALSE /* fIsVEH */)) { InterpThreadContext *pThreadContext = pThread->GetInterpThreadContext(); From 0d16f9b3263471ad544f79bf668ed7175715c131 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Thu, 30 Apr 2026 13:57:37 +0200 Subject: [PATCH 033/115] Update OSR docs for recent changes (#127384) This updates OSR docs to account for #127074, #127158 and #127213 (not yet merged). Also address some comment feedback from #127074. --- docs/design/features/OnStackReplacement.md | 62 +++++++++++-------- .../design/features/OsrDetailsAndDebugging.md | 20 +++--- src/coreclr/jit/emitxarch.cpp | 10 +-- 3 files changed, 50 insertions(+), 42 deletions(-) diff --git a/docs/design/features/OnStackReplacement.md b/docs/design/features/OnStackReplacement.md index db202a22666e40..d8b08f4a1edeaf 100644 --- a/docs/design/features/OnStackReplacement.md +++ b/docs/design/features/OnStackReplacement.md @@ -237,15 +237,23 @@ pseudocode: ``` Patchpoint: // each assigned a dense set of IDs - if (++counter[ppID] > threshold) call PatchpointHelper(ppID) + if (++counter[ppID] > threshold) + { + var continuation = PatchpointHelper(ppID); + jmp continuation; + } ``` -The helper can use the return address to determine which patchpoint is making -the request. To keep overheads manageable, we might instead want to down-count -and pass the counter address to the helper. +The helper can use the return address to determine which patchpoint is making the request. +The return address is also used in case we should continue without transitioning into an OSR method. +To keep overheads manageable, we might instead want to down-count and pass the counter address to the helper. ``` Patchpoint: // each assigned a dense set of IDs - if (--counter[ppID] <= 0) call PatchpointHelper(ppID, &counter[ppID]) + if (--counter[ppID] <= 0) + { + var continuation = PatchpointHelper(ppID, &counter[ppID]); + jmp continuation; + } ``` The helper logic would be similar to the following: ``` @@ -259,20 +267,20 @@ PatchpointHelper(int ppID, int* counter) case Unknown: *counter = initialThreshold; SetState(s, Active); - return; + return patchpointSite + ; case Active: *counter = checkThreshold; SetState(s, Pending); RequestAlternative(ppID); - return; + return patchpointSite + ; case Pending: *counter = checkThreshold; - return; + return patchpointSite + ; case Ready: - Transition(...); // does not return + return
; } } ``` @@ -477,15 +485,13 @@ this is to just leave the original frame in place, and have the OSR frame #### 3.4.1 Transition Implementation -The original method conditionally calls to the patchpoint helper at -patchpoints. The helper will return if there is no transition. +The original method conditionally calls to the patchpoint helper at patchpoints. +The helper returns a continuation address. +If transition is desired, this is the address of the alternative version. +Otherwise, it is the address in the tier0 code that follows the patchpoint helper call and jump instruction. -For a transition, the helper will capture context and virtually unwind itself -and the original method from the stack to recover callee-save register values -live into the original method and then restore the callee FP and SP values into -the context (preserving the original method frame); then set the context IP to -the OSR method entry and restore context. OSR method will incorporate the -original method frame as part of its frame. +After transitioning the OSR method will incorporate the original method frame as part of its frame. +This incorporation is slightly different between x64 and other targets. See below for more details. ## 4 Complications @@ -658,13 +664,16 @@ prolog and duplicates its saves, and then a subsequent "shrink wrapped" prolog #### Implementation -Callee-saves are currently handled sightly differently on x64 -than it is on arm64: -* on x64, all the integer callee saves are saved in space pre-reserved in the Tier0 frame. The Tier0 method saves whatever subset it uses, and the OSR method saves any additional callee saves it uses. THe OSR method then restores this entire set on exit, with a single stack pointer adjustment. See [OSR x64 Epilog Redesign](https://github.com/dotnet/runtime/blob/main/docs/design/features/OSRX64EpilogRedesign.md) and the pull request [revise approach for x64 OSR epilogs](https://github.com/dotnet/runtime/pull/65609) for details. -* for arm64, the virtual unwind done by the runtime restores the Tier0 callee saves, so the OSR method saves and restores the full set of callee saves it uses, and then does a second stack pointer adjustment to pop the Tier0 frame. -Eventually we will revise arm64 to behave more like x64. -* float callee-saves are handled separately for tier0 and OSR methods; there is opportunity here to also share save space as we do for x64 integer registers, -but this might also lead to needlessly large tier0 frames. +Callee-saves are currently handled differently on x64 than it is on other targets: +* on x64, all the integer callee saves are saved in space pre-reserved in the Tier0 frame. + The Tier0 method saves whatever subset it uses, and the OSR method saves any additional callee saves it uses. + The OSR method then restores this entire set on exit, with a single stack pointer adjustment. + See [OSR x64 Epilog Redesign](https://github.com/dotnet/runtime/blob/main/docs/design/features/OSRX64EpilogRedesign.md) and the pull request [revise approach for x64 OSR epilogs](https://github.com/dotnet/runtime/pull/65609) for details. +* for other targets the OSR method first restores the full set of callee saves saved by the tier0 version. + Its used callee saves are then saved and restored from the OSR part of the stack frame, in the same way as any normal prolog. +* For x64 we disallow the use of float callee-saves in the tier0 method. + This avoids the need for special restore logic for float callee saves in the OSR method. + For other platforms the handling of callee saves falls out naturally together with the integer register handling. You might think the runtime helper would need to carefully save all the register state on entry, but that's not the case. Because the original method is un-optimized, @@ -672,9 +681,7 @@ there isn't any live IL state in registers across the call to the patchpoint helper—all the live IL state for the method is on the original frame—so the argument and caller-save registers are dead at the patchpoint. Thus only part of register state that is significant for ongoing -computation is the callee-saves, which are recovered via virtual unwind, and the -frame and stack pointers of the original method, which are likewise recovered by -virtual unwind. +computation is the callee-saves and frame and stack pointers. If we were to support patchpoints in optimized code things would be more complicated. @@ -803,6 +810,7 @@ G_M6138_IG04: ;; bbWeight=0.01 488D4DF0 lea rcx, bword ptr [rbp-10H] // &patchpointCounter BA06000000 mov edx, 6 // ilOffset E808CA465F call CORINFO_HELP_PATCHPOINT + jmp rax G_M6138_IG05: 8B45FC mov eax, dword ptr [rbp-04H] diff --git a/docs/design/features/OsrDetailsAndDebugging.md b/docs/design/features/OsrDetailsAndDebugging.md index e1080fbc8bd792..262a0d435524cd 100644 --- a/docs/design/features/OsrDetailsAndDebugging.md +++ b/docs/design/features/OsrDetailsAndDebugging.md @@ -281,7 +281,15 @@ But often much of the Tier0 frame is effectively dead after the transition and e The OSR prolog is conceptually similar to a normal method prolog, with a few key difference. -When an OSR method is entered, all callee-save registers have the values they had when the Tier0 method was called, but the values in argument registers are unknown (and almost certainly not the args passed to the Tier0 method). The OSR method must initialize any live-in enregistered args or locals from the corresponding slots on the Tier0 frame. This happens in `genEnregisterOSRArgsAndLocals`. +An OSR method is entered via a jump from the tier0 method. +This means callee save registers used by the tier0 method may require special handling: +- On x64, the OSR method keeps the original values of the callee saves in the tier0 frame. + They will be restored directly by the epilog, meaning that no instructions are needed. +- For other targets the callee saves used by tier0 are restored in the prolog, and they are then saved again in the OSR frame as normal. + The above happens in `genOSRHandleTier0CalleeSavedRegistersAndFrame`. + +The OSR method must also initialize any live-in enregistered args or locals from the corresponding slots on the Tier0 frame. +This happens in `genEnregisterOSRArgsAndLocals`. If the OSR method needs to report a generics context it uses the Tier0 frame slot; we ensure this is possible by forcing a Tier0 method with patchpoints to always report its generics context. @@ -309,7 +317,7 @@ OSR funclets are more or less normal funclets. #### OSR Unwind Info -On x64 the prolog unwind includes a phantom SP adjustment at offset 0 for the Tier0 frame. +The prolog unwind includes a phantom SP adjustment at offset 0 for the Tier0 frame. As noted above the two SP adjusts in the x64 epilog are currently causing problems if we try and unwind in the epilog. Unwinding in the prolog and method body seems to work correctly; the unwind codes properly describe what needs to be done. @@ -323,12 +331,11 @@ OSR GC info is standard. The only unusual aspect is that some special offsets (g ### Execution of an OSR Method -OSR methods are never called directly; they can only be invoked by `CORINFO_HELP_PATCHPOINT` when called from a Tier0 method with patchpoints. +OSR methods are never called directly; they can only be invoked by jump from a Tier0 method with patchpoints. -On x64, to preserve proper stack alignment, the runtime helper will "push" a phantom return address on the stack (x64 methods assume SP is aligned 8 mod 16 on entry). This is not necessary on arm64 as calls do not push to the stack. +On x64, to preserve proper stack alignment, the prolog will "push" a phantom return address on the stack (x64 methods assume SP is aligned 8 mod 16 on entry). This is not necessary on arm64 as calls do not push to the stack. -When the OSR method returns, it cleans up both its own stack and the -Tier0 method stack. +When the OSR method returns, it cleans up both its own stack and the Tier0 method stack. Note if a Tier0 method is recursive and has loops there can be some interesting dynamics. After a sufficient amount of looping an OSR method will be created, and the currently active Tier0 instance will transition to the OSR method. When the OSR method makes a recursive call, it will invoke the Tier0 method, which will then fairly quickly transition to the OSR version just created. @@ -474,7 +481,6 @@ to spend considerable time in OSR methods (e.g., the all-in-`Main` benchmark). Generally speaking the performance of an OSR method should be comparable to the equivalent Tier1 method. In practice we see variations of +/- 20% or so. There are a number or reasons for this: * OSR methods are often a subset of the full Tier1 method, and in many cases just comprise one loop. The JIT can often generate much better code for a single loop in isolation than a single loop in a more complex method. -* A few optimizations are disabled in OSR methods, notably struct promotion. * OSR methods may only see fractional PGO data (as parts of the Tier0 method may not have executed yet). The JIT doesn't cope very well yet with this sort of partial PGO coverage. ### Impact on BenchmarkDotNet Results diff --git a/src/coreclr/jit/emitxarch.cpp b/src/coreclr/jit/emitxarch.cpp index e8b6cd44a8ce64..c4389db758c22a 100644 --- a/src/coreclr/jit/emitxarch.cpp +++ b/src/coreclr/jit/emitxarch.cpp @@ -9123,10 +9123,7 @@ void emitter::emitIns_R_L(instruction ins, emitAttr attr, BasicBlock* dst, regNu emitTotalIGjmps++; #endif - // Set the relocation flags - these give hint to zap to perform - // relocation of the specified 32bit address. - // - // Note the relocation flags influence the size estimate. + // Set reloc flags for AOT purposes. This also affects emitInsSizeAM below. id->idSetRelocFlags(attr); UNATIVE_OFFSET sz = emitInsSizeAM(id, insCodeRM(ins)); @@ -9185,10 +9182,7 @@ void emitter::emitIns_R_L(instruction ins, emitAttr attr, insGroup* dst, regNumb emitTotalIGjmps++; #endif - // Set the relocation flags - these give hint to zap to perform - // relocation of the specified 32bit address. - // - // Note the relocation flags influence the size estimate. + // Set reloc flags for AOT purposes. This also affects emitInsSizeAM below. id->idSetRelocFlags(attr); UNATIVE_OFFSET sz = emitInsSizeAM(id, insCodeRM(ins)); From c4244d9e26949a3f28868c8e0ac2655461e8ced9 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Thu, 30 Apr 2026 13:59:12 +0200 Subject: [PATCH 034/115] JIT: VN-level (a-x)+x and a-(a-x) folds (#127533) Add two safe value-numbering algebraic identities for non-overflow integer types: - `(a - x) + x == a` (and commutative variants) - `a - (a - x) == x` [Diffs](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1400847&view=ms.vss-build-web.run-extensions-tab) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/valuenum.cpp | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/coreclr/jit/valuenum.cpp b/src/coreclr/jit/valuenum.cpp index 52000ff982a315..6570e0c5965162 100644 --- a/src/coreclr/jit/valuenum.cpp +++ b/src/coreclr/jit/valuenum.cpp @@ -5304,7 +5304,7 @@ ValueNum ValueNumStore::EvalUsingMathIdentity(var_types typ, VNFunc func, ValueN opVN = arg0VN; } - auto identityForAddition = [=]() -> ValueNum { + auto identityForAddition = [=](bool ovf) -> ValueNum { ValueNum ZeroVN = VNZeroForType(typ); if (!varTypeIsFloating(typ)) @@ -5314,6 +5314,25 @@ ValueNum ValueNumStore::EvalUsingMathIdentity(var_types typ, VNFunc func, ValueN { return opVN; } + + if (!ovf) + { + // (a - x) + x == a and x + (a - x) == a + // (x - a) + a == x and a + (x - a) == x + // + // Since ADD is commutative, the args have already been canonicalized, + // but we don't know which side is the SUB. Check both arrangements. + for (int i = 0; i < 2; i++) + { + ValueNum subVN = (i == 0) ? arg0VN : arg1VN; + ValueNum otherVN = (i == 0) ? arg1VN : arg0VN; + VNFuncApp sub; + if (GetVNFunc(subVN, &sub) && (sub.m_func == VNF_SUB) && (sub.m_args[1] == otherVN)) + { + return sub.m_args[0]; + } + } + } } else if (cnsVN == NoVN) { @@ -5388,6 +5407,15 @@ ValueNum ValueNumStore::EvalUsingMathIdentity(var_types typ, VNFunc func, ValueN } } + // a - (a - x) == x + if (IsVNBinFunc(arg1VN, VNF_SUB, &arg1Op1, &arg1Op2)) + { + if (arg1Op1 == arg0VN) + { + return arg1Op2; + } + } + // (x + a) - x == a // (a + x) - x == a VNFuncApp add; @@ -5517,7 +5545,7 @@ ValueNum ValueNumStore::EvalUsingMathIdentity(var_types typ, VNFunc func, ValueN { case GT_ADD: { - resultVN = identityForAddition(); + resultVN = identityForAddition(/* ovf */ false); break; } @@ -6023,7 +6051,7 @@ ValueNum ValueNumStore::EvalUsingMathIdentity(var_types typ, VNFunc func, ValueN case VNF_ADD_OVF: case VNF_ADD_UN_OVF: { - resultVN = identityForAddition(); + resultVN = identityForAddition(/* ovf */ true); break; } From 6732d727f41efba32132bcb0c911825810bf3fa1 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 05:42:14 -0700 Subject: [PATCH 035/115] Remove ApplicationContext::GetApplicationName() (#127597) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ApplicationContext::GetApplicationName()` and its backing field `m_applicationName` were dead code — declared and defined but never called anywhere in the binder. Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: elinor-fung <47805090+elinor-fung@users.noreply.github.com> --- src/coreclr/binder/inc/applicationcontext.hpp | 3 --- src/coreclr/binder/inc/applicationcontext.inl | 5 ----- 2 files changed, 8 deletions(-) diff --git a/src/coreclr/binder/inc/applicationcontext.hpp b/src/coreclr/binder/inc/applicationcontext.hpp index 53bcfbcac48012..680e993c5875fe 100644 --- a/src/coreclr/binder/inc/applicationcontext.hpp +++ b/src/coreclr/binder/inc/applicationcontext.hpp @@ -84,8 +84,6 @@ namespace BINDER_SPACE ~ApplicationContext(); HRESULT Init(); - inline SString &GetApplicationName(); - HRESULT SetupBindingPaths(/* in */ SString &sTrustedPlatformAssemblies, /* in */ SString &sPlatformResourceRoots, /* in */ SString &sAppPaths, @@ -108,7 +106,6 @@ namespace BINDER_SPACE private: Volatile m_cVersion; - SString m_applicationName; ExecutionContext *m_pExecutionContext; FailureCache *m_pFailureCache; CRITSEC_COOKIE m_contextCS; diff --git a/src/coreclr/binder/inc/applicationcontext.inl b/src/coreclr/binder/inc/applicationcontext.inl index 16ec0c6d87de2a..59c84a7603f9ce 100644 --- a/src/coreclr/binder/inc/applicationcontext.inl +++ b/src/coreclr/binder/inc/applicationcontext.inl @@ -24,11 +24,6 @@ void ApplicationContext::IncrementVersion() InterlockedIncrement(&m_cVersion); } -SString &ApplicationContext::GetApplicationName() -{ - return m_applicationName; -} - ExecutionContext *ApplicationContext::GetExecutionContext() { return m_pExecutionContext; From 05dd3b004e794d340b87210464b28755cf733395 Mon Sep 17 00:00:00 2001 From: BoyBaykiller <88141582+BoyBaykiller@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:46:12 +0200 Subject: [PATCH 036/115] JIT: Treat store in JTRUE block as the ElseOperation in if-conversion (#124738) If-conversion phase now produces the same IR for these two cases: ```cs bool First(int tMinLeft, int tMinRight) { bool leftCloser = false; if (tMinLeft < tMinRight) { leftCloser = true; } return leftCloser; } bool Second(int tMinLeft, int tMinRight) { bool leftCloser; if (tMinLeft < tMinRight) { leftCloser = true; } else { leftCloser = false; } return leftCloser; } ``` The last unconditional store (`leftCloser = false`) is substituted into the SELECT which enables further optimization. Specifically it fixes #124713. --- src/coreclr/jit/ifconversion.cpp | 178 ++++++++++++++++++++++++------- 1 file changed, 141 insertions(+), 37 deletions(-) diff --git a/src/coreclr/jit/ifconversion.cpp b/src/coreclr/jit/ifconversion.cpp index 836643225d0b3f..d14473d12f551a 100644 --- a/src/coreclr/jit/ifconversion.cpp +++ b/src/coreclr/jit/ifconversion.cpp @@ -30,8 +30,8 @@ class OptIfConversionDsc private: Compiler* m_compiler; // The Compiler instance. - BasicBlock* m_startBlock; // First block in the If Conversion. - BasicBlock* m_finalBlock = nullptr; // Block where the flows merge. In a return case, this can be nullptr. + BasicBlock* m_startBlock; // JTRUE block where flow diverges. + BasicBlock* m_finalBlock = nullptr; // Final block where flow merges again. Can be nullptr in return case. // The node, statement and block of an operation. struct IfConvertOperation @@ -41,16 +41,16 @@ class OptIfConversionDsc GenTree* node = nullptr; }; - GenTree* m_cond; // The condition in the conversion + GenTree* m_cond; // The condition in the conversion. IfConvertOperation m_thenOperation; // The single operation in the Then case. IfConvertOperation m_elseOperation; // The single operation in the Else case. - genTreeOps m_mainOper = GT_COUNT; // The main oper of the if conversion. - bool m_doElseConversion = false; // Does the If conversion have an else statement. + genTreeOps m_mainOper = GT_COUNT; // The main oper of the if conversion. bool IfConvertCheck(); bool IfConvertCheckFlow(); bool IfConvertCheckStmts(BasicBlock* block, IfConvertOperation* foundOperation); + bool IfConvertTryGetElseFromJtrueBlock(GenTreeLclVar* thenStore, IfConvertOperation* foundOperation); GenTree* TryTransformSelectOperOrLocal(GenTree* oper, GenTree* lcl); GenTree* TryTransformSelectOperOrZero(GenTree* oper, GenTree* lcl); @@ -59,6 +59,13 @@ class OptIfConversionDsc void IfConvertDump(); #endif + bool HasElseBlock() + { + // Note: Even when this is false we can have an Else operation + // by treating a STORE inside JTRUE block as one + return m_startBlock->GetTrueTarget()->GetUniquePred(m_compiler) != nullptr; + } + public: bool optIfConvert(int* pReachabilityBudget); }; @@ -69,7 +76,7 @@ class OptIfConversionDsc // Check whether the JTRUE block and its successors can be expressed as a SELECT. // In the process, get the data required to perform the transformation. // Notes: -// Sets m_finalBlock, m_doElseConversion, m_thenOperation, m_elseOperation and m_mainOper +// Sets m_finalBlock, m_thenOperation, m_elseOperation and m_mainOper // bool OptIfConversionDsc::IfConvertCheck() { @@ -80,19 +87,30 @@ bool OptIfConversionDsc::IfConvertCheck() if (!IfConvertCheckStmts(m_startBlock->GetFalseTarget(), &m_thenOperation)) { + m_thenOperation = {}; return false; } m_mainOper = m_thenOperation.node->OperGet(); assert(m_mainOper == GT_RETURN || m_mainOper == GT_STORE_LCL_VAR); - if (m_doElseConversion) + if (HasElseBlock()) { if (!IfConvertCheckStmts(m_startBlock->GetTrueTarget(), &m_elseOperation)) { + m_elseOperation = {}; return false; } + } + else if (m_startBlock->StatementCount() > 1) + { + assert(m_mainOper == GT_STORE_LCL_VAR); + + IfConvertTryGetElseFromJtrueBlock(m_thenOperation.node->AsLclVar(), &m_elseOperation); + } + if (m_elseOperation.block != nullptr) + { // Both operations are the same node type. assert(m_thenOperation.node->OperGet() == m_elseOperation.node->OperGet()); @@ -118,7 +136,7 @@ bool OptIfConversionDsc::IfConvertCheck() // Check if there is a valid flow from m_startBlock to a final block. // // Notes: -// Sets m_finalBlock and m_doElseConversion. +// Sets m_finalBlock. // bool OptIfConversionDsc::IfConvertCheckFlow() { @@ -130,8 +148,7 @@ bool OptIfConversionDsc::IfConvertCheckFlow() return false; } - m_doElseConversion = trueBb->GetUniquePred(m_compiler) != nullptr; - m_finalBlock = m_doElseConversion ? trueBb->GetUniqueSucc() : trueBb; + m_finalBlock = HasElseBlock() ? trueBb->GetUniqueSucc() : trueBb; // m_finalBlock is only allowed to be null if both return. // E.g: Then block exits by throwing an exception => we bail here. @@ -154,7 +171,7 @@ bool OptIfConversionDsc::IfConvertCheckFlow() // foundOperation - The found operation // // Returns: -// True if the statements are valid for an If conversion. In that case foundOperation is also set. +// True if the statements are valid for an If conversion. In which case foundOperation is set. // bool OptIfConversionDsc::IfConvertCheckStmts(BasicBlock* block, IfConvertOperation* foundOperation) { @@ -219,6 +236,84 @@ bool OptIfConversionDsc::IfConvertCheckStmts(BasicBlock* block, IfConvertOperati return found; } +//----------------------------------------------------------------------------- +// IfConvertTryGetElseFromJtrueBlock +// +// Look for a STORE to the same local that thenStore targets and +// see if we can safely move it to after JTRUE and thenStore stmts. +// If so it is effectively the Else operation. Assumes there is no Else block. +// +// Arguments: +// thenStore - The existing store inside the Then block +// foundOperation - The found operation +// +// Returns: +// True if a corresponding Else operation was found. In which case foundOperation is set. +// +bool OptIfConversionDsc::IfConvertTryGetElseFromJtrueBlock(GenTreeLclVar* thenStore, IfConvertOperation* foundOperation) +{ + assert(!HasElseBlock()); + + unsigned targetLclNum = thenStore->GetLclNum(); + + if (m_compiler->lvaGetDesc(targetLclNum)->IsAddressExposed()) + { + return false; + } + + assert((thenStore->Data()->gtFlags & GTF_SIDE_EFFECT) == 0); + if (m_compiler->gtTreeHasLocalRead(thenStore->Data(), targetLclNum)) + { + return false; + } + + int stmtSearchBudget = 8; + bool hasEhSuccs = m_startBlock->HasPotentialEHSuccs(m_compiler); + Statement* last = m_startBlock->lastStmt(); + Statement* stmt = last; + do + { + if (stmtSearchBudget-- <= 0) + { + break; + } + + GenTree* tree = stmt->GetRootNode(); + if (tree->OperIs(GT_STORE_LCL_VAR)) + { + GenTreeLclVar* prevStore = tree->AsLclVar(); + if (prevStore->GetLclNum() == targetLclNum) + { + if (prevStore->Data()->IsInvariant()) + { + m_elseOperation.block = m_startBlock; + m_elseOperation.stmt = stmt; + m_elseOperation.node = tree; + + return true; + } + + // We found a STORE but its def might evaluate to something else when moving + return false; + } + } + + if (((tree->gtFlags & GTF_EXCEPT) != 0) && hasEhSuccs) + { + break; + } + + if (m_compiler->gtTreeHasLocalRead(tree, targetLclNum) || m_compiler->gtTreeHasLocalStore(tree, targetLclNum)) + { + break; + } + + stmt = stmt->GetPrevStmt(); + } while (stmt != last); + + return false; +} + //----------------------------------------------------------------------------- // IfConvertDump // @@ -232,11 +327,13 @@ void OptIfConversionDsc::IfConvertDump() // Then & Else only exist before the transformation if (m_startBlock->KindIs(BBJ_COND)) { - m_compiler->fgDumpBlock(m_thenOperation.block); - if (m_doElseConversion) + JITDUMP("\n------------------------------------"); + m_compiler->fgDumpStmtTree(m_thenOperation.block, m_thenOperation.stmt); + if (m_elseOperation.block != nullptr) { - m_compiler->fgDumpBlock(m_elseOperation.block); + m_compiler->fgDumpStmtTree(m_elseOperation.block, m_elseOperation.stmt); } + JITDUMP("------------------------------------\n"); } } #endif @@ -398,12 +495,13 @@ bool OptIfConversionDsc::optIfConvert(int* pReachabilityBudget) #ifdef DEBUG if (m_compiler->verbose) { - JITDUMP("\nConditionally executing " FMT_BB, m_thenOperation.block->bbNum); - if (m_doElseConversion) + JITDUMP("JTRUE block is " FMT_BB ". ", m_startBlock->bbNum); + JITDUMP("Statement " FMT_STMT " (Then) ", m_thenOperation.stmt->GetID()); + if (m_elseOperation.block != nullptr) { - JITDUMP(" and " FMT_BB, m_elseOperation.block->bbNum); + JITDUMP("and " FMT_STMT " (Else) ", m_elseOperation.stmt->GetID()); } - JITDUMP(" inside " FMT_BB "\n", m_startBlock->bbNum); + JITDUMP("can be expressed as SELECT:\n"); IfConvertDump(); } #endif @@ -419,7 +517,7 @@ bool OptIfConversionDsc::optIfConvert(int* pReachabilityBudget) { thenCost = m_thenOperation.node->AsLclVar()->Data()->GetCostEx() + (m_compiler->gtIsLikelyRegVar(m_thenOperation.node) ? 0 : 2); - if (m_doElseConversion) + if (HasElseBlock()) { elseCost = m_elseOperation.node->AsLclVar()->Data()->GetCostEx() + (m_compiler->gtIsLikelyRegVar(m_elseOperation.node) ? 0 : 2); @@ -428,11 +526,9 @@ bool OptIfConversionDsc::optIfConvert(int* pReachabilityBudget) else { assert(m_mainOper == GT_RETURN); + assert(HasElseBlock()); thenCost = m_thenOperation.node->AsOp()->GetReturnValue()->GetCostEx(); - if (m_doElseConversion) - { - elseCost = m_elseOperation.node->AsOp()->GetReturnValue()->GetCostEx(); - } + elseCost = m_elseOperation.node->AsOp()->GetReturnValue()->GetCostEx(); } // Cost to allow for "x = cond ? a + b : c + d". @@ -478,7 +574,7 @@ bool OptIfConversionDsc::optIfConvert(int* pReachabilityBudget) if (m_mainOper == GT_STORE_LCL_VAR) { selectFalseInput = m_thenOperation.node->AsLclVar()->Data(); - selectTrueInput = m_doElseConversion ? m_elseOperation.node->AsLclVar()->Data() : nullptr; + selectTrueInput = (m_elseOperation.block != nullptr) ? m_elseOperation.node->AsLclVar()->Data() : nullptr; // Pick the type as the type of the local, which should always be compatible even for implicit coercions. selectType = genActualType(m_thenOperation.node); @@ -486,7 +582,7 @@ bool OptIfConversionDsc::optIfConvert(int* pReachabilityBudget) else { assert(m_mainOper == GT_RETURN); - assert(m_doElseConversion); + assert(m_elseOperation.block != nullptr); assert(m_thenOperation.node->TypeGet() == m_elseOperation.node->TypeGet()); selectTrueInput = m_elseOperation.node->AsOp()->GetReturnValue(); @@ -504,7 +600,7 @@ bool OptIfConversionDsc::optIfConvert(int* pReachabilityBudget) if (selectTrueInput == nullptr) { // Duplicate the destination of the Then store. - assert(m_mainOper == GT_STORE_LCL_VAR && !m_doElseConversion); + assert(m_mainOper == GT_STORE_LCL_VAR && (m_elseOperation.block == nullptr)); GenTreeLclVar* store = m_thenOperation.node->AsLclVar(); selectTrueInput = m_compiler->gtNewLclVarNode(store->GetLclNum(), store->TypeGet()); } @@ -525,7 +621,7 @@ bool OptIfConversionDsc::optIfConvert(int* pReachabilityBudget) m_compiler->gtSetEvalOrder(m_thenOperation.node); m_compiler->fgSetStmtSeq(m_thenOperation.stmt); - // Replace JTRUE with STORE(SELECT)/RETURN(SELECT) statement + // Replace JTRUE with STORE(SELECT)/RETURN(SELECT) statement. m_compiler->fgInsertStmtBefore(m_startBlock, m_startBlock->lastStmt(), m_thenOperation.stmt); m_compiler->fgRemoveStmt(m_startBlock, m_startBlock->lastStmt()); m_thenOperation.block->SetFirstStmt(nullptr); @@ -533,8 +629,9 @@ bool OptIfConversionDsc::optIfConvert(int* pReachabilityBudget) BasicBlock* falseBb = m_startBlock->GetFalseTarget(); BasicBlock* trueBb = m_startBlock->GetTrueTarget(); - // JTRUE block now contains SELECT. Change it's kind and make it flow + // JTRUE block now contains SELECT. Change its kind and make it flow // directly into block where flows merge, which is null in case of GT_RETURN. + bool hasElseBlock = HasElseBlock(); if (m_mainOper == GT_RETURN) { m_startBlock->SetKindAndTargetEdge(BBJ_RETURN); @@ -542,21 +639,28 @@ bool OptIfConversionDsc::optIfConvert(int* pReachabilityBudget) else { FlowEdge* newEdge = - m_doElseConversion ? m_compiler->fgAddRefPred(m_finalBlock, m_startBlock) : m_startBlock->GetTrueEdge(); + hasElseBlock ? m_compiler->fgAddRefPred(m_finalBlock, m_startBlock) : m_startBlock->GetTrueEdge(); m_startBlock->SetKindAndTargetEdge(BBJ_ALWAYS, newEdge); } assert(m_startBlock->GetUniqueSucc() == m_finalBlock); - // Remove Then/Else block - auto removeBlock = [&](BasicBlock* start) { - start->bbWeight = BB_ZERO_WEIGHT; - m_compiler->fgRemoveAllRefPreds(start, m_startBlock); - m_compiler->fgRemoveBlock(start, true); + auto removeBlock = [&](BasicBlock* block) { + block->bbWeight = BB_ZERO_WEIGHT; + m_compiler->fgRemoveAllRefPreds(block, m_startBlock); + m_compiler->fgRemoveBlock(block, true); }; + removeBlock(falseBb); - if (m_doElseConversion) + if (m_elseOperation.block != nullptr) { - removeBlock(trueBb); + if (hasElseBlock) + { + removeBlock(trueBb); + } + else + { + m_compiler->fgRemoveStmt(m_startBlock, m_elseOperation.stmt); + } } #ifdef DEBUG @@ -785,7 +889,7 @@ GenTree* OptIfConversionDsc::TryTransformSelectToOrdinaryOps(GenTree* trueInput, { if (trueInput == nullptr) { - assert(m_mainOper == GT_STORE_LCL_VAR && !m_doElseConversion); + assert(m_mainOper == GT_STORE_LCL_VAR && (m_elseOperation.block == nullptr)); trueInput = m_thenOperation.node; } From c97aac601d74a6cd1df45f3d06e4d72071a33a75 Mon Sep 17 00:00:00 2001 From: Milos Kotlar Date: Thu, 30 Apr 2026 15:10:47 +0200 Subject: [PATCH 037/115] [clr-ios] Enable System.Private.Xml tests on CoreCLR (#127464) ## Description This PR enables System.Private.Xml tests on Apple mobile CoreCLR. The local tests succeeded without failures. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Xslt/XslCompiledTransformApi/XslCompiledTransform.cs | 2 ++ .../tests/Xslt/XslCompiledTransformApi/XsltApiV2.cs | 1 + .../tests/DataContractSerializer.cs | 1 + src/libraries/tests.proj | 7 ------- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Private.Xml/tests/Xslt/XslCompiledTransformApi/XslCompiledTransform.cs b/src/libraries/System.Private.Xml/tests/Xslt/XslCompiledTransformApi/XslCompiledTransform.cs index 00511a00ec6f19..7f9707004c9bdf 100644 --- a/src/libraries/System.Private.Xml/tests/Xslt/XslCompiledTransformApi/XslCompiledTransform.cs +++ b/src/libraries/System.Private.Xml/tests/Xslt/XslCompiledTransformApi/XslCompiledTransform.cs @@ -600,6 +600,7 @@ public void XmlResolver3(object param, XslInputType xslInputType, ReaderType rea [InlineData(XslInputType.Navigator, ReaderType.XmlValidatingReader, OutputType.Writer, NavType.XPathDocument)] [InlineData(XslInputType.Navigator, ReaderType.XmlValidatingReader, OutputType.TextWriter, NavType.XPathDocument)] [Theory] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124344", typeof(PlatformDetection), nameof(PlatformDetection.IsAppleMobile), nameof(PlatformDetection.IsCoreCLR))] public void XmlResolver7(XslInputType xslInputType, ReaderType readerType, OutputType outputType, NavType navType) { using (new AllowDefaultResolverContext()) @@ -2548,6 +2549,7 @@ public void XmlResolver3(object param, XslInputType xslInputType, ReaderType rea [InlineData(XslInputType.Navigator, ReaderType.XmlValidatingReader, OutputType.Writer, NavType.XPathDocument)] [InlineData(XslInputType.Navigator, ReaderType.XmlValidatingReader, OutputType.TextWriter, NavType.XPathDocument)] [Theory] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124344", typeof(PlatformDetection), nameof(PlatformDetection.IsAppleMobile), nameof(PlatformDetection.IsCoreCLR))] public void XmlResolver5(XslInputType xslInputType, ReaderType readerType, OutputType outputType, NavType navType) { using (new AllowDefaultResolverContext()) diff --git a/src/libraries/System.Private.Xml/tests/Xslt/XslCompiledTransformApi/XsltApiV2.cs b/src/libraries/System.Private.Xml/tests/Xslt/XslCompiledTransformApi/XsltApiV2.cs index a4cee46f961fe2..7dc7693a689ead 100644 --- a/src/libraries/System.Private.Xml/tests/Xslt/XslCompiledTransformApi/XsltApiV2.cs +++ b/src/libraries/System.Private.Xml/tests/Xslt/XslCompiledTransformApi/XsltApiV2.cs @@ -38,6 +38,7 @@ public enum NavType // //////////////////////////////////////////////////////////////// [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsReflectionEmitSupported))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/124344", typeof(PlatformDetection), nameof(PlatformDetection.IsAppleMobile), nameof(PlatformDetection.IsCoreCLR))] public class XsltApiTestCaseBase2 { // Generic data for all derived test cases diff --git a/src/libraries/System.Runtime.Serialization.Xml/tests/DataContractSerializer.cs b/src/libraries/System.Runtime.Serialization.Xml/tests/DataContractSerializer.cs index ef58dd59207ced..c78e842212dbdb 100644 --- a/src/libraries/System.Runtime.Serialization.Xml/tests/DataContractSerializer.cs +++ b/src/libraries/System.Runtime.Serialization.Xml/tests/DataContractSerializer.cs @@ -4568,6 +4568,7 @@ public static void DCS_TypeWithPrimitiveKnownTypes() private static bool IsNotWindowsRandomOSR => !PlatformDetection.IsWindows || (Environment.GetEnvironmentVariable("DOTNET_JitRandomOnStackReplacement") == null); [SkipOnPlatform(TestPlatforms.Browser, "Causes a stack overflow")] + [ActiveIssue("https://github.com/dotnet/runtime/issues/127463", typeof(PlatformDetection), nameof(PlatformDetection.IsAppleMobile), nameof(PlatformDetection.IsCoreCLR))] [ConditionalFact(typeof(DataContractSerializerTests), nameof(IsNotWindowsRandomOSR))] public static void DCS_DeeplyLinkedData() { diff --git a/src/libraries/tests.proj b/src/libraries/tests.proj index 5951e8d101798f..1290579eeede68 100644 --- a/src/libraries/tests.proj +++ b/src/libraries/tests.proj @@ -654,13 +654,6 @@ - - - - - - - From da9be991525537758334f6e584a1d7fbf400597a Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 30 Apr 2026 16:14:48 +0300 Subject: [PATCH 038/115] Add JSONL serialization support to System.Text.Json (#127567) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #126395. ## Summary Adds four new public `JsonSerializer.SerializeAsyncEnumerable` overloads (`Stream` and `PipeWriter`, each with `JsonSerializerOptions` and `JsonTypeInfo` variants) that serialize an `IAsyncEnumerable` either as a root-level JSON array (default) or as a [JSON Lines (JSONL)](https://jsonlines.org/) document when `topLevelValues: true` — mirroring the existing `DeserializeAsyncEnumerable` shape. Approved API: https://github.com/dotnet/runtime/issues/126395#issuecomment-4338250586 ```csharp namespace System.Text.Json; public static partial class JsonSerializer { public static Task SerializeAsyncEnumerable( Stream utf8Json, IAsyncEnumerable value, bool topLevelValues = false, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default); public static Task SerializeAsyncEnumerable( Stream utf8Json, IAsyncEnumerable value, JsonTypeInfo jsonTypeInfo, bool topLevelValues = false, CancellationToken cancellationToken = default); public static Task SerializeAsyncEnumerable( PipeWriter utf8Json, IAsyncEnumerable value, bool topLevelValues = false, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default); public static Task SerializeAsyncEnumerable( PipeWriter utf8Json, IAsyncEnumerable value, JsonTypeInfo jsonTypeInfo, bool topLevelValues = false, CancellationToken cancellationToken = default); } ``` ## API-review feedback addressed 1. **Canonical `\n` line terminator** — emits `(byte)`'`\n`'` between (and after) each value regardless of `JsonSerializerOptions.NewLine`. 2. **`WriteIndented` ignored** — new `JsonSerializerOptions.GetWriterOptionsForJsonLines()` produces writer options with default (i.e. `Indented = false`), so each value occupies one line. 3. **Comprehensive JSONL deserialization tests** — `[Theory]` covering trailing/no-trailing LF, single value, empty doc, whitespace-only, line-terminator-only, lenient `\r\n`, lenient extra whitespace, plus heterogeneous JSON value kinds (null, bool, number, string, array, object). 4. **Both `Serialize-` and `DeserializeAsyncEnumerable` doc comments** explicitly cite https://jsonlines.org/. ## Implementation notes - `GetOrAddIAsyncEnumerableTypeInfoForSerialize` synthesizes `JsonTypeInfo>` from the supplied element `JsonTypeInfo` and caches it on the element. Both Stream and PipeWriter overloads use this for array-mode serialization so source-gen contexts that haven`'t registered `IAsyncEnumerable` continue to work — mirroring the existing `GetOrAddListTypeInfoForArrayMode` pattern used by deserialization. - Each JSONL item is serialized to a `Utf8JsonWriter`, followed by a single `\n` byte and a flush; the writer is reused via the parameterless `Reset()` between items. - The `PipeWriter` overload honors `FlushResult.IsCompleted` (early break) and `FlushResult.IsCanceled` (throws `OperationCanceledException`). - Writer disposal follows the simpler pattern used by sync `Serialize(Stream)` and the fast-path async `SerializeAsync(PipeWriter)` (plain `using`/`try-finally`); we don`'t catch+`Reset` because that defense is incomplete (it only narrows the leak window for in-`_memory` bytes; bytes already `Advance`d via `Grow()` cannot be rolled back through the `IBufferWriter` contract). ## Test coverage (462 net new + existing) Serialize: - JSONL output for objects, primitives, empty sequences, and the default JSON-array mode; round-trips via Stream and PipeWriter at counts 0, 1, 5, 100. - Canonical `\n` terminator regardless of `JsonSerializerOptions.NewLine` (`\n`, `\r\n`). - `JsonSerializerOptions.WriteIndented` ignored. - `JsonSerializerOptions.Encoder` propagated. - Null-arg validation for both Stream and PipeWriter overloads. - Strings containing raw newlines / control chars round-trip via JSON escaping. - `null` elements written as `null`. - Polymorphic items with `[JsonDerivedType]`. - Items larger than `DefaultBufferSize` (exercises `Utf8JsonWriter.Grow()`). - PipeWriter `FlushResult.IsCompleted` early-break path (custom `PipeWriter` for determinism). - Mid-iteration `CancellationToken` cancellation; pre-cancelled tokens; async-enumerator disposal on success and on cancellation. - Partial-item failure: prior items fully written; behavior documented for both Stream and PipeWriter. - `JsonTypeInfo` overload works with element-only metadata. - `JsonSerializerOptions` overload works with element-only metadata (regression for source-gen contexts). Deserialize (existing API, JSONL spec coverage): - All valid JSONL shapes per https://jsonlines.org/ (Theory with 9 inputs). - Heterogeneous JSON value types (null, bool, number, string, array, object). - Buffer-boundary stress (sizes 1, 8, 64, 4096). - Stream → PipeReader round-trip. Verified: - `dotnet test System.Text.Json.Tests` — **452** AsyncEnumerable tests pass on net11.0 and net481. - `dotnet test System.Text.Json.SourceGeneration.Roslyn4.4.Tests` — **135** AsyncEnumerable tests pass on net11.0 and net481. - Full library test suite passes with no regressions. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../System.Text.Json/ref/System.Text.Json.cs | 8 + .../Serialization/JsonSerializer.Helpers.cs | 20 + .../Serialization/JsonSerializer.Read.Pipe.cs | 12 +- .../JsonSerializer.Read.Stream.cs | 12 +- .../JsonSerializer.Write.Pipe.cs | 144 +++ .../JsonSerializer.Write.Stream.cs | 136 +++ .../Serialization/JsonSerializerOptions.cs | 15 + .../Metadata/JsonTypeInfoOfT.ReadHelper.cs | 3 +- .../tests/Common/AsyncEnumerableTests.cs | 1019 ++++++++++++++++- 9 files changed, 1359 insertions(+), 10 deletions(-) diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index 07091b502b1143..6560cfcdd796e6 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -356,6 +356,14 @@ public static void Serialize(System.Text.Json.Utf8JsonWriter writer, object? val [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] public static System.Threading.Tasks.Task SerializeAsync(System.IO.Pipelines.PipeWriter utf8Json, object? value, System.Type inputType, System.Text.Json.JsonSerializerOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Threading.Tasks.Task SerializeAsync(System.IO.Pipelines.PipeWriter utf8Json, object? value, System.Type inputType, System.Text.Json.Serialization.JsonSerializerContext context, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] + public static System.Threading.Tasks.Task SerializeAsyncEnumerable(System.IO.Stream utf8Json, System.Collections.Generic.IAsyncEnumerable value, bool topLevelValues = false, System.Text.Json.JsonSerializerOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task SerializeAsyncEnumerable(System.IO.Stream utf8Json, System.Collections.Generic.IAsyncEnumerable value, System.Text.Json.Serialization.Metadata.JsonTypeInfo jsonTypeInfo, bool topLevelValues = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] + public static System.Threading.Tasks.Task SerializeAsyncEnumerable(System.IO.Pipelines.PipeWriter utf8Json, System.Collections.Generic.IAsyncEnumerable value, bool topLevelValues = false, System.Text.Json.JsonSerializerOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task SerializeAsyncEnumerable(System.IO.Pipelines.PipeWriter utf8Json, System.Collections.Generic.IAsyncEnumerable value, System.Text.Json.Serialization.Metadata.JsonTypeInfo jsonTypeInfo, bool topLevelValues = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Text.Json.JsonDocument SerializeToDocument(object? value, System.Text.Json.Serialization.Metadata.JsonTypeInfo jsonTypeInfo) { throw null; } [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.")] [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs index 43a57196f9fb53..d16b1a3edfca1d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs @@ -178,5 +178,25 @@ static void ThrowUnableToCastValue(object? value) elementTypeInfo._asyncEnumerableArrayTypeInfo = listTypeInfo; return listTypeInfo; } + + private static JsonTypeInfo> GetOrAddIAsyncEnumerableTypeInfoForSerialize(JsonTypeInfo elementTypeInfo) + { + if (elementTypeInfo._asyncEnumerableRootLevelSerializer != null) + { + return (JsonTypeInfo>)elementTypeInfo._asyncEnumerableRootLevelSerializer; + } + + // Synthesize the IAsyncEnumerable type info from the element type info so we don't depend on the + // resolver (e.g. a source-generated context) having registered IAsyncEnumerable explicitly. + var converter = new IAsyncEnumerableOfTConverter, T>(); + var asyncEnumerableTypeInfo = new JsonTypeInfo>(converter, elementTypeInfo.Options) + { + ElementTypeInfo = elementTypeInfo, + }; + + asyncEnumerableTypeInfo.EnsureConfigured(); + elementTypeInfo._asyncEnumerableRootLevelSerializer = asyncEnumerableTypeInfo; + return asyncEnumerableTypeInfo; + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Pipe.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Pipe.cs index 4cfe7acb0b14ab..4f7bd70f3b1a1d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Pipe.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Pipe.cs @@ -240,7 +240,8 @@ public static partial class JsonSerializer /// An representation of the provided JSON sequence. /// JSON data to parse. /// Metadata about the element type to convert. - /// Whether to deserialize from a sequence of top-level JSON values. + /// to deserialize from a sequence of top-level JSON values + /// (for example, a JSON Lines (JSONL) document); to deserialize from a single top-level array. /// The that can be used to cancel the read operation. /// /// or is . @@ -248,8 +249,9 @@ public static partial class JsonSerializer /// /// When is set to , treats the PipeReader as a sequence of /// whitespace separated top-level JSON values and attempts to deserialize each value into . + /// This is a superset of the JSON Lines (JSONL) format and can be used to consume valid JSONL documents. /// When is set to , treats the PipeReader as a JSON array and - /// attempts to serialize each element into . + /// attempts to deserialize each element into . /// public static IAsyncEnumerable DeserializeAsyncEnumerable( PipeReader utf8Json, @@ -271,7 +273,8 @@ public static partial class JsonSerializer /// The element type to deserialize asynchronously. /// An representation of the provided JSON sequence. /// JSON data to parse. - /// to deserialize from a sequence of top-level JSON values, or to deserialize from a single top-level array. + /// to deserialize from a sequence of top-level JSON values + /// (for example, a JSON Lines (JSONL) document); to deserialize from a single top-level array. /// Options to control the behavior during reading. /// The that can be used to cancel the read operation. /// @@ -280,8 +283,9 @@ public static partial class JsonSerializer /// /// When is set to , treats the PipeReader as a sequence of /// whitespace separated top-level JSON values and attempts to deserialize each value into . + /// This is a superset of the JSON Lines (JSONL) format and can be used to consume valid JSONL documents. /// When is set to , treats the PipeReader as a JSON array and - /// attempts to serialize each element into . + /// attempts to deserialize each element into . /// [RequiresUnreferencedCode(SerializationUnreferencedCodeMessage)] [RequiresDynamicCode(SerializationRequiresDynamicCodeMessage)] diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs index 4ad3daa45d7cda..2e677f418eb3b1 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs @@ -374,7 +374,8 @@ public static partial class JsonSerializer /// The element type to deserialize asynchronously. /// An representation of the provided JSON sequence. /// JSON data to parse. - /// to deserialize from a sequence of top-level JSON values, or to deserialize from a single top-level array. + /// to deserialize from a sequence of top-level JSON values + /// (for example, a JSON Lines (JSONL) document); to deserialize from a single top-level array. /// Options to control the behavior during reading. /// The that can be used to cancel the read operation. /// @@ -383,8 +384,9 @@ public static partial class JsonSerializer /// /// When is set to , treats the stream as a sequence of /// whitespace separated top-level JSON values and attempts to deserialize each value into . + /// This is a superset of the JSON Lines (JSONL) format and can be used to consume valid JSONL documents. /// When is set to , treats the stream as a JSON array and - /// attempts to serialize each element into . + /// attempts to deserialize each element into . /// [RequiresUnreferencedCode(SerializationUnreferencedCodeMessage)] [RequiresDynamicCode(SerializationRequiresDynamicCodeMessage)] @@ -428,7 +430,8 @@ public static partial class JsonSerializer /// An representation of the provided JSON sequence. /// JSON data to parse. /// Metadata about the element type to convert. - /// Whether to deserialize from a sequence of top-level JSON values. + /// to deserialize from a sequence of top-level JSON values + /// (for example, a JSON Lines (JSONL) document); to deserialize from a single top-level array. /// The that can be used to cancel the read operation. /// /// or is . @@ -436,8 +439,9 @@ public static partial class JsonSerializer /// /// When is set to , treats the stream as a sequence of /// whitespace separated top-level JSON values and attempts to deserialize each value into . + /// This is a superset of the JSON Lines (JSONL) format and can be used to consume valid JSONL documents. /// When is set to , treats the stream as a JSON array and - /// attempts to serialize each element into . + /// attempts to deserialize each element into . /// public static IAsyncEnumerable DeserializeAsyncEnumerable( Stream utf8Json, diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Pipe.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Pipe.cs index dc3c4a00d04efd..c5e14c6aa39c7a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Pipe.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Pipe.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO.Pipelines; using System.Text.Json.Serialization; @@ -164,5 +166,147 @@ public static Task SerializeAsync( return jsonTypeInfo.SerializeAsObjectAsync(utf8Json, value, cancellationToken); } + + /// + /// Serializes each element of an to the + /// in a streaming manner. + /// + /// The type of elements in the sequence to serialize. + /// The to write to. + /// The sequence to serialize. + /// to serialize the elements as a sequence of newline-separated top-level JSON values + /// (a JSON Lines (JSONL) document); to serialize them as a single root-level JSON array. + /// Options to control the serialization behavior. + /// The that can be used to cancel the write operation. + /// A task that represents the asynchronous write operation. + /// + /// or is . + /// + /// + /// When is , the output conforms to the + /// JSON Lines (JSONL) specification: each element is serialized as a + /// single-line JSON value followed by a line-feed (\n) terminator. The line terminator is always \n + /// regardless of , and + /// is ignored so that each value is emitted on a single line. + /// + [RequiresUnreferencedCode(SerializationUnreferencedCodeMessage)] + [RequiresDynamicCode(SerializationRequiresDynamicCodeMessage)] + public static Task SerializeAsyncEnumerable( + PipeWriter utf8Json, + IAsyncEnumerable value, + bool topLevelValues = false, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(utf8Json); + ArgumentNullException.ThrowIfNull(value); + + JsonTypeInfo jsonTypeInfo = GetTypeInfo(options); + if (topLevelValues) + { + return SerializeAsyncEnumerableAsJsonLines(utf8Json, value, jsonTypeInfo, cancellationToken); + } + + JsonTypeInfo> collectionTypeInfo = GetOrAddIAsyncEnumerableTypeInfoForSerialize(jsonTypeInfo); + return collectionTypeInfo.SerializeAsync(utf8Json, value, cancellationToken); + } + + /// + /// Serializes each element of an to the + /// in a streaming manner. + /// + /// The type of elements in the sequence to serialize. + /// The to write to. + /// The sequence to serialize. + /// Metadata about the type of elements to serialize. + /// to serialize the elements as a sequence of newline-separated top-level JSON values + /// (a JSON Lines (JSONL) document); to serialize them as a single root-level JSON array. + /// The that can be used to cancel the write operation. + /// A task that represents the asynchronous write operation. + /// + /// , , or is . + /// + /// + /// When is , the output conforms to the + /// JSON Lines (JSONL) specification: each element is serialized as a + /// single-line JSON value followed by a line-feed (\n) terminator. The line terminator is always \n + /// regardless of , and + /// is ignored so that each value is emitted on a single line. + /// + public static Task SerializeAsyncEnumerable( + PipeWriter utf8Json, + IAsyncEnumerable value, + JsonTypeInfo jsonTypeInfo, + bool topLevelValues = false, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(utf8Json); + ArgumentNullException.ThrowIfNull(value); + ArgumentNullException.ThrowIfNull(jsonTypeInfo); + + jsonTypeInfo.EnsureConfigured(); + if (topLevelValues) + { + return SerializeAsyncEnumerableAsJsonLines(utf8Json, value, jsonTypeInfo, cancellationToken); + } + + JsonTypeInfo> collectionTypeInfo = GetOrAddIAsyncEnumerableTypeInfoForSerialize(jsonTypeInfo); + return collectionTypeInfo.SerializeAsync(utf8Json, value, cancellationToken); + } + + private static async Task SerializeAsyncEnumerableAsJsonLines( + PipeWriter utf8Json, + IAsyncEnumerable value, + JsonTypeInfo jsonTypeInfo, + CancellationToken cancellationToken) + { + Debug.Assert(jsonTypeInfo.IsConfigured); + + JsonWriterOptions writerOptions = jsonTypeInfo.Options.GetWriterOptionsForJsonLines(); + var writer = new Utf8JsonWriter(utf8Json, writerOptions); + + try + { + bool first = true; + await foreach (TValue item in value.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + if (!first) + { + writer.Reset(); + } + + first = false; + jsonTypeInfo.Serialize(writer, item); + + // The JSON Lines spec mandates a single line-feed character as the line separator, + // independently of any platform-specific or user-configured newline preference. + Span dest = utf8Json.GetSpan(1); + dest[0] = (byte)'\n'; + utf8Json.Advance(1); + + FlushResult result = await utf8Json.FlushAsync(cancellationToken).ConfigureAwait(false); + + if (result.IsCanceled) + { + ThrowHelper.ThrowOperationCanceledException_PipeWriteCanceled(); + } + + if (result.IsCompleted) + { + break; + } + } + } + catch + { + // Reset the writer in exception cases so writer.Dispose() doesn't flush a partially-written value to the pipe. + writer.Reset(); + throw; + } + finally + { + writer.Dispose(); + } + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs index efa9bb0f357e5f..d10d09a821c022 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Stream.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text.Json.Serialization; @@ -306,5 +308,139 @@ public static void Serialize( JsonTypeInfo jsonTypeInfo = GetTypeInfo(context, inputType); jsonTypeInfo.SerializeAsObject(utf8Json, value); } + + /// + /// Serializes each element of an to the + /// in a streaming manner. + /// + /// The type of elements in the sequence to serialize. + /// The UTF-8 to write to. + /// The sequence to serialize. + /// to serialize the elements as a sequence of newline-separated top-level JSON values + /// (a JSON Lines (JSONL) document); to serialize them as a single root-level JSON array. + /// Options to control the serialization behavior. + /// The that can be used to cancel the write operation. + /// A task that represents the asynchronous write operation. + /// + /// or is . + /// + /// + /// When is , the output conforms to the + /// JSON Lines (JSONL) specification: each element is serialized as a + /// single-line JSON value followed by a line-feed (\n) terminator. The line terminator is always \n + /// regardless of , and + /// is ignored so that each value is emitted on a single line. + /// + [RequiresUnreferencedCode(SerializationUnreferencedCodeMessage)] + [RequiresDynamicCode(SerializationRequiresDynamicCodeMessage)] + public static Task SerializeAsyncEnumerable( + Stream utf8Json, + IAsyncEnumerable value, + bool topLevelValues = false, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(utf8Json); + ArgumentNullException.ThrowIfNull(value); + + JsonTypeInfo jsonTypeInfo = GetTypeInfo(options); + if (topLevelValues) + { + return SerializeAsyncEnumerableAsJsonLines(utf8Json, value, jsonTypeInfo, cancellationToken); + } + + JsonTypeInfo> collectionTypeInfo = GetOrAddIAsyncEnumerableTypeInfoForSerialize(jsonTypeInfo); + return collectionTypeInfo.SerializeAsync(utf8Json, value, cancellationToken); + } + + /// + /// Serializes each element of an to the + /// in a streaming manner. + /// + /// The type of elements in the sequence to serialize. + /// The UTF-8 to write to. + /// The sequence to serialize. + /// Metadata about the type of elements to serialize. + /// to serialize the elements as a sequence of newline-separated top-level JSON values + /// (a JSON Lines (JSONL) document); to serialize them as a single root-level JSON array. + /// The that can be used to cancel the write operation. + /// A task that represents the asynchronous write operation. + /// + /// , , or is . + /// + /// + /// When is , the output conforms to the + /// JSON Lines (JSONL) specification: each element is serialized as a + /// single-line JSON value followed by a line-feed (\n) terminator. The line terminator is always \n + /// regardless of , and + /// is ignored so that each value is emitted on a single line. + /// + public static Task SerializeAsyncEnumerable( + Stream utf8Json, + IAsyncEnumerable value, + JsonTypeInfo jsonTypeInfo, + bool topLevelValues = false, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(utf8Json); + ArgumentNullException.ThrowIfNull(value); + ArgumentNullException.ThrowIfNull(jsonTypeInfo); + + jsonTypeInfo.EnsureConfigured(); + if (topLevelValues) + { + return SerializeAsyncEnumerableAsJsonLines(utf8Json, value, jsonTypeInfo, cancellationToken); + } + + JsonTypeInfo> collectionTypeInfo = GetOrAddIAsyncEnumerableTypeInfoForSerialize(jsonTypeInfo); + return collectionTypeInfo.SerializeAsync(utf8Json, value, cancellationToken); + } + + private static async Task SerializeAsyncEnumerableAsJsonLines( + Stream utf8Json, + IAsyncEnumerable value, + JsonTypeInfo jsonTypeInfo, + CancellationToken cancellationToken) + { + Debug.Assert(jsonTypeInfo.IsConfigured); + + JsonWriterOptions writerOptions = jsonTypeInfo.Options.GetWriterOptionsForJsonLines(); + + var bufferWriter = new PooledByteBufferWriter(jsonTypeInfo.Options.DefaultBufferSize, utf8Json); + var writer = new Utf8JsonWriter(bufferWriter, writerOptions); + + try + { + bool first = true; + await foreach (TValue item in value.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + if (!first) + { + writer.Reset(); + } + + first = false; + jsonTypeInfo.Serialize(writer, item); + + // The JSON Lines spec mandates a single line-feed character as the line separator, + // independently of any platform-specific or user-configured newline preference. + Span dest = bufferWriter.GetSpan(1); + dest[0] = (byte)'\n'; + bufferWriter.Advance(1); + await bufferWriter.FlushAsync(cancellationToken).ConfigureAwait(false); + } + } + catch + { + // Reset the writer in exception cases so writer.Dispose() doesn't flush a partially-written value. + writer.Reset(); + throw; + } + finally + { + writer.Dispose(); + bufferWriter.Dispose(); + } + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs index 9af7bd341bbc19..f7acaa6e54c876 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs @@ -1100,6 +1100,21 @@ internal JsonWriterOptions GetWriterOptions() }; } + // Per JSON Lines spec (https://jsonlines.org/) every value must occupy a single line. + // Indentation must be suppressed regardless of the user-configured WriteIndented setting, + // and the JsonWriterOptions.NewLine setting is irrelevant when Indented is false. + internal JsonWriterOptions GetWriterOptionsForJsonLines() + { + return new JsonWriterOptions + { + Encoder = Encoder, + MaxDepth = EffectiveMaxDepth, +#if !DEBUG + SkipValidation = true +#endif + }; + } + internal void VerifyMutable() { if (_isReadOnly) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.ReadHelper.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.ReadHelper.cs index b8e99b30bf9ae0..68e82560e08600 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.ReadHelper.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.ReadHelper.cs @@ -102,12 +102,13 @@ public partial class JsonTypeInfo } /// - /// Caches JsonTypeInfo<List<T>> instances used by the DeserializeAsyncEnumerable method. + /// Caches JsonTypeInfo instances used by the Serialize/DeserializeAsyncEnumerable methods. /// Store as a non-generic type to avoid triggering generic recursion in the AOT compiler. /// cf. https://github.com/dotnet/runtime/issues/85184 /// internal JsonTypeInfo? _asyncEnumerableArrayTypeInfo; internal JsonTypeInfo? _asyncEnumerableRootLevelValueTypeInfo; + internal JsonTypeInfo? _asyncEnumerableRootLevelSerializer; internal sealed override object? DeserializeAsObject(ref Utf8JsonReader reader, ref ReadStack state) => Deserialize(ref reader, ref state); diff --git a/src/libraries/System.Text.Json/tests/Common/AsyncEnumerableTests.cs b/src/libraries/System.Text.Json/tests/Common/AsyncEnumerableTests.cs index a676d07984a7de..3dff7d630054fa 100644 --- a/src/libraries/System.Text.Json/tests/Common/AsyncEnumerableTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/AsyncEnumerableTests.cs @@ -1,10 +1,11 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; +using System.IO.Pipelines; using System.Linq; using System.Text.Json.Serialization.Metadata; using System.Threading; @@ -513,5 +514,1021 @@ public ValueTask DisposeAsync() public void Dispose() => _cts.Dispose(); } + + private sealed class ThrowingValue + { + private readonly int _v; + private readonly bool _throwOnSerialize; + + public ThrowingValue() { } + + public ThrowingValue(int value, bool throwOnSerialize = false) + { + _v = value; + _throwOnSerialize = throwOnSerialize; + } + + public int V => _throwOnSerialize + ? throw new InvalidOperationException("Simulated serialization failure.") + : _v; + } + + private sealed class DisposableAsyncEnumerable : IAsyncEnumerable + { + public bool Disposed { get; private set; } + + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + return new Enumerator(this, cancellationToken); + } + + private sealed class Enumerator : IAsyncEnumerator + { + private readonly DisposableAsyncEnumerable _parent; + private readonly CancellationToken _ct; + private int _index; + + public Enumerator(DisposableAsyncEnumerable parent, CancellationToken ct) + { + _parent = parent; + _ct = ct; + } + + public int Current { get; private set; } + + public async ValueTask MoveNextAsync() + { + _ct.ThrowIfCancellationRequested(); + await Task.Yield(); + if (_index < 3) + { + Current = _index++; + return true; + } + return false; + } + + public ValueTask DisposeAsync() + { + _parent.Disposed = true; + return default; + } + } + } + + private sealed class EmptyResolver : IJsonTypeInfoResolver + { + public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) => null; + } + + private sealed class ElementOnlyInt32Resolver : IJsonTypeInfoResolver + { + public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) + { + // Simulates a source-gen context that only knows the element type. + if (type == typeof(int)) + { + return JsonMetadataServices.CreateValueInfo(options, JsonMetadataServices.Int32Converter); + } + + return null; + } + } + + // ----------------------------- + // SerializeAsyncEnumerable tests + // ----------------------------- + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(5)] + [InlineData(100)] + public async Task SerializeAsyncEnumerable_TopLevelValues_ProducesJsonLines(int count) + { + using MemoryStream stream = new(); + + await JsonSerializer.SerializeAsyncEnumerable( + stream, + GenerateItems(count), + ResolveJsonTypeInfo(), + topLevelValues: true); + + byte[] bytes = stream.ToArray(); + if (count == 0) + { + Assert.Empty(bytes); + return; + } + + // Per JSONL spec each value is followed by a single \n, including the final one. + Assert.Equal((byte)'\n', bytes[bytes.Length - 1]); + // No \r should appear anywhere (the canonical JSONL terminator is \n only). + Assert.DoesNotContain((byte)'\r', bytes); + + string result = Encoding.UTF8.GetString(bytes); + string[] lines = result.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(count, lines.Length); + + for (int i = 0; i < count; i++) + { + SimpleTestClass deserialized = JsonSerializer.Deserialize(lines[i]); + Assert.Equal(i, deserialized.MyInt32); + } + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(5)] + [InlineData(100)] + public async Task SerializeAsyncEnumerable_PipeWriter_TopLevelValues_ProducesJsonLines(int count) + { + Pipe pipe = new(); + + Task writeTask = JsonSerializer.SerializeAsyncEnumerable( + pipe.Writer, + GenerateItems(count), + ResolveJsonTypeInfo(), + topLevelValues: true); + + byte[] bytes = await ReadAllAsync(pipe, writeTask); + if (count == 0) + { + Assert.Empty(bytes); + return; + } + + Assert.Equal((byte)'\n', bytes[bytes.Length - 1]); + Assert.DoesNotContain((byte)'\r', bytes); + + string result = Encoding.UTF8.GetString(bytes); + string[] lines = result.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(count, lines.Length); + + for (int i = 0; i < count; i++) + { + SimpleTestClass deserialized = JsonSerializer.Deserialize(lines[i]); + Assert.Equal(i, deserialized.MyInt32); + } + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(5)] + [InlineData(100)] + public async Task SerializeAsyncEnumerable_DefaultMode_ProducesJsonArray(int count) + { + using MemoryStream stream = new(); + + await JsonSerializer.SerializeAsyncEnumerable( + stream, + GenerateItems(count), + ResolveJsonTypeInfo()); + + stream.Position = 0; + SimpleTestClass[] roundTripped = JsonSerializer.Deserialize(stream); + Assert.Equal(count, roundTripped.Length); + for (int i = 0; i < count; i++) + { + Assert.Equal(i, roundTripped[i].MyInt32); + } + } + + [Fact] + public async Task SerializeAsyncEnumerable_TopLevelValues_RoundTripsWithDeserializeAsyncEnumerable() + { + const int count = 50; + using MemoryStream stream = new(); + + await JsonSerializer.SerializeAsyncEnumerable( + stream, + GenerateItems(count), + ResolveJsonTypeInfo(), + topLevelValues: true); + + stream.Position = 0; + + int i = 0; + await foreach (SimpleTestClass item in JsonSerializer.DeserializeAsyncEnumerable(stream, topLevelValues: true)) + { + Assert.Equal(i++, item.MyInt32); + } + + Assert.Equal(count, i); + } + + [Fact] + public async Task SerializeAsyncEnumerable_PipeWriter_TopLevelValues_RoundTripsWithDeserializeAsyncEnumerable() + { + const int count = 50; + Pipe pipe = new(); + + Task writeTask = JsonSerializer.SerializeAsyncEnumerable( + pipe.Writer, + GenerateItems(count), + ResolveJsonTypeInfo(), + topLevelValues: true); + + byte[] bytes = await ReadAllAsync(pipe, writeTask); + using MemoryStream stream = new(bytes); + + int i = 0; + await foreach (SimpleTestClass item in JsonSerializer.DeserializeAsyncEnumerable(stream, topLevelValues: true)) + { + Assert.Equal(i++, item.MyInt32); + } + + Assert.Equal(count, i); + } + + [Fact] + public async Task SerializeAsyncEnumerable_TopLevelValues_PrimitiveValues_ProducesJsonLines() + { + using MemoryStream stream = new(); + + await JsonSerializer.SerializeAsyncEnumerable( + stream, + GenerateInts(), + ResolveJsonTypeInfo(), + topLevelValues: true); + + byte[] bytes = stream.ToArray(); + // JSONL line terminator is canonical \n irrespective of platform. + Assert.Equal("1\n2\n3\n", Encoding.UTF8.GetString(bytes)); + + static async IAsyncEnumerable GenerateInts() + { + yield return 1; + yield return 2; + yield return 3; + await Task.CompletedTask; + } + } + + [Theory] + [InlineData("\n")] + [InlineData("\r\n")] + public async Task SerializeAsyncEnumerable_TopLevelValues_LineTerminatorIsAlwaysLineFeed(string optionsNewLine) + { + // Per API review feedback (#126395): the JSONL line terminator is canonical \n + // regardless of JsonSerializerOptions.NewLine. + JsonSerializerOptions options = new() { NewLine = optionsNewLine }; + + using MemoryStream stream = new(); + await JsonSerializer.SerializeAsyncEnumerable( + stream, + GenerateItems(3), + ResolveJsonTypeInfo(options), + topLevelValues: true); + + byte[] bytes = stream.ToArray(); + Assert.DoesNotContain((byte)'\r', bytes); + } + + [Fact] + public async Task SerializeAsyncEnumerable_TopLevelValues_IgnoresWriteIndented() + { + // Per API review feedback (#126395): WriteIndented is ignored in JSONL mode so + // that each value fits on a single line. + JsonSerializerOptions options = new() { WriteIndented = true }; + + using MemoryStream stream = new(); + await JsonSerializer.SerializeAsyncEnumerable( + stream, + GenerateItems(5), + ResolveJsonTypeInfo(options), + topLevelValues: true); + + byte[] bytes = stream.ToArray(); + Assert.Equal((byte)'\n', bytes[bytes.Length - 1]); + + // Each non-trailing newline marks the boundary between top-level values; the + // payload between them must be a single self-contained JSON value. + string result = Encoding.UTF8.GetString(bytes); + string[] lines = result.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(5, lines.Length); + + foreach (string line in lines) + { + // Each entry is parseable as a single JSON value (no embedded raw newlines, no indentation breaking it apart). + SimpleTestClass parsed = JsonSerializer.Deserialize(line); + Assert.NotNull(parsed); + Assert.DoesNotContain('\n', line); + Assert.DoesNotContain('\r', line); + } + } + + [Fact] + public async Task SerializeAsyncEnumerable_TopLevelValues_EmptySequence_ProducesEmptyOutput() + { + using MemoryStream stream = new(); + + await JsonSerializer.SerializeAsyncEnumerable( + stream, + EmptyAsyncEnumerable(), + ResolveJsonTypeInfo(), + topLevelValues: true); + + Assert.Equal(0, stream.Length); + + static async IAsyncEnumerable EmptyAsyncEnumerable() + { + await Task.CompletedTask; + yield break; + } + } + + [Fact] + public async Task SerializeAsyncEnumerable_TopLevelValues_PartialItemFailure_PriorItemsAreFullyWritten() + { + // When element serialization throws mid-item, items written prior to the failure + // are fully visible to the Stream consumer. Bytes for the failing item are not + // flushed to the stream because the JSONL writer only flushes after each fully + // serialized item. + + using MemoryStream stream = new(); + + await Assert.ThrowsAsync(() => + JsonSerializer.SerializeAsyncEnumerable( + stream, + Items(), + ResolveJsonTypeInfo(), + topLevelValues: true)); + + string actual = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal("{\"V\":1}\n{\"V\":2}\n", actual); + + static async IAsyncEnumerable Items() + { + yield return new ThrowingValue(1); + yield return new ThrowingValue(2); + yield return new ThrowingValue(3, throwOnSerialize: true); + await Task.CompletedTask; + } + } + + [Fact] + public async Task SerializeAsyncEnumerable_PipeWriter_TopLevelValues_PartialItemFailure_PriorItemsAreFullyWritten() + { + // When element serialization throws mid-item, items written prior to the failure + // are fully visible to the PipeWriter consumer. Bytes for the failing item itself + // may or may not be visible depending on internal buffering — we make no guarantee + // either way for the partial item. + + Pipe pipe = new(); + + Task writeTask = JsonSerializer.SerializeAsyncEnumerable( + pipe.Writer, + Items(), + ResolveJsonTypeInfo(), + topLevelValues: true); + + Task readerTask = Task.Run(async () => + { + try + { + using MemoryStream output = new(); + while (true) + { + ReadResult result = await pipe.Reader.ReadAsync(); + foreach (ReadOnlyMemory segment in result.Buffer) + { + byte[] tmp = segment.ToArray(); + output.Write(tmp, 0, tmp.Length); + } + + pipe.Reader.AdvanceTo(result.Buffer.End); + if (result.IsCompleted) + { + break; + } + } + + return output.ToArray(); + } + finally + { + await pipe.Reader.CompleteAsync(); + } + }); + + await Assert.ThrowsAsync(() => writeTask); + await pipe.Writer.CompleteAsync(); + byte[] bytes = await readerTask; + + string actual = Encoding.UTF8.GetString(bytes); + // The first two items are guaranteed to be fully written; we don't make guarantees + // about whether bytes for the failing third item are observable. + const string ExpectedPrefix = "{\"V\":1}\n{\"V\":2}\n"; + Assert.StartsWith(ExpectedPrefix, actual); + + // Whatever follows must not be a self-contained third value; the third item failed. + string trailing = actual.Substring(ExpectedPrefix.Length).TrimEnd('\n'); + Assert.DoesNotContain("\"V\":3", trailing); + + static async IAsyncEnumerable Items() + { + yield return new ThrowingValue(1); + yield return new ThrowingValue(2); + yield return new ThrowingValue(3, throwOnSerialize: true); + await Task.CompletedTask; + } + } + + [Fact] + public async Task SerializeAsyncEnumerable_TopLevelValues_DisposesAsyncEnumerator() + { + DisposableAsyncEnumerable enumerable = new(); + using MemoryStream stream = new(); + + await JsonSerializer.SerializeAsyncEnumerable( + stream, + enumerable, + ResolveJsonTypeInfo(), + topLevelValues: true); + + Assert.True(enumerable.Disposed); + } + + [Fact] + public async Task SerializeAsyncEnumerable_TopLevelValues_CancellationToken_DisposesAsyncEnumerator() + { + DisposableAsyncEnumerable enumerable = new(); + using MemoryStream stream = new(); + using CancellationTokenSource cts = new(); + cts.Cancel(); + + await Assert.ThrowsAnyAsync(() => + JsonSerializer.SerializeAsyncEnumerable( + stream, + enumerable, + ResolveJsonTypeInfo(), + topLevelValues: true, + cts.Token)); + + Assert.True(enumerable.Disposed); + } + + [Fact] + public async Task SerializeAsyncEnumerable_JsonTypeInfo_ArrayMode_WorksWithoutAsyncEnumerableMetadata() + { + // The JsonTypeInfo overload should not require the resolver to know + // IAsyncEnumerable; the wrapper type info is synthesized from the supplied element metadata. + JsonSerializerOptions options = new() { TypeInfoResolver = new EmptyResolver() }; + + JsonTypeInfo elementInfo = JsonMetadataServices.CreateValueInfo(options, JsonMetadataServices.Int32Converter); + options.MakeReadOnly(); + + using MemoryStream stream = new(); + await JsonSerializer.SerializeAsyncEnumerable(stream, GenerateInts(), elementInfo); + + string actual = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal("[1,2,3]", actual); + + static async IAsyncEnumerable GenerateInts() + { + yield return 1; + yield return 2; + yield return 3; + await Task.CompletedTask; + } + } + + [Fact] + public async Task SerializeAsyncEnumerable_Options_ArrayMode_WorksWithoutAsyncEnumerableMetadata() + { + // The JsonSerializerOptions-based overload must also synthesize IAsyncEnumerable + // metadata from the element type info: a source-gen resolver may register only the element type. + JsonSerializerOptions options = new() { TypeInfoResolver = new ElementOnlyInt32Resolver() }; + + using MemoryStream stream = new(); + await JsonSerializer.SerializeAsyncEnumerable(stream, GenerateInts(), options: options); + + string actual = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal("[1,2,3]", actual); + + static async IAsyncEnumerable GenerateInts() + { + yield return 1; + yield return 2; + yield return 3; + await Task.CompletedTask; + } + } + + [Fact] + public async Task SerializeAsyncEnumerable_Options_TopLevelValues_WorksWithoutAsyncEnumerableMetadata() + { + JsonSerializerOptions options = new() { TypeInfoResolver = new ElementOnlyInt32Resolver() }; + + using MemoryStream stream = new(); + await JsonSerializer.SerializeAsyncEnumerable(stream, GenerateInts(), topLevelValues: true, options); + + string actual = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal("1\n2\n3\n", actual); + + static async IAsyncEnumerable GenerateInts() + { + yield return 1; + yield return 2; + yield return 3; + await Task.CompletedTask; + } + } + + [Fact] + public async Task SerializeAsyncEnumerable_TopLevelValues_PropagatesCustomEncoder() + { + // JsonSerializerOptions.Encoder must propagate to the JSONL writer. UnsafeRelaxedJsonEscaping + // leaves '+' unescaped; the default encoder escapes it as \u002B. + JsonSerializerOptions strict = new(); + JsonSerializerOptions relaxed = new() { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; + + using MemoryStream strictStream = new(); + using MemoryStream relaxedStream = new(); + + await JsonSerializer.SerializeAsyncEnumerable(strictStream, Strings(), ResolveJsonTypeInfo(strict), topLevelValues: true); + await JsonSerializer.SerializeAsyncEnumerable(relaxedStream, Strings(), ResolveJsonTypeInfo(relaxed), topLevelValues: true); + + Assert.Equal("\"a\\u002Bb\"\n", Encoding.UTF8.GetString(strictStream.ToArray())); + Assert.Equal("\"a+b\"\n", Encoding.UTF8.GetString(relaxedStream.ToArray())); + + static async IAsyncEnumerable Strings() + { + yield return "a+b"; + await Task.CompletedTask; + } + } + + [Fact] + public async Task SerializeAsyncEnumerable_TopLevelValues_StringsContainingNewlinesAreEscaped() + { + // Round-trip: strings containing raw newline characters must be JSON-escaped (\n inside the + // quoted string) so they don't break JSONL line parsing. + using MemoryStream stream = new(); + await JsonSerializer.SerializeAsyncEnumerable( + stream, + Strings(), + ResolveJsonTypeInfo(), + topLevelValues: true); + + byte[] bytes = stream.ToArray(); + // Each item is followed by exactly one literal '\n' (the JSONL terminator). Embedded \n + // characters in the strings must appear escaped (as the two-char sequence "\n"). + int rawNewlines = bytes.Count(b => b == (byte)'\n'); + Assert.Equal(3, rawNewlines); + + stream.Position = 0; + int i = 0; + string[] expected = ["line1\nline2", "tab\there", "carriage\rreturn"]; + await foreach (string? entry in JsonSerializer.DeserializeAsyncEnumerable(stream, topLevelValues: true)) + { + Assert.Equal(expected[i++], entry); + } + Assert.Equal(3, i); + + static async IAsyncEnumerable Strings() + { + yield return "line1\nline2"; + yield return "tab\there"; + yield return "carriage\rreturn"; + await Task.CompletedTask; + } + } + + [Fact] + public async Task SerializeAsyncEnumerable_TopLevelValues_NullElementsAreWrittenAsNull() + { + using MemoryStream stream = new(); + await JsonSerializer.SerializeAsyncEnumerable( + stream, + Items(), + ResolveJsonTypeInfo(), + topLevelValues: true); + + Assert.Equal("\"a\"\nnull\n\"b\"\n", Encoding.UTF8.GetString(stream.ToArray())); + + stream.Position = 0; + List roundTripped = new(); + await foreach (string? item in JsonSerializer.DeserializeAsyncEnumerable(stream, topLevelValues: true)) + { + roundTripped.Add(item); + } + Assert.Equal(["a", null, "b"], roundTripped); + + static async IAsyncEnumerable Items() + { + yield return "a"; + yield return null; + yield return "b"; + await Task.CompletedTask; + } + } + + [Fact] + public async Task SerializeAsyncEnumerable_TopLevelValues_HandlesItemsLargerThanDefaultBufferSize() + { + // The Utf8JsonWriter calls _output.Advance + GetMemory mid-write (Grow) when an item is + // larger than its current span. Verify large items round-trip cleanly. + const int LargeStringLength = 16 * 1024; + string payload = new('x', LargeStringLength); + + JsonSerializerOptions options = new() { DefaultBufferSize = 64 }; + + using MemoryStream stream = new(); + await JsonSerializer.SerializeAsyncEnumerable( + stream, + Items(), + ResolveJsonTypeInfo(options), + topLevelValues: true); + + stream.Position = 0; + int i = 0; + await foreach (string? item in JsonSerializer.DeserializeAsyncEnumerable(stream, topLevelValues: true, options)) + { + Assert.Equal(payload, item); + i++; + } + Assert.Equal(3, i); + + async IAsyncEnumerable Items() + { + yield return payload; + yield return payload; + yield return payload; + await Task.CompletedTask; + } + } + + [Fact] + public async Task SerializeAsyncEnumerable_TopLevelValues_PolymorphicItems() + { + // Polymorphic root-level values: each derived type must serialize with its discriminator + // and round-trip back to the correct derived instance. + using MemoryStream stream = new(); + await JsonSerializer.SerializeAsyncEnumerable( + stream, + Items(), + ResolveJsonTypeInfo(), + topLevelValues: true); + + stream.Position = 0; + List roundTripped = new(); + await foreach (PolymorphicBase? item in JsonSerializer.DeserializeAsyncEnumerable(stream, topLevelValues: true)) + { + roundTripped.Add(item); + } + + Assert.Equal(2, roundTripped.Count); + Assert.IsType(roundTripped[0]); + Assert.Equal(7, ((PolymorphicDerivedA)roundTripped[0]!).A); + Assert.IsType(roundTripped[1]); + Assert.Equal("hi", ((PolymorphicDerivedB)roundTripped[1]!).B); + + static async IAsyncEnumerable Items() + { + yield return new PolymorphicDerivedA { A = 7 }; + yield return new PolymorphicDerivedB { B = "hi" }; + await Task.CompletedTask; + } + } + + [Fact] + public async Task SerializeAsyncEnumerable_PipeWriter_TopLevelValues_StopsWhenReaderCompletes() + { + // Verify that when FlushAsync returns IsCompleted=true (the reader has completed), the + // writer breaks out of the loop and does not consume the rest of the IAsyncEnumerable. + int yielded = 0; + CompletingPipeWriter pipeWriter = new(completeAfterFlushes: 2); + + await JsonSerializer.SerializeAsyncEnumerable( + pipeWriter, + Items(), + ResolveJsonTypeInfo(), + topLevelValues: true); + + // Stops after producing the items that triggered the first two flushes; the rest of the + // 100-item enumerable is never consumed. + Assert.InRange(yielded, 1, 10); + Assert.Equal(2, pipeWriter.FlushCount); + + async IAsyncEnumerable Items() + { + for (int i = 0; i < 100; i++) + { + yielded++; + yield return i; + await Task.Yield(); + } + } + } + + private sealed class CompletingPipeWriter : PipeWriter + { + private readonly int _completeAfterFlushes; + private byte[] _buffer = new byte[1024]; + private int _written; + + public CompletingPipeWriter(int completeAfterFlushes) + { + _completeAfterFlushes = completeAfterFlushes; + } + + public int FlushCount { get; private set; } + public override bool CanGetUnflushedBytes => true; + public override long UnflushedBytes => _written; + + public override void Advance(int bytes) => _written += bytes; + + public override Memory GetMemory(int sizeHint = 0) + { + if (sizeHint <= 0) sizeHint = 256; + if (_written + sizeHint > _buffer.Length) + { + Array.Resize(ref _buffer, Math.Max(_buffer.Length * 2, _written + sizeHint)); + } + return _buffer.AsMemory(_written); + } + + public override Span GetSpan(int sizeHint = 0) => GetMemory(sizeHint).Span; + + public override void CancelPendingFlush() { } + public override void Complete(Exception? exception = null) { } + + public override ValueTask FlushAsync(CancellationToken cancellationToken = default) + { + FlushCount++; + bool isCompleted = FlushCount >= _completeAfterFlushes; + return new ValueTask(new FlushResult(isCanceled: false, isCompleted: isCompleted)); + } + } + + [Fact] + public async Task SerializeAsyncEnumerable_TopLevelValues_PipeReader_RoundTrip() + { + // Round-trip serialize-via-Stream → deserialize-via-PipeReader. + using MemoryStream stream = new(); + await JsonSerializer.SerializeAsyncEnumerable( + stream, + GenerateItems(20), + ResolveJsonTypeInfo(), + topLevelValues: true); + + stream.Position = 0; + PipeReader reader = PipeReader.Create(stream); + + try + { + int i = 0; + await foreach (SimpleTestClass? item in JsonSerializer.DeserializeAsyncEnumerable(reader, topLevelValues: true)) + { + Assert.NotNull(item); + Assert.Equal(i++, item.MyInt32); + } + Assert.Equal(20, i); + } + finally + { + await reader.CompleteAsync(); + } + } + + [Fact] + public async Task SerializeAsyncEnumerable_TopLevelValues_CancellationMidIteration() + { + // Cancel after a few items have been emitted; verify the remaining items are not consumed + // and an OperationCanceledException is raised. + using CancellationTokenSource cts = new(); + using MemoryStream stream = new(); + int yielded = 0; + + await Assert.ThrowsAnyAsync(() => + JsonSerializer.SerializeAsyncEnumerable( + stream, + Items(), + ResolveJsonTypeInfo(), + topLevelValues: true, + cts.Token)); + + // We expected to produce a few items before cancellation took effect. + Assert.InRange(yielded, 1, 10); + + async IAsyncEnumerable Items() + { + for (int i = 0; i < 1000; i++) + { + if (i == 3) + { + cts.Cancel(); + } + yielded++; + yield return i; + await Task.Yield(); + } + } + } + + [JsonPolymorphic(TypeDiscriminatorPropertyName = "$kind")] + [JsonDerivedType(typeof(PolymorphicDerivedA), typeDiscriminator: "a")] + [JsonDerivedType(typeof(PolymorphicDerivedB), typeDiscriminator: "b")] + public abstract class PolymorphicBase { } + + public sealed class PolymorphicDerivedA : PolymorphicBase + { + public int A { get; set; } + } + + public sealed class PolymorphicDerivedB : PolymorphicBase + { + public string? B { get; set; } + } + + [Fact] + public async Task SerializeAsyncEnumerable_NullArguments_ThrowsArgumentNullException() + { + JsonTypeInfo typeInfo = ResolveJsonTypeInfo(); + IAsyncEnumerable source = AsyncEnumerable(); + + await AssertExtensions.ThrowsAsync("utf8Json", () => + JsonSerializer.SerializeAsyncEnumerable(utf8Json: (Stream)null!, source)); + await AssertExtensions.ThrowsAsync("value", () => + JsonSerializer.SerializeAsyncEnumerable(new MemoryStream(), value: null!)); + await AssertExtensions.ThrowsAsync("utf8Json", () => + JsonSerializer.SerializeAsyncEnumerable(utf8Json: (Stream)null!, source, typeInfo)); + await AssertExtensions.ThrowsAsync("value", () => + JsonSerializer.SerializeAsyncEnumerable(new MemoryStream(), value: null!, typeInfo)); + await AssertExtensions.ThrowsAsync("jsonTypeInfo", () => + JsonSerializer.SerializeAsyncEnumerable(new MemoryStream(), source, jsonTypeInfo: null!)); + + await AssertExtensions.ThrowsAsync("utf8Json", () => + JsonSerializer.SerializeAsyncEnumerable(utf8Json: (PipeWriter)null!, source)); + await AssertExtensions.ThrowsAsync("value", () => + JsonSerializer.SerializeAsyncEnumerable(new Pipe().Writer, value: null!)); + await AssertExtensions.ThrowsAsync("utf8Json", () => + JsonSerializer.SerializeAsyncEnumerable(utf8Json: (PipeWriter)null!, source, typeInfo)); + await AssertExtensions.ThrowsAsync("value", () => + JsonSerializer.SerializeAsyncEnumerable(new Pipe().Writer, value: null!, typeInfo)); + await AssertExtensions.ThrowsAsync("jsonTypeInfo", () => + JsonSerializer.SerializeAsyncEnumerable(new Pipe().Writer, source, jsonTypeInfo: null!)); + + static async IAsyncEnumerable AsyncEnumerable() + { + await Task.CompletedTask; + yield break; + } + } + + // ---------------------------------------------------------- + // DeserializeAsyncEnumerable JSONL spec coverage (issue 126395) + // ---------------------------------------------------------- + + public static IEnumerable JsonLinesValidShapes() + { + // Per https://jsonlines.org/, every line is a valid JSON value separated by \n. + // The reader is intentionally lenient (Postel's law) so it accepts a superset of strict JSONL. + + // Strict JSONL with trailing line feed. + yield return new object[] { "1\n2\n3\n", new[] { 1, 2, 3 } }; + // Strict JSONL without trailing line feed. + yield return new object[] { "1\n2\n3", new[] { 1, 2, 3 } }; + // Single value, no trailing newline. + yield return new object[] { "42", new[] { 42 } }; + // Single value, with trailing newline. + yield return new object[] { "42\n", new[] { 42 } }; + // Empty document. + yield return new object[] { "", Array.Empty() }; + // Document containing only whitespace. + yield return new object[] { " \n\t\n", Array.Empty() }; + // Document containing only line terminators. + yield return new object[] { "\n\n\n", Array.Empty() }; + // Lenient: \r\n separator (still valid JSONL since values are self-delimiting). + yield return new object[] { "1\r\n2\r\n3\r\n", new[] { 1, 2, 3 } }; + // Lenient: extra whitespace (tabs, spaces) between/around values. + yield return new object[] { " 1 \n\t2\t\n 3 \n", new[] { 1, 2, 3 } }; + } + + [Theory] + [MemberData(nameof(JsonLinesValidShapes))] + public async Task DeserializeAsyncEnumerable_TopLevelValues_AcceptsAllJsonLinesShapes(string input, int[] expected) + { + using Utf8MemoryStream stream = new(input); + + List actual = new(); + await foreach (int item in Serializer.DeserializeAsyncEnumerable(stream, topLevelValues: true)) + { + actual.Add(item); + } + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task DeserializeAsyncEnumerable_TopLevelValues_HeterogeneousJsonValueTypes() + { + // JSONL spec admits any JSON value, not just objects. + // Verify we round-trip through JsonElement to cover all six JSON token kinds. + const string Document = "null\ntrue\n42\n\"hello\"\n[1,2,3]\n{\"k\":\"v\"}\n"; + + using Utf8MemoryStream stream = new(Document); + + List values = new(); + await foreach (JsonElement element in Serializer.DeserializeAsyncEnumerable(stream, topLevelValues: true)) + { + values.Add(element.Clone()); + } + + Assert.Equal(6, values.Count); + Assert.Equal(JsonValueKind.Null, values[0].ValueKind); + Assert.Equal(JsonValueKind.True, values[1].ValueKind); + Assert.Equal(JsonValueKind.Number, values[2].ValueKind); + Assert.Equal(JsonValueKind.String, values[3].ValueKind); + Assert.Equal(JsonValueKind.Array, values[4].ValueKind); + Assert.Equal(JsonValueKind.Object, values[5].ValueKind); + } + + [Theory] + [InlineData(1)] + [InlineData(8)] + [InlineData(64)] + [InlineData(4096)] + public async Task DeserializeAsyncEnumerable_TopLevelValues_HandlesArbitraryBufferBoundaries(int bufferSize) + { + // Stress test: ensure the reader correctly resumes across small/large stream chunks + // when JSONL line terminators land in different places relative to buffer boundaries. + const int Count = 200; + + StringBuilder sb = new(); + for (int i = 0; i < Count; i++) + { + sb.Append('{').Append("\"V\":").Append(i).Append("}\n"); + } + + using Utf8MemoryStream stream = new(sb.ToString()); + JsonSerializerOptions options = new() { DefaultBufferSize = bufferSize }; + + int next = 0; + await foreach (Dictionary entry in Serializer.DeserializeAsyncEnumerable>(stream, topLevelValues: true, options)) + { + Assert.Equal(next, entry["V"]); + next++; + } + + Assert.Equal(Count, next); + } + + // ----------------------------- + // Helpers + // ----------------------------- + + private static async IAsyncEnumerable GenerateItems(int count) + { + for (int i = 0; i < count; i++) + { + var obj = new SimpleTestClass(); + obj.Initialize(); + obj.MyInt32 = i; + yield return obj; + await Task.Yield(); + } + } + + private static async Task ReadAllAsync(Pipe pipe, Task writerTask) + { + // Run the writer to completion in parallel with the reader. + // When the writer finishes, signal end-of-stream by completing the PipeWriter. + Task completer = writerTask.ContinueWith( + t => pipe.Writer.CompleteAsync(t.Exception?.InnerException).AsTask(), + TaskScheduler.Default).Unwrap(); + + using MemoryStream output = new(); + try + { + while (true) + { + ReadResult result = await pipe.Reader.ReadAsync(); + foreach (ReadOnlyMemory segment in result.Buffer) + { + byte[] tmp = segment.ToArray(); + output.Write(tmp, 0, tmp.Length); + } + + pipe.Reader.AdvanceTo(result.Buffer.End); + if (result.IsCompleted) + { + break; + } + } + } + finally + { + await pipe.Reader.CompleteAsync(); + } + + await completer; + await writerTask; + return output.ToArray(); + } } } From dd17b6dcc548fed9998e4f76c8708b57c5e5dd78 Mon Sep 17 00:00:00 2001 From: Ahmed Waleed Date: Thu, 30 Apr 2026 16:29:00 +0300 Subject: [PATCH 039/115] Optimize ImmutableHashSet.SetEquals to avoid unnecessary allocations (#126309) Fixes #90986, Part of #127279 ### Summary `ImmutableHashSet.SetEquals` always creates a new intermediate `HashSet` for the `other` collection, leading to avoidable allocations and GC pressure, especially for large datasets ### Optimization Logic * **O(1) Pre-Scan**: Immediately returns `false` if `other` is an `ICollection` with a smaller `Count`, avoiding any overhead. * **Fast-Path Pattern Matching**: Detects `ImmutableHashSet` and `HashSet` to bypass intermediate allocations. * **Comparer Guard**: Validates `EqualityComparer` compatibility before triggering fast paths to ensure logical consistency. * **Short-Circuit Validation**: Re-validates `Count` within specialized paths for an immediate exit before $O(n)$ enumeration. * **Reverse-Lookup Strategy**: An architectural shift where the ImmutableHashSet (The Source) iterates and queries the other collection if was Hashset. This leverages the O(1) lookup of the HashSet instead of the O(log N) lookup of the immutable tree. * **Zero-Allocation Execution**: Direct iteration over compatible collections, eliminating the costly `new HashSet(other)` fallback. * **Deferred fallback**: Reserves the expensive allocation solely for general `IEnumerable` types.
Click to expand Benchmark Source Code ```csharp using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Order; using BenchmarkDotNet.Running; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; namespace ImmutableHashSetBenchmarks { [MemoryDiagnoser] [Orderer(SummaryOrderPolicy.FastestToSlowest)] [RankColumn] public class ImmutableHashSetSetEqualsBenchmark_Int { private ImmutableHashSet _sourceSet = null!; private ImmutableHashSet _immutableHashSetEqual = null!; private HashSet _bclHashSetEqual = null!; private List _listEqual = null!; private IEnumerable _linqSelectEqual = null!; private int[] _arrayEqual = null!; private List _listLastDiff = null!; private List _listSmaller = null!; private ImmutableHashSet _immutableLarger = null!; private int[] _smallerArray = null!; private HashSet _smallerHashSetDiffComparer = null!; // Worst case: same count, last element different private ImmutableHashSet _immutableHashSetLastDiff = null!; private HashSet _bclHashSetLastDiff = null!; private List _listWithDuplicates = null!; private List _listWithDuplicatesMatch = null!; // Different comparers (fallback path) private HashSet _bclHashSetDiffComparer = null!; // Count mismatch early exit private ImmutableHashSet _immutableHashSetSmaller = null!; private HashSet _bclHashSetSmaller = null!; // Lazy enumerable for worst case private IEnumerable _lazyEnumerableLastDiff = null!; [Params(100000)] public int Size { get; set; } [GlobalSetup] public void Setup() { var elements = Enumerable.Range(0, Size).ToList(); var elementsWithLastDiff = Enumerable.Range(0, Size - 1).Concat(new[] { Size + 1000 }).ToList(); var smallerElements = Enumerable.Range(0, Size / 2).ToList(); var duplicates = Enumerable.Repeat(1, Size).ToList(); var smallerList = new List(); for(int i = 0; i < Size - 1; i++) smallerList.Add(i); _sourceSet = ImmutableHashSet.CreateRange(elements); _immutableHashSetEqual = ImmutableHashSet.CreateRange(elements); _bclHashSetEqual = new HashSet(elements); _listEqual = elements; _linqSelectEqual = elements.Select(x => x); // Lazy LINQ enumerable _arrayEqual = elements.ToArray(); _immutableHashSetLastDiff = ImmutableHashSet.CreateRange(elementsWithLastDiff); _bclHashSetLastDiff = new HashSet(elementsWithLastDiff); _listLastDiff = elementsWithLastDiff; _bclHashSetDiffComparer = new HashSet(elements, new ReverseComparer()); _immutableHashSetSmaller = ImmutableHashSet.CreateRange(smallerElements); _bclHashSetSmaller = new HashSet(smallerElements); _lazyEnumerableLastDiff = elementsWithLastDiff.Select(x => x); _immutableLarger = ImmutableHashSet.CreateRange(elements.Concat(new[] { -1 })); _listWithDuplicates = duplicates; _listWithDuplicatesMatch = elements.Concat(elements).ToList(); // Matches source but with duplicates _listSmaller = smallerList; _smallerArray = Enumerable.Range(0, Size - 1).ToArray(); _smallerHashSetDiffComparer = new HashSet(_listSmaller, new ReverseComparer()); } #region Fast Path: Same Type and Comparer (Optimized) [Benchmark(Description = "ImmutableHashSet (Match - Same Comparer)")] public bool Case_ImmutableHashSet_Match() => _sourceSet.SetEquals(_immutableHashSetEqual); [Benchmark(Description = "BCL HashSet (Match - Same Comparer)")] public bool Case_BclHashSet_Match() => _sourceSet.SetEquals(_bclHashSetEqual); [Benchmark(Description = "ImmutableHashSet (Mismatch - Same Count)")] public bool Case_ImmutableHashSet_LastDiff() => _sourceSet.SetEquals(_immutableHashSetLastDiff); [Benchmark(Description = "Case 04: BCL HashSet (Mismatch - Same Count)")] public bool Case_BclHashSet_LastDiff() => _sourceSet.SetEquals(_bclHashSetLastDiff); #endregion #region Early Exit: Count Mismatch [Benchmark(Description = "ImmutableHashSet (Smaller Count)")] public bool Case_ImmutableHashSet_SmallerCount() => _sourceSet.SetEquals(_immutableHashSetSmaller); [Benchmark(Description = "BCL HashSet (Smaller Count)")] public bool Case_BclHashSet_SmallerCount() => _sourceSet.SetEquals(_bclHashSetSmaller); [Benchmark(Description = "Array (Smaller Count)")] public bool Case_SmallerCollection_EarlyExit() { return _sourceSet.SetEquals(_smallerArray); } #endregion #region Fallback Path: Different Comparer [Benchmark(Description = "HashSet (Different Comparer)")] public bool Case_HashSet_DifferentComparer() => _sourceSet.SetEquals(_bclHashSetDiffComparer); [Benchmark(Description = "HashSet (Smaller Count - Different Comparer)")] public bool Case_HashSet_SmallerCount_DiffComparer() => _sourceSet.SetEquals(_smallerHashSetDiffComparer); #endregion #region Fallback Path: Non-Set Collections (IEnumerable/ICollection) [Benchmark(Description = "List (Match - Fallback)")] public bool Case_List_Match() => _sourceSet.SetEquals(_listEqual); [Benchmark(Description = "LINQ (Mismatch - Lazy IEnumerable)")] public bool Case_LazyEnumerable_LastDiff() => _sourceSet.SetEquals(_lazyEnumerableLastDiff); [Benchmark(Description = "LINQ (Match - Lazy IEnumerable)")] public bool Case_LazyEnumerable_Match() => _sourceSet.SetEquals(_linqSelectEqual); [Benchmark(Description = "List (Last Diff - Fallback)")] public bool Case_List_LastDiff() => _sourceSet.SetEquals(_listLastDiff); [Benchmark(Description = "Array (Match - Fallback)")] public bool Case_Array_Match() => _sourceSet.SetEquals(_arrayEqual); [Benchmark(Description = "ImmutableHashSet (Larger Count)")] public bool Case_LargerCount() => _sourceSet.SetEquals(_immutableLarger); #endregion #region Handling Duplicates (Fallback Path) [Benchmark(Description = "List with Duplicates (Mismatch)")] public bool Case_List_Duplicates_Mismatch() => _sourceSet.SetEquals(_listWithDuplicates); [Benchmark(Description = "List with Duplicates (Match)")] public bool Case_List_Duplicates_Match() => _sourceSet.SetEquals(_listWithDuplicatesMatch); #endregion } public class ReverseComparer : IEqualityComparer where T : IComparable { public bool Equals(T? x, T? y) { if (x is null && y is null) return true; if (x is null || y is null) return false; return x.CompareTo(y) == 0; } public int GetHashCode(T? obj) { return obj?.GetHashCode() ?? 0; } } public class Program { public static void Main(string[] args) { BenchmarkRunner.Run(); } } } ```
Click to expand Benchmark Results ### Benchmark Results (Before Optimization) | Method | Size | Mean | Error | StdDev | Rank | Gen0 | Gen1 | Gen2 | Allocated | |:--- |:---:|---:|---:|---:|---:|---:|---:|---:|---:| | 'BCL HashSet (Smaller Count)' | 100000 | 313.8 us | 6.01 us | 6.43 us | 1 | 15.6250 | 15.6250 | 15.6250 | 818.33 KB | | 'Array (Smaller Count)' | 100000 | 647.9 us | 11.20 us | 11.50 us | 2 | 26.3672 | 26.3672 | 26.3672 | 1697.7 KB | | 'List with Duplicates (Mismatch)' | 100000 | 954.1 us | 18.77 us | 41.60 us | 3 | 31.2500 | 31.2500 | 31.2500 | 1697.77 KB | | ' HashSet (Smaller Count - Different Comparer)' | 100000 | 1,449.3 us | 28.65 us | 74.46 us | 4 | 41.0156 | 41.0156 | 41.0156 | 1697.8 KB | | ' ImmutableHashSet (Smaller Count)' | 100000 | 4,733.2 us | 74.18 us | 69.39 us | 5 | 23.4375 | 23.4375 | 23.4375 | 818.58 KB | | ' BCL HashSet (Match - Same Comparer)' | 100000 | 7,084.0 us | 65.02 us | 57.64 us | 6 | 54.6875 | 54.6875 | 54.6875 | 1697.9 KB | | 'Array (Match - Fallback)' | 100000 | 7,821.7 us | 30.71 us | 27.23 us | 7 | 46.8750 | 46.8750 | 46.8750 | 1697.86 KB | | 'List (Match - Fallback)' | 100000 | 8,428.4 us | 30.82 us | 28.83 us | 8 | 46.8750 | 46.8750 | 46.8750 | 1697.9 KB | | 'BCL HashSet (Mismatch - Same Count)' | 100000 | 8,636.3 us | 52.37 us | 46.42 us | 8 | 46.8750 | 46.8750 | 46.8750 | 1697.86 KB | | 'List (Last Diff - Fallback)' | 100000 | 9,172.5 us | 35.85 us | 33.54 us | 9 | 46.8750 | 46.8750 | 46.8750 | 1697.9 KB | | 'List with Duplicates (Match)' | 100000 | 9,310.2 us | 128.11 us | 119.83 us | 9 | 109.3750 | 109.3750 | 109.3750 | 3521.42 KB | | ' ImmutableHashSet (Larger Count)' | 100000 | 9,477.3 us | 141.55 us | 125.48 us | 9 | 46.8750 | 46.8750 | 46.8750 | 1697.89 KB | | ' HashSet (Different Comparer)' | 100000 | 9,839.2 us | 99.14 us | 87.88 us | 9 | 46.8750 | 46.8750 | 46.8750 | 1697.79 KB | | 'LINQ (Mismatch - Lazy IEnumerable)' | 100000 | 11,274.4 us | 63.77 us | 56.53 us | 10 | 296.8750 | 156.2500 | 156.2500 | 4717.23 KB | | 'LINQ (Match - Lazy IEnumerable)' | 100000 | 11,341.5 us | 69.37 us | 61.49 us | 10 | 296.8750 | 156.2500 | 156.2500 | 4717.23 KB | | 'ImmutableHashSet (Mismatch - Same Count)' | 100000 | 17,015.5 us | 170.03 us | 150.73 us | 11 | 31.2500 | 31.2500 | 31.2500 | 1697.88 KB | | 'ImmutableHashSet (Match - Same Comparer)' | 100000 | 17,410.2 us | 334.48 us | 312.87 us | 11 | 31.2500 | 31.2500 | 31.2500 | 1697.87 KB | --- ### Benchmark Results (After Optimization) | Method | Size | Mean | Error | StdDev | Rank | Gen0 | Gen1 | Gen2 | Allocated | |:--- |:--- |---:|---:|---:|:---:|---:|---:|---:|---:| | 'ImmutableHashSet (Smaller Count)' | 100000 | 2.300 ns | 0.0478 ns | 0.0447 ns | 1 | - | - | - | - | | 'ImmutableHashSet (Larger Count)' | 100000 | 2.328 ns | 0.0650 ns | 0.0576 ns | 1 | - | - | - | - | | 'BCL HashSet (Smaller Count)' | 100000 | 2.595 ns | 0.0524 ns | 0.0491 ns | 2 | - | - | - | - | | 'HashSet (Smaller Count - Different Comparer)' | 100000 | 2.644 ns | 0.0464 ns | 0.0411 ns | 2 | - | - | - | - | | 'Array (Smaller Count)' | 100000 | 2.711 ns | 0.0568 ns | 0.0504 ns | 2 | - | - | - | - | | 'List with Duplicates (Mismatch)' | 100000 | 794,876.698 ns | 15,781.0452 ns | 35,941.4284 ns | 3 | 31.2500 | 31.2500 | 31.2500 | 1738498 B | | 'List (Last Diff - Fallback)' | 100000 | 4,722,211.915 ns | 55,323.2393 ns | 51,749.3924 ns | 4 | 54.6875 | 54.6875 | 54.6875 | 1738698 B | | 'List (Match - Fallback)' | 100000 | 4,778,905.952 ns | 33,894.4095 ns | 28,303.3670 ns | 4 | 54.6875 | 54.6875 | 54.6875 | 1738688 B | | 'List with Duplicates (Match)' | 100000 | 5,517,422.167 ns | 110,159.9473 ns | 171,505.7803 ns | 5 | 93.7500 | 93.7500 | 93.7500 | 3605853 B | | 'BCL HashSet (Match - Same Comparer)' | 100000 | 5,576,721.937 ns | 45,754.5403 ns | 38,207.1134 ns | 5 | - | - | - | - | | 'Case 04: BCL HashSet (Mismatch - Same Count)' | 100000 | 5,640,651.163 ns | 64,526.5199 ns | 60,358.1468 ns | 5 | - | - | - | - | | 'LINQ (Mismatch - Lazy IEnumerable)' | 100000 | 6,406,188.227 ns | 132,260.6999 ns | 379,480.7689 ns | 6 | 281.2500 | 140.6250 | 140.6250 | 4830429 B | | 'LINQ (Match - Lazy IEnumerable)' | 100000 | 6,784,385.648 ns | 135,159.5121 ns | 290,945.1304 ns | 7 | 250.0000 | 125.0000 | 125.0000 | 4830439 B | | 'Array (Match - Fallback)' | 100000 | 6,812,793.701 ns | 40,732.0373 ns | 36,107.8901 ns | 7 | 54.6875 | 54.6875 | 54.6875 | 1738653 B | | 'HashSet (Different Comparer)' | 100000 | 7,497,254.730 ns | 80,339.5419 ns | 75,149.6574 ns | 8 | 62.5000 | 62.5000 | 62.5000 | 1738753 B | | 'ImmutableHashSet (Mismatch - Same Count)' | 100000 | 12,946,989.847 ns | 94,279.9494 ns | 83,576.7194 ns | 9 | - | - | - | - | | 'ImmutableHashSet (Match - Same Comparer)' | 100000 | 13,615,905.022 ns | 57,544.4439 ns | 48,052.2169 ns | 10 | - | - | - | - |
### Performance Analysis Summary (100,000 Elements) | Case / Method | Before (ns) | After (ns) | Speedup Ratio | Memory Improvement | |:---|---:|---:|---:|:---| | **ImmutableHashSet (Larger Count)** | 9,477,300 | 2.328 | **~4,071,005x** | **Zero Alloc** | | **ImmutableHashSet (Smaller Count)** | 4,733,200 | 2.300 | **~2,057,913x** | **Zero Alloc** | | **HashSet (Smaller - Diff Comparer)** | 1,449,300 | 2.644 | **~548,146x** | **Zero Alloc** | | **Array (Smaller Count)** | 647,900 | 2.711 | **~238,989x** | **Zero Alloc** | | **BCL HashSet (Smaller Count)** | 313,800 | 2.595 | **~120,924x** | **Zero Alloc** | | **HashSet (Different Comparer)** | 9,839,200 | 7,497,254 | **1.31x** | Stable (~1.7 MB) | | **LINQ (Match/Mismatch)** | 11,341,500 | 6,406,188 | **1.77x** | Stable (~4.8 MB) | | **BCL HashSet (Mismatch - Same Count)**| 8,636,300 | 5,640,651 | **1.53x** | **Zero Alloc** | | **ImmutableHashSet (Match)** | 17,410,200 | 13,615,905 | **1.28x** | **Zero Alloc** | | **ImmutableHashSet (Mismatch)** | 17,015,500 | 12,946,989 | **1.31x** | **Zero Alloc** | | **List (Match/Diff - Fallback)** | 9,172,500 | 4,722,211 | **1.94x** | Stable (~1.7 MB) | | **BCL HashSet (Match - Same Comp)** | 7,084,000 | 5,576,721 | **1.27x** | **Zero Alloc** | | **List (Duplicates - Mismatch)** | 954,100 | 794,876 | **1.20x** | Stable (~1.7 MB) | | **List (Duplicates - Match)** | 9,310,200 | 5,517,422 | **1.69x** | Stable (~3.6 MB) | | **Array (Match - Fallback)** | 7,821,700 | 6,812,793 | **1.15x** | Stable (~1.7 MB) | --- .../Immutable/ImmutableHashSet_1.cs | 76 +++++++++++++++++-- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableHashSet_1.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableHashSet_1.cs index c6b6f3140e4cc2..4c9974fadd5b0b 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableHashSet_1.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableHashSet_1.cs @@ -736,26 +736,90 @@ private static bool Overlaps(IEnumerable other, MutationInput origin) return false; } - /// - /// Performs the set operation on a given data structure. - /// private static bool SetEquals(IEnumerable other, MutationInput origin) { Requires.NotNull(other, nameof(other)); + switch (other) + { + case ImmutableHashSet otherAsImmutableHashSet: + if (otherAsImmutableHashSet.Count != origin.Count) + { + return false; + } + + if (EqualityComparer>.Default.Equals(origin.EqualityComparer, otherAsImmutableHashSet.KeyComparer)) + { + return SetEqualsWithImmutableHashset(otherAsImmutableHashSet, origin); + } + break; + + case HashSet otherAsHashset: + if (otherAsHashset.Count != origin.Count) + { + return false; + } + + if (EqualityComparer>.Default.Equals(origin.EqualityComparer, otherAsHashset.Comparer)) + { + return SetEqualsWithHashset(otherAsHashset, origin); + } + break; + + case ICollection otherAsICollectionGeneric: + // We check for < instead of != because other is not guaranteed to be a set, it could be a collection with duplicates. + if (otherAsICollectionGeneric.Count < origin.Count) + { + return false; + } + break; + + case ICollection otherAsICollection: + if (otherAsICollection.Count < origin.Count) + { + return false; + } + break; + } + var otherSet = new HashSet(other, origin.EqualityComparer); - if (origin.Count != otherSet.Count) + if (otherSet.Count != origin.Count) { return false; } - foreach (T item in otherSet) + return SetEqualsWithHashset(otherSet, origin); + } + + private static bool SetEqualsWithImmutableHashset(ImmutableHashSet other, MutationInput origin) + { + Requires.NotNull(other, nameof(other)); + + using var e = new ImmutableHashSet.Enumerator(origin.Root); + while (e.MoveNext()) { - if (!Contains(item, origin)) + if (!other.Contains(e.Current)) { return false; } } + + return true; + } + + private static bool SetEqualsWithHashset(HashSet other, MutationInput origin) + { + Requires.NotNull(other, nameof(other)); + + using var e = new ImmutableHashSet.Enumerator(origin.Root); + while (e.MoveNext()) + { + if (!other.Contains(e.Current)) + { + return false; + } + } + return true; } From 4ff20e677cef69aefa2df70cd8b615adfc9df13a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 07:09:36 -0700 Subject: [PATCH 040/115] cDAC: Add GetGCDescSeries contract API and continuation pretty-printing (#127419) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Implements continuation pretty-printing in the cDAC, mirroring `AsyncContinuationsManager::PrintContinuationName` from `asynccontinuations.h`, and adds a new `GetGCDescSeries` API to the `RuntimeTypeSystem` contract. Fixes the WebApp3 test failure seen in recent runtime-diagnostics legs such as this: https://dev.azure.com/dnceng-public/public/_build/results?buildId=1395576 The `GetGCDescSeries` API accepts an optional `numComponents` parameter (defaulting to `0` for non-array types) and handles both regular (positive `NumSeries`) and value-class repeating (negative `NumSeries`) GCDesc layouts. For value-class GCDesc, the outer loop iterates `numComponents` times — callers must pass the actual element count to enumerate pointer runs across array elements. > [!NOTE] > This PR was created with the assistance of GitHub Copilot (AI-generated content). --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rcj1 <77995559+rcj1@users.noreply.github.com> Co-authored-by: rcj1 Co-authored-by: Noah Falk --- .../design/datacontracts/RuntimeTypeSystem.md | 60 +++ .../vm/datadescriptor/datadescriptor.inc | 4 + .../Contracts/IRuntimeTypeSystem.cs | 6 + .../DataType.cs | 1 + .../Contracts/RuntimeTypeSystem_1.cs | 75 ++++ .../TypeNameBuilder.cs | 56 ++- .../managed/cdac/tests/MethodTableTests.cs | 398 ++++++++++++++++++ .../MockDescriptors.RuntimeTypeSystem.cs | 130 ++++++ 8 files changed, 729 insertions(+), 1 deletion(-) diff --git a/docs/design/datacontracts/RuntimeTypeSystem.md b/docs/design/datacontracts/RuntimeTypeSystem.md index 83182979b22088..d3f86c8bf0d74d 100644 --- a/docs/design/datacontracts/RuntimeTypeSystem.md +++ b/docs/design/datacontracts/RuntimeTypeSystem.md @@ -58,6 +58,11 @@ partial interface IRuntimeTypeSystem : IContract public virtual bool RequiresAlign8(TypeHandle typeHandle); // True if the MethodTable represents a continuation type used by the async continuation feature public virtual bool IsContinuation(TypeHandle typeHandle); + // Returns the GC pointer runs for the method table as (offset, size) pairs. Each + // run starts Offset bytes from the object pointer (`this`), where offset 0 + // is the method table pointer, and includes Size bytes of contiguous pointers + // For handles representing value types the object is assumed to be stored in the boxed layout. + public virtual IEnumerable<(uint Offset, uint Size)> GetGCDescSeries(TypeHandle typeHandle, uint numComponents = 0); public virtual bool IsDynamicStatics(TypeHandle typeHandle); public virtual ushort GetNumInterfaces(TypeHandle typeHandle); @@ -554,6 +559,61 @@ Contracts used: public bool RequiresAlign8(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.RequiresAlign8; + public bool IsContinuation(TypeHandle typeHandle) => typeHandle.IsMethodTable() + && ContinuationMethodTablePointer != TargetPointer.Null + && _methodTables[typeHandle.Address].ParentMethodTable == ContinuationMethodTablePointer; + + IEnumerable<(uint Offset, uint Size)> GetGCDescSeries(TypeHandle typeHandle, uint numComponents = 0) + { + // Returns empty if not a method table or has no GC pointers. + // Compute objectSize: baseSize + numComponents * componentSize. + // For non-array types, numComponents == 0 so objectSize == baseSize. + + // Read NumSeries from (mtAddress - pointerSize), sign-extended to native width. + // NumSeries == 0 → empty. + + // NumSeries > 0: Regular series. + // Memory layout (each slot is pointer-sized, growing away from MT): + // + // MT - (2*N+1)*ptrSize : series[N-1].seriessize + // MT - (2*N) *ptrSize : series[N-1].startoffset + // ... + // MT - 3*ptrSize : series[0].seriessize + // MT - 2*ptrSize : series[0].startoffset + // MT - 1*ptrSize : NumSeries (positive) + // MT : MethodTable + // + // The raw seriessize is stored with baseSize subtracted. + // Add objectSize back to get the true run length: + // trueRunLength = rawSeriesSize + objectSize + // For non-arrays objectSize == baseSize, recovering the actual run. + // For arrays objectSize > baseSize, extending the single series across all elements. + + // NumSeries < 0: Value-class (repeating) series. + // |NumSeries| val_serie_items describe pointer runs within one array element. + // Memory layout: + // + // MT - (N+2)*ptrSize : val_serie[N-1] + // ... + // MT - 3*ptrSize : val_serie[0] + // MT - 2*ptrSize : startoffset + // MT - 1*ptrSize : NumSeries (-N) + // MT : MethodTable + // + // Each val_serie_item is { HALF_SIZE_T nptrs; HALF_SIZE_T skip; } packed into one pointer-width. + // HALF_SIZE_T is uint16 on 32-bit, uint32 on 64-bit. + // Read nptrs and skip as separate typed reads for endianness safety. + // runBytes = nptrs * pointerSize; advance currentOffset by runBytes + skip after each item. + // + // The val_serie_items describe the GC pointer layout of a single array element. + // An outer loop repeats the pattern across all elements in the array: + // currentOffset = startoffset + // while currentOffset <= objectSize - pointerSize: + // for each val_serie_item: + // yield (currentOffset, nptrs * pointerSize) + // currentOffset += nptrs * pointerSize + skip + } + public bool IsDynamicStatics(TypeHandle TypeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[TypeHandle.Address].Flags.IsDynamicStatics; public ushort GetNumInterfaces(TypeHandle TypeHandle) => !typeHandle.IsMethodTable() ? 0 : _methodTables[TypeHandle.Address].NumInterfaces; diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 6caeddf628fbd8..40a9397767d289 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -177,6 +177,10 @@ CDAC_TYPE_FIELD(String, T_POINTER, m_FirstChar, cdac_data::m_First CDAC_TYPE_FIELD(String, T_UINT32, m_StringLength, cdac_data::m_StringLength) CDAC_TYPE_END(String) +CDAC_TYPE_BEGIN(ContinuationObject) +CDAC_TYPE_SIZE(sizeof(ContinuationObject)) +CDAC_TYPE_END(ContinuationObject) + CDAC_TYPE_BEGIN(Array) CDAC_TYPE_SIZE(sizeof(ArrayBase)) CDAC_TYPE_FIELD(Array, T_UINT32, m_NumComponents, cdac_data::m_NumComponents) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs index 8cc74fc5d1ee72..7b81723becd276 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs @@ -121,6 +121,12 @@ public interface IRuntimeTypeSystem : IContract bool RequiresAlign8(TypeHandle typeHandle) => throw new NotImplementedException(); // True if the MethodTable represents a continuation type used by the async continuation feature bool IsContinuation(TypeHandle typeHandle) => throw new NotImplementedException(); + /// + /// Enumerates GC pointer runs from the CGCDesc stored before the method table. + /// Returns (offset, size) pairs normalized to actual byte lengths. + /// See RuntimeTypeSystem.md for the full GCDesc format documentation. + /// + IEnumerable<(uint Offset, uint Size)> GetGCDescSeries(TypeHandle typeHandle, uint numComponents = 0) => throw new NotImplementedException(); bool IsDynamicStatics(TypeHandle typeHandle) => throw new NotImplementedException(); ushort GetNumInterfaces(TypeHandle typeHandle) => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs index f6fe1c47cc1aad..edd009d638a924 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs @@ -78,6 +78,7 @@ public enum DataType StressMsg, StressMsgHeader, Object, + ContinuationObject, NativeObjectWrapperObject, ManagedObjectWrapperHolderObject, ManagedObjectWrapperLayout, diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs index 27e971fc0cd615..a0fe5b5eeb3a54 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs @@ -564,6 +564,81 @@ private Data.EEClass GetClassData(TypeHandle typeHandle) public bool IsContinuation(TypeHandle typeHandle) => typeHandle.IsMethodTable() && _continuationMethodTablePointer != TargetPointer.Null && _methodTables[typeHandle.Address].ParentMethodTable == _continuationMethodTablePointer; + + IEnumerable<(uint Offset, uint Size)> IRuntimeTypeSystem.GetGCDescSeries(TypeHandle typeHandle, uint numComponents) + { + if (!typeHandle.IsMethodTable()) + yield break; + + if (!ContainsGCPointers(typeHandle)) + yield break; + + uint baseSize = GetBaseSize(typeHandle); + uint componentSize = GetComponentSize(typeHandle); + uint objectSize = baseSize + numComponents * componentSize; + + ulong mtAddress = typeHandle.Address; + ulong pointerSize = (ulong)_target.PointerSize; + + // Sign-extend NumSeries from native pointer width. + long numSeries = _target.PointerSize == sizeof(uint) + ? (long)(int)_target.ReadPointer(mtAddress - pointerSize).Value + : (long)_target.ReadPointer(mtAddress - pointerSize).Value; + if (numSeries == 0) + yield break; + + if (numSeries > 0) + { + // Regular series: iterate from highest (closest to MT) to lowest. + for (ulong i = 0; i < (ulong)numSeries; i++) + { + ulong seriesBase = mtAddress - (3 + 2 * i) * pointerSize; + ulong rawSeriesSize = _target.ReadPointer(seriesBase).Value; + ulong seriesOffset = _target.ReadPointer(seriesBase + pointerSize).Value; + yield return ((uint)seriesOffset, (uint)(rawSeriesSize + objectSize)); + } + } + else + { + long absNumSeries = -numSeries; + ulong startOffset = _target.ReadPointer(mtAddress - 2 * pointerSize).Value; + + var seriesItems = new (uint Nptrs, uint Skip)[absNumSeries]; + for (long i = 0; i < absNumSeries; i++) + { + ulong itemAddress = mtAddress - (3 + (ulong)i) * pointerSize; + + // Read val_serie_item fields individually for endianness safety. + uint nptrs, skip; + if (_target.PointerSize == sizeof(uint)) + { + nptrs = _target.Read(itemAddress); + skip = _target.Read(itemAddress + sizeof(ushort)); + } + else + { + nptrs = _target.Read(itemAddress); + skip = _target.Read(itemAddress + sizeof(uint)); + } + + seriesItems[i] = (nptrs, skip); + } + + ulong currentOffset = startOffset; + for (int i = 0; i < numComponents; i++) + { + for (long j = 0; j < absNumSeries; j++) + { + if (currentOffset > objectSize - pointerSize) + yield break; + uint runBytes = seriesItems[j].Nptrs * (uint)pointerSize; + yield return ((uint)currentOffset, runBytes); + currentOffset += runBytes + seriesItems[j].Skip; + } + } + } + } + public bool IsDynamicStatics(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.IsDynamicStatics; public ushort GetNumInterfaces(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? (ushort)0 : _methodTables[typeHandle.Address].NumInterfaces; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/TypeNameBuilder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/TypeNameBuilder.cs index 201fdef999bab0..0bd9de10466510 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/TypeNameBuilder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/TypeNameBuilder.cs @@ -6,6 +6,7 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Text; +using Microsoft.Diagnostics.DataContractReader; using Microsoft.Diagnostics.DataContractReader.Contracts; namespace Microsoft.Diagnostics.DataContractReader.Legacy; @@ -283,7 +284,14 @@ private static void AppendTypeCore(ref TypeNameBuilder tnb, Contracts.TypeHandle Contracts.ModuleHandle moduleHandle = tnb.Target.Contracts.Loader.GetModuleHandleFromModulePtr(typeSystemContract.GetModule(typeHandle)); if (MetadataTokens.EntityHandle((int)typeDefToken).IsNil) { - tnb.AddName("(dynamicClass)"); + if (typeSystemContract.IsContinuation(typeHandle)) + { + AppendContinuationName(ref tnb, typeSystemContract, typeHandle); + } + else + { + tnb.AddName("(dynamicClass)"); + } } else { @@ -480,6 +488,52 @@ private void AddAssemblySpec(string? assemblySpec) } } + /// + /// Builds the synthetic name for a dynamically-created continuation method table, mirroring the + /// native AsyncContinuationsManager::PrintContinuationName in asynccontinuations.h. + /// + /// + /// The name has the form: + /// Continuation_<dataSize>[_<gcOffset>_<gcCount>]* + /// where: + /// + /// + /// dataSize is the number of bytes of data payload (base size minus the fixed + /// object-header and continuation-header overhead). + /// + /// + /// Each _gcOffset_gcCount pair describes one GC-pointer run: gcOffset is the + /// offset in bytes from the start of the data payload to the run, and gcCount is the + /// number of pointer-sized GC references in that run. + /// + /// + /// Only GC descriptor series whose startoffset is at or above the continuation data + /// payload (i.e., after the fixed CORINFO_Continuation header fields) are included. + /// + private static void AppendContinuationName(ref TypeNameBuilder tnb, IRuntimeTypeSystem typeSystemContract, TypeHandle typeHandle) + { + uint baseSize = typeSystemContract.GetBaseSize(typeHandle); + uint continuationDataOffset = tnb.Target.GetTypeInfo(DataType.ContinuationObject).Size!.Value; + uint objHeaderSize = tnb.Target.GetTypeInfo(DataType.ObjectHeader).Size!.Value; + uint dataSize = baseSize - (objHeaderSize + continuationDataOffset); + + var name = new StringBuilder("Continuation_"); + name.Append(dataSize); + + foreach ((uint seriesOffset, uint seriesSize) in typeSystemContract.GetGCDescSeries(typeHandle)) + { + if (seriesOffset < continuationDataOffset) + continue; + + name.Append('_'); + name.Append(seriesOffset - continuationDataOffset); + name.Append('_'); + name.Append(seriesSize / (uint)tnb.Target.PointerSize); + } + + tnb.AddNameNoEscaping(name); + } + private static void AppendNestedTypeDef(ref TypeNameBuilder tnb, MetadataReader reader, TypeDefinitionHandle typeDefToken, TypeNameFormat format) { TypeDefinition typeDef = reader.GetTypeDefinition(typeDefToken); diff --git a/src/native/managed/cdac/tests/MethodTableTests.cs b/src/native/managed/cdac/tests/MethodTableTests.cs index 89a87fd259cc4a..e8c15ad11f6734 100644 --- a/src/native/managed/cdac/tests/MethodTableTests.cs +++ b/src/native/managed/cdac/tests/MethodTableTests.cs @@ -27,6 +27,7 @@ public class MethodTableTests [DataType.ParamTypeDesc] = TargetTestHelpers.CreateTypeInfo(rtsBuilder.ParamTypeDescLayout), [DataType.TypeVarTypeDesc] = TargetTestHelpers.CreateTypeInfo(rtsBuilder.TypeVarTypeDescLayout), [DataType.GCCoverageInfo] = TargetTestHelpers.CreateTypeInfo(rtsBuilder.GCCoverageInfoLayout), + [DataType.ContinuationObject] = new Target.TypeInfo { Size = rtsBuilder.ContinuationObjectSize }, }; internal static (string Name, ulong Value)[] CreateContractGlobals(MockRTS rtsBuilder) @@ -649,4 +650,401 @@ public void RequiresAlign8(MockTarget.Architecture arch, bool flagSet) Contracts.TypeHandle typeHandle = contract.GetTypeHandle(methodTablePtr); Assert.Equal(flagSet, contract.RequiresAlign8(typeHandle)); } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesReturnsEmptyForNonMethodTable(MockTarget.Architecture arch) + { + // TypeDesc handles should yield no series + TargetPointer typeDescAddress = default; + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + MockParamTypeDesc typeDesc = rtsBuilder.AddParamTypeDesc(); + typeDescAddress = typeDesc.Address | (ulong)RuntimeTypeSystem_1.TypeHandleBits.TypeDesc; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeDescHandle = contract.GetTypeHandle(typeDescAddress); + Assert.Empty(contract.GetGCDescSeries(typeDescHandle)); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesReturnsEmptyWhenNoGCPointers(MockTarget.Architecture arch) + { + TargetPointer mtPtr = default; + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + MockEEClass eeClass = rtsBuilder.AddEEClass("NoGCPointers"); + MockMethodTable mt = rtsBuilder.AddMethodTable("NoGCPointers"); + uint baseSize = rtsBuilder.Builder.TargetTestHelpers.ObjectBaseSize; + mt.BaseSize = baseSize; + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + // MTFlags does NOT have ContainsGCPointers (0x01000000) set + mtPtr = mt.Address; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + Assert.False(contract.ContainsGCPointers(typeHandle)); + Assert.Empty(contract.GetGCDescSeries(typeHandle)); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesReturnsSingleSeries(MockTarget.Architecture arch) + { + TargetPointer mtPtr = default; + uint expectedSeriesOffset = 0; + uint expectedSeriesSize = 0; + + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + TargetTestHelpers helpers = rtsBuilder.Builder.TargetTestHelpers; + uint pointerSize = (uint)helpers.PointerSize; + + // Object layout: [ObjHeader][MT*][ref1] + // BaseSize = ObjHeader + MT* + ref1 = 3 * pointerSize + uint baseSize = helpers.ObjHeaderSize + 2u * pointerSize; + + // One series covering the single reference field. + // seriessize is stored as (actualSize - baseSize); for one pointer: pointerSize - baseSize + ulong rawSeriesSize = pointerSize - baseSize; // stored as size_t (wraps to large value) + ulong rawSeriesOffset = helpers.ObjHeaderSize + pointerSize; // after ObjHeader+MT* + + MockEEClass eeClass = rtsBuilder.AddEEClass("SingleRef"); + MockMethodTable mt = rtsBuilder.AddMethodTableWithGCDesc( + "SingleRef", + baseSize, + [(rawSeriesSize, rawSeriesOffset)]); + mt.MTFlags |= 0x01000000u; // ContainsGCPointers + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + + mtPtr = mt.Address; + expectedSeriesOffset = (uint)rawSeriesOffset; + // After normalization: rawSeriesSize + baseSize = pointerSize (one pointer-sized run) + expectedSeriesSize = pointerSize; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + Assert.True(contract.ContainsGCPointers(typeHandle)); + + (uint Offset, uint Size)[] series = contract.GetGCDescSeries(typeHandle).ToArray(); + Assert.Single(series); + Assert.Equal(expectedSeriesOffset, series[0].Offset); + Assert.Equal(expectedSeriesSize, series[0].Size); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesReturnsMultipleSeriesInOrder(MockTarget.Architecture arch) + { + TargetPointer mtPtr = default; + (uint Offset, uint Size)[] expectedSeries = []; + + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + TargetTestHelpers helpers = rtsBuilder.Builder.TargetTestHelpers; + uint pointerSize = (uint)helpers.PointerSize; + + // Two separate GC reference runs in the object. + // Object layout: [ObjHeader][MT*][ref1][nonref][ref2] + uint baseSize = helpers.ObjHeaderSize + 4u * pointerSize; + + ulong series0Offset = helpers.ObjHeaderSize + pointerSize; // ref1 field + ulong series0Size = pointerSize - baseSize; // raw stored size + + ulong series1Offset = helpers.ObjHeaderSize + 3u * pointerSize; // ref2 field + ulong series1Size = pointerSize - baseSize; + + MockEEClass eeClass = rtsBuilder.AddEEClass("TwoRefs"); + MockMethodTable mt = rtsBuilder.AddMethodTableWithGCDesc( + "TwoRefs", + baseSize, + // Ordered highest (lowest index) to lowest as required by AddMethodTableWithGCDesc + [(series0Size, series0Offset), (series1Size, series1Offset)]); + mt.MTFlags |= 0x01000000u; // ContainsGCPointers + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + + mtPtr = mt.Address; + // After normalization (rawSize + baseSize), each series covers one pointer + expectedSeries = + [ + ((uint)series0Offset, pointerSize), + ((uint)series1Offset, pointerSize), + ]; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + + (uint Offset, uint Size)[] series = contract.GetGCDescSeries(typeHandle).ToArray(); + Assert.Equal(expectedSeries.Length, series.Length); + for (int i = 0; i < expectedSeries.Length; i++) + { + Assert.Equal(expectedSeries[i].Offset, series[i].Offset); + Assert.Equal(expectedSeries[i].Size, series[i].Size); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesReturnsSingleValueClassSeries(MockTarget.Architecture arch) + { + // A negative NumSeries indicates a value-class (repeating) series layout. + // This models a 1-element array of struct { ref field; }: one val_serie_item with nptrs=1, skip=0. + TargetPointer mtPtr = default; + uint expectedOffset = 0; + uint expectedSize = 0; + + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + TargetTestHelpers helpers = rtsBuilder.Builder.TargetTestHelpers; + uint pointerSize = (uint)helpers.PointerSize; + + // Array of structs each containing one GC ref. + // startoffset is relative to the object pointer (MT* slot), past MT* + length. + uint startOffset = 2u * pointerSize; // past MT* + length + uint componentSize = pointerSize; // element is struct { ref field; } + uint baseSize = helpers.ObjHeaderSize + 2u * pointerSize; // ObjHeader + MT* + length + + MockEEClass eeClass = rtsBuilder.AddEEClass("ValueClassArray_1ref"); + MockMethodTable mt = rtsBuilder.AddMethodTableWithValueClassGCDesc( + "ValueClassArray_1ref", + baseSize, + startOffset, + [(1, 0)]); // nptrs=1, skip=0 + // Set array flags with componentSize so GetComponentSize returns the element size. + mt.MTFlags = (uint)(MethodTableFlags_1.WFLAGS_HIGH.HasComponentSize + | MethodTableFlags_1.WFLAGS_HIGH.Category_Array) + | componentSize + | 0x01000000u; // ContainsGCPointers + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + mtPtr = mt.Address; + + expectedOffset = startOffset; + expectedSize = 1u * pointerSize; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + Assert.True(contract.ContainsGCPointers(typeHandle)); + + // Pass numComponents=1 because value-class GCDesc iterates one element per component. + (uint Offset, uint Size)[] series = contract.GetGCDescSeries(typeHandle, 1).ToArray(); + Assert.Single(series); + Assert.Equal(expectedOffset, series[0].Offset); + Assert.Equal(expectedSize, series[0].Size); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesReturnsMultipleValueClassSeries(MockTarget.Architecture arch) + { + // Two val_serie_items: [2 ptrs, skip 2*ptrSize] [1 ptr, skip 0 bytes] + // This models a 1-element array of struct { ref a; ref b; int pad1; int pad2; ref c; } + TargetPointer mtPtr = default; + (uint Offset, uint Size)[] expectedSeries = []; + + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + TargetTestHelpers helpers = rtsBuilder.Builder.TargetTestHelpers; + uint pointerSize = (uint)helpers.PointerSize; + + // startoffset is relative to the object pointer (MT* slot), past MT* + length. + uint startOffset = 2u * pointerSize; // past MT* + length + uint baseSize = helpers.ObjHeaderSize + 2u * pointerSize; // ObjHeader + MT* + length + + uint skip = 2u * pointerSize; // two pointer-sized non-ref fields between runs + // Element layout: [ref a (ptr)][ref b (ptr)][pad1 (ptr)][pad2 (ptr)][ref c (ptr)] = 5 * pointerSize + uint componentSize = (2u + 2u + 1u) * pointerSize; + + MockEEClass eeClass = rtsBuilder.AddEEClass("ValueClassArray_2runs"); + MockMethodTable mt = rtsBuilder.AddMethodTableWithValueClassGCDesc( + "ValueClassArray_2runs", + baseSize, + startOffset, + [(2, skip), (1, 0)]); // first: 2 ptrs then skip, second: 1 ptr no skip + // Set array flags with componentSize so GetComponentSize returns the element size. + mt.MTFlags = (uint)(MethodTableFlags_1.WFLAGS_HIGH.HasComponentSize + | MethodTableFlags_1.WFLAGS_HIGH.Category_Array) + | componentSize + | 0x01000000u; // ContainsGCPointers + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + mtPtr = mt.Address; + + expectedSeries = + [ + (startOffset, 2u * pointerSize), // first run: 2 ptrs + (startOffset + 2u * pointerSize + skip, 1u * pointerSize), // second run: 1 ptr after skip + ]; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + + // Pass numComponents=1 because value-class GCDesc iterates one element per component. + (uint Offset, uint Size)[] series = contract.GetGCDescSeries(typeHandle, 1).ToArray(); + Assert.Equal(expectedSeries.Length, series.Length); + for (int i = 0; i < expectedSeries.Length; i++) + { + Assert.Equal(expectedSeries[i].Offset, series[i].Offset); + Assert.Equal(expectedSeries[i].Size, series[i].Size); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesRegularSeriesWithArrayNumComponents(MockTarget.Architecture arch) + { + // object[] has a single regular series. When numComponents > 0, the series size + // should extend across all elements: rawSeriesSize + baseSize + numComponents * componentSize. + TargetPointer mtPtr = default; + uint expectedSeriesOffset = 0; + + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + TargetTestHelpers helpers = rtsBuilder.Builder.TargetTestHelpers; + uint pointerSize = (uint)helpers.PointerSize; + + // object[] layout: [ObjHeader][MT*][Length][elem0][elem1][elem2] + // baseSize covers the header: ObjHeader + MT* + Length = 3 * pointerSize + // componentSize = pointerSize (each element is a reference) + uint baseSize = helpers.ObjHeaderSize + 2u * pointerSize; // ObjHeader + MT* + Length field + uint componentSize = pointerSize; + + // One series starting after ObjHeader+MT*+Length, covering element slots. + // rawSeriesSize is stored as (actualRunForOneElement - baseSize). + // For object[], the series covers from first element to end: actualRun = pointerSize (per-element). + // But the raw value encodes (pointerSize - baseSize) so that rawSeriesSize + objectSize gives total span. + ulong rawSeriesSize = pointerSize - baseSize; // wraps unsigned + ulong seriesOffset = helpers.ObjHeaderSize + 2u * pointerSize; // after header + length + + MockEEClass eeClass = rtsBuilder.AddEEClass("ObjectArray"); + MockMethodTable mt = rtsBuilder.AddMethodTableWithGCDesc( + "ObjectArray", + baseSize, + [(rawSeriesSize, seriesOffset)]); + // Set array flags: HasComponentSize | Category_Array | componentSize in low bits + mt.MTFlags = (uint)(MethodTableFlags_1.WFLAGS_HIGH.HasComponentSize + | MethodTableFlags_1.WFLAGS_HIGH.Category_Array) + | componentSize + | 0x01000000u; // ContainsGCPointers + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + + mtPtr = mt.Address; + expectedSeriesOffset = (uint)seriesOffset; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + Assert.True(contract.ContainsGCPointers(typeHandle)); + uint pointerSz = (uint)target.PointerSize; + + // With 0 components, series size = rawSeriesSize + baseSize = pointerSize (one element worth) + (uint Offset, uint Size)[] series0 = contract.GetGCDescSeries(typeHandle, 0).ToArray(); + Assert.Single(series0); + Assert.Equal(expectedSeriesOffset, series0[0].Offset); + Assert.Equal(pointerSz, series0[0].Size); + + // With 3 components, objectSize = baseSize + 3*pointerSize, so series size = pointerSize - baseSize + objectSize = 4*pointerSize + uint numComponents = 3; + (uint Offset, uint Size)[] series3 = contract.GetGCDescSeries(typeHandle, numComponents).ToArray(); + Assert.Single(series3); + Assert.Equal(expectedSeriesOffset, series3[0].Offset); + Assert.Equal((numComponents + 1) * pointerSz, series3[0].Size); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetGCDescSeriesValueClassRepeatingWithArrayNumComponents(MockTarget.Architecture arch) + { + // Array of structs where each element has one GC ref (nptrs=1, skip=pointerSize for a non-ref field). + // With numComponents > 0, the repeating pattern should iterate across multiple elements. + TargetPointer mtPtr = default; + + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + TargetTestHelpers helpers = rtsBuilder.Builder.TargetTestHelpers; + uint pointerSize = (uint)helpers.PointerSize; + + // Array layout: [ObjHeader][MT*][Length][elem0.ref][elem0.int][elem1.ref][elem1.int]... + // Each element is { ref field, int field } = 2 * pointerSize. + uint baseSize = helpers.ObjHeaderSize + 2u * pointerSize; // header + length + uint componentSize = 2u * pointerSize; + uint startOffset = helpers.ObjHeaderSize + 2u * pointerSize; // first element starts after header + + MockEEClass eeClass = rtsBuilder.AddEEClass("StructArray"); + MockMethodTable mt = rtsBuilder.AddMethodTableWithValueClassGCDesc( + "StructArray", + baseSize, + startOffset, + [(1, pointerSize)]); // nptrs=1 ref, skip=pointerSize (non-ref field) + mt.MTFlags = (uint)(MethodTableFlags_1.WFLAGS_HIGH.HasComponentSize + | MethodTableFlags_1.WFLAGS_HIGH.Category_Array) + | componentSize + | 0x01000000u; // ContainsGCPointers + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + + mtPtr = mt.Address; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + Contracts.TypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + Assert.True(contract.ContainsGCPointers(typeHandle)); + uint elemSize = 2 * (uint)target.PointerSize; + uint startOff = 3u * (uint)target.PointerSize; + + // With 0 components, the for loop runs 0 times so the result is always empty. + (uint Offset, uint Size)[] series0 = contract.GetGCDescSeries(typeHandle, 0).ToArray(); + Assert.Empty(series0); + + // With 2 components, objectSize = baseSize + 2 * elemSize = baseSize + 4*ptr. + // The loop should produce 2 runs (one per element), each at the ref field of that element. + uint numComponents = 2; + (uint Offset, uint Size)[] series2 = contract.GetGCDescSeries(typeHandle, numComponents).ToArray(); + Assert.Equal(2, series2.Length); + Assert.Equal(startOff, series2[0].Offset); + Assert.Equal((uint)target.PointerSize, series2[0].Size); + Assert.Equal(startOff + elemSize, series2[1].Offset); + Assert.Equal((uint)target.PointerSize, series2[1].Size); + } } diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs index 8510203e0e5ae0..ccf99761dd4dd4 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.RuntimeTypeSystem.cs @@ -316,6 +316,8 @@ public class RuntimeTypeSystem internal ulong ContinuationMethodTableGlobalAddress => TestContinuationMethodTableGlobalAddress; internal ulong MethodDescAlignment => GetMethodDescAlignment(Builder.TargetTestHelpers); internal ulong ArrayBaseSize => Builder.TargetTestHelpers.ArrayBaseBaseSize; + // sizeof(ContinuationObject) = sizeof(MT*) + sizeof(Next*) + sizeof(Resume*) + sizeof(Flags) + sizeof(State) + internal uint ContinuationObjectSize => 3u * (uint)Builder.TargetTestHelpers.PointerSize + 8; public RuntimeTypeSystem(MockMemorySpace.Builder builder) : this(builder, (DefaultAllocationRangeStart, DefaultAllocationRangeEnd)) @@ -447,5 +449,133 @@ private TView Add(Layout layout, ulong size, string name) MockMemorySpace.HeapFragment fragment = TypeSystemAllocator.Allocate(size, name); return layout.Create(fragment.Data.AsMemory(), fragment.Address); } + + /// + /// Allocates a method table together with a CGCDesc immediately before it in memory. + /// + /// Descriptive name for the allocation. + /// Value to store in the method table's BaseSize field. + /// + /// GC descriptor series ordered from highest to lowest + /// (matching CGCDesc::GetHighestSeries down to GetLowestSeries). + /// Each entry is (Size, Offset) – the raw field values stored in the + /// CGCDescSeries struct (i.e. seriessize already has BaseSize subtracted). + /// + /// + /// A whose address is after the GCDesc bytes. + /// The caller is responsible for setting additional method table flags (e.g. + /// ContainsGCPointers = 0x01000000) and linking to an EEClass. + /// + internal MockMethodTable AddMethodTableWithGCDesc(string name, uint baseSize, (ulong Size, ulong Offset)[] series) + { + int pointerSize = Builder.TargetTestHelpers.PointerSize; + + // GCDesc layout (each slot is pointer-sized): + // [ series[N-1].seriessize ] [ series[N-1].startoffset ] <- lowest series (fragment start) + // ... + // [ series[0].seriessize ] [ series[0].startoffset ] <- highest series (MT - 3*ptrSize) + // [ NumSeries ] <- MT - 1*ptrSize + // [ MethodTable data starts here ] + int gcDescSize = (1 + 2 * series.Length) * pointerSize; + int totalSize = gcDescSize + MethodTableLayout.Size; + + MockMemorySpace.HeapFragment fragment = TypeSystemAllocator.Allocate((ulong)totalSize, $"GCDesc+MethodTable '{name}'"); + + // Write series entries. The highest series (index 0) lives closest to the MT (highest address), + // and the lowest series (index N-1) lives farthest from the MT (lowest address). + // So series[i] goes at fragment offset (N-1-i)*2*pointerSize. + for (int i = 0; i < series.Length; i++) + { + int seriesBase = (series.Length - 1 - i) * 2 * pointerSize; + Builder.TargetTestHelpers.WritePointer(fragment.Data.AsSpan(seriesBase, pointerSize), series[i].Size); + Builder.TargetTestHelpers.WritePointer(fragment.Data.AsSpan(seriesBase + pointerSize, pointerSize), series[i].Offset); + } + + // Write NumSeries immediately before the MT + Builder.TargetTestHelpers.WritePointer( + fragment.Data.AsSpan(series.Length * 2 * pointerSize, pointerSize), + (ulong)series.Length); + + // The MockMethodTable lives at offset gcDescSize within the combined fragment + ulong mtAddress = fragment.Address + (ulong)gcDescSize; + MockMethodTable mt = MethodTableLayout.Create(fragment.Data.AsMemory(gcDescSize, MethodTableLayout.Size), mtAddress); + mt.BaseSize = baseSize; + return mt; + } + + /// + /// Allocates a method table together with a value-class (repeating) CGCDesc immediately before it. + /// + /// Descriptive name for the allocation. + /// Value to store in the method table's BaseSize field. + /// The startoffset field of the CGCDescSeries (offset from object start). + /// + /// The val_serie_item entries, each as (nptrs, skip). + /// Index 0 corresponds to val_serie[0] (overlapping seriessize in the union). + /// + /// + /// A whose address is after the GCDesc bytes. + /// The caller is responsible for setting additional method table flags (e.g. + /// ContainsGCPointers = 0x01000000) and linking to an EEClass. + /// + internal MockMethodTable AddMethodTableWithValueClassGCDesc(string name, uint baseSize, ulong startOffset, (uint Nptrs, uint Skip)[] valSeries) + { + TargetTestHelpers helpers = Builder.TargetTestHelpers; + int pointerSize = helpers.PointerSize; + int halfSize = pointerSize / 2; + + // Value-class GCDesc layout (each slot is pointer-sized unless noted): + // [ val_serie[N-1] ] <- lowest address (fragment start) + // ... + // [ val_serie[0] ] <- overlaps seriessize in CGCDescSeries union + // [ startoffset ] <- one ptrSize slot + // [ NumSeries (-N) ] <- one ptrSize slot (negative) + // [ MethodTable data starts here ] + // + // ComputeSizeRepeating = sizeof(size_t) + sizeof(CGCDescSeries) + (N-1)*sizeof(val_serie_item) + // = ptrSize + 2*ptrSize + (N-1)*ptrSize = (N+2)*ptrSize + int gcDescSize = (valSeries.Length + 2) * pointerSize; + int totalSize = gcDescSize + MethodTableLayout.Size; + + MockMemorySpace.HeapFragment fragment = TypeSystemAllocator.Allocate((ulong)totalSize, $"ValueClassGCDesc+MethodTable '{name}'"); + + // Write val_serie items. val_serie[0] is closest to MT (highest address in the GCDesc region), + // val_serie[N-1] is farthest (lowest address). + // Each val_serie_item is { HALF_SIZE_T nptrs; HALF_SIZE_T skip; } + for (int i = 0; i < valSeries.Length; i++) + { + int itemOffset = (valSeries.Length - 1 - i) * pointerSize; + if (pointerSize == sizeof(uint)) + { + helpers.Write(fragment.Data.AsSpan(itemOffset, halfSize), (ushort)valSeries[i].Nptrs); + helpers.Write(fragment.Data.AsSpan(itemOffset + halfSize, halfSize), (ushort)valSeries[i].Skip); + } + else + { + helpers.Write(fragment.Data.AsSpan(itemOffset, halfSize), valSeries[i].Nptrs); + helpers.Write(fragment.Data.AsSpan(itemOffset + halfSize, halfSize), valSeries[i].Skip); + } + } + + // Write startoffset + helpers.WritePointer(fragment.Data.AsSpan(valSeries.Length * pointerSize, pointerSize), startOffset); + + // Write NumSeries as a negative value (-N) + long negativeCount = -valSeries.Length; + if (pointerSize == sizeof(uint)) + { + helpers.Write(fragment.Data.AsSpan((valSeries.Length + 1) * pointerSize, pointerSize), (int)negativeCount); + } + else + { + helpers.WritePointer(fragment.Data.AsSpan((valSeries.Length + 1) * pointerSize, pointerSize), unchecked((ulong)negativeCount)); + } + + // The MockMethodTable lives at offset gcDescSize within the combined fragment + ulong mtAddress = fragment.Address + (ulong)gcDescSize; + MockMethodTable mt = MethodTableLayout.Create(fragment.Data.AsMemory(gcDescSize, MethodTableLayout.Size), mtAddress); + mt.BaseSize = baseSize; + return mt; + } } } From 34c50585cfb47c8235098f33df62aa4b1cea99b1 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 09:44:43 -0700 Subject: [PATCH 041/115] Update skip_reason to clarify cancelled leg scenario (#127479) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Improves the `skip_reason` message set on pipelines with zero test results to hint at the most common root cause: a cancelled leg. ### Changes - **`extract_failed_tests.py`**: Updated `skip_reason` string from `'Build failed but no test failures detected via Test Results API'` to `'Build failed but no test failures detected via Test Results API, e.g., due to a cancelled leg'`
Original prompt > Update the `skip_reason` string on line 267 of `.github/skills/ci-pipeline-monitor/scripts/extract_failed_tests.py` in the `dotnet/runtime` repository. > > Change the existing string from: > > ``` > Build failed but no test failures detected via Test Results API > ``` > > to the exact string: > > ``` > Build failed but no test failures detected via Test Results API, e.g., due to a cancelled leg > ``` > > The line currently reads: > > ```python > "UPDATE pipelines SET result = 'inconclusive', skip_reason = 'Build failed but no test failures detected via Test Results API' WHERE name = ?", > ``` > > After the change, it should read: > > ```python > "UPDATE pipelines SET result = 'inconclusive', skip_reason = 'Build failed but no test failures detected via Test Results API, e.g., due to a cancelled leg' WHERE name = ?", > ``` > > Only this single line needs to be modified. Do not change any other occurrences or files unless an identical duplicate of this exact string exists elsewhere in the same script and clearly needs to be kept consistent. > > The following is the prior conversation context from the user's chat exploration (may be truncated): > > User: File a PR to change this line to set skip_reason to Build failed but no test failures detected via Test Results API, e.g., due to a > cancelled leg. > > Assistant: Waiting for your response to the dialog before proceeding with creating the pull request. > > User: @Copilot Dismissed Confirmation: Confirm agent session > > Assistant: Understood — I won't create the PR. Let me know if you'd like me to proceed with it later. > > User: It should be exact string "Build failed but no test failures detected via Test Results API, e.g., due to a cancelled leg" > > Assistant: I've started a coding agent session to open the PR updating the `skip_reason` string to `"Build failed but no test failures detected via Test Results API, e.g., due to a cancelled leg"`. > >
*This pull request was created from Copilot chat.* > --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: JulieLeeMSFT <63486087+JulieLeeMSFT@users.noreply.github.com> --- .../skills/ci-pipeline-monitor/scripts/extract_failed_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/skills/ci-pipeline-monitor/scripts/extract_failed_tests.py b/.github/skills/ci-pipeline-monitor/scripts/extract_failed_tests.py index b981fca7665a3f..dacac035c6a0d0 100644 --- a/.github/skills/ci-pipeline-monitor/scripts/extract_failed_tests.py +++ b/.github/skills/ci-pipeline-monitor/scripts/extract_failed_tests.py @@ -264,7 +264,7 @@ def main(): # Mark pipelines with 0 test results as inconclusive for name in zero_result_pipelines: conn.execute( - "UPDATE pipelines SET result = 'inconclusive', skip_reason = 'Build failed but no test failures detected via Test Results API' WHERE name = ?", + "UPDATE pipelines SET result = 'inconclusive', skip_reason = 'Build failed but no test failures detected via Test Results API, e.g., due to a cancelled leg' WHERE name = ?", (name,) ) if zero_result_pipelines: From a5ad3cf537bd9fa7d4042e0cccdc821c01703ee8 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:56:47 +0000 Subject: [PATCH 042/115] Reduce thread count on 32-bit in GetTotalAllocatedBytes.TestAnotherThread (#127583) `GC/API/GC/GetTotalAllocatedBytes` intermittently fails with `OutOfMemoryException` on linux-arm Checked jitstress: rapidly creating 1000 threads exhausts the 32-bit address space. ## Changes - **`src/tests/GC/API/GC/GetTotalAllocatedBytes.cs`** - `TestAnotherThread`: cap the inner thread-creation loop at 100 on 32-bit (`IntPtr.Size == 4`); 64-bit keeps the original 1000. - Remove the `[ActiveIssue(..., IsArm)]` suppression so the test runs on 32-bit ARM again. ```csharp // 1000 quickly created threads can be too many for a 32-bit environment, so reduce on 32-bit. int threadCount = IntPtr.Size == 4 ? 100 : 1000; tsk = Task.Run(() => { for (int i = 0; i < threadCount; i++) { ... } }); ``` > [!NOTE] > This PR description and the contained changes were generated with assistance from GitHub Copilot. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: VSadov <8218165+VSadov@users.noreply.github.com> --- src/tests/GC/API/GC/GetTotalAllocatedBytes.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/tests/GC/API/GC/GetTotalAllocatedBytes.cs b/src/tests/GC/API/GC/GetTotalAllocatedBytes.cs index c6369889ca80b1..13ef35dbf46742 100644 --- a/src/tests/GC/API/GC/GetTotalAllocatedBytes.cs +++ b/src/tests/GC/API/GC/GetTotalAllocatedBytes.cs @@ -110,8 +110,11 @@ public static void TestAnotherThread() { object lck = new object(); + // 1000 quickly created threads can be too many for a 32-bit environment, so reduce on 32-bit. + int threadCount = IntPtr.Size == 4 ? 100 : 1000; + tsk = Task.Run(() => { - for (int i = 0; i < 1000; i++) + for (int i = 0; i < threadCount; i++) { Thread thd = new Thread(() => { lock (lck) @@ -177,7 +180,6 @@ public static void TestLohSohConcurrently() } [ActiveIssue("needs triage", TestRuntimes.Mono)] - [ActiveIssue("https://github.com/dotnet/runtime/issues/121482", typeof(TestLibrary.Utilities), nameof(TestLibrary.Utilities.IsArm))] [Fact] public static void TestEntryPoint() { From 86eee16b708d4b83d0c45786b7edcf36e31cdc79 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Thu, 30 Apr 2026 18:50:19 +0100 Subject: [PATCH 043/115] Change unique stack trace GC flag from 0xF to 0x10 (#127612) Updated the unique stack trace GC flag from 0xF to 0x10. Looks like this was a typo, gcenv.h uses 16 / 0x10 for GCSTRESS_UNIQUE. --- docs/design/coreclr/jit/investigate-stress.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design/coreclr/jit/investigate-stress.md b/docs/design/coreclr/jit/investigate-stress.md index a3b09019835530..0a70f8ae6d4410 100644 --- a/docs/design/coreclr/jit/investigate-stress.md +++ b/docs/design/coreclr/jit/investigate-stress.md @@ -22,7 +22,7 @@ Enabling GC Hole Stress causes GCs to always occur in specific locations and tha - **0x2** – GC on transitions to Preemptive GC. - **0x4** – GC on every allowable JITed instr. - **0x8** – GC on every allowable R2R instr. -- **0xF** – GC only on a unique stack trace. +- **0x10** – GC only on a unique stack trace. ### Common combinations From 4f111f8c99ae3fae4303fe87d231216955abeb94 Mon Sep 17 00:00:00 2001 From: Steve Molloy Date: Thu, 30 Apr 2026 12:00:15 -0700 Subject: [PATCH 044/115] Implement XmlSerializer end element handling workaround and tests (#126765) This pull request improves handling of XML fragments over streaming transports, particularly when deserializing multiple root elements. The changes add a workaround (enabled by default) to avoid unnecessary reads after a top-level end element, preventing timeouts or exceptions when more data is not immediately available. An AppContext opt-out is also included. The PR also adds comprehensive tests and supporting infrastructure to validate and control this behavior. **XML Serializer Compatibility and Behavior Improvements:** * Added a new `UseXmlSerializerReadEndElementWorkaround` switch (default: true) to `LocalAppContextSwitches` to control workaround behavior for reading end elements in XML fragments. * Updated `XmlSerializationReader.ReadEndElement()` to respect the new switch, avoiding extra reads after a top-level end element when enabled, which prevents blocking or timeouts in streaming/fragment scenarios. * Introduced `AdvancePastTopLevelEndElementIfNeeded(XmlReader)` utility and invoked it in `XmlSerializer` methods (`GetMapping`, `CanDeserialize`) to ensure correct reader advancement when the workaround is enabled. [[1]](diffhunk://#diff-fa07acc29a1aed946fd7f4fca4b198bc71c7bb4204019cd407b9447882af374bR489-R490) [[2]](diffhunk://#diff-fa07acc29a1aed946fd7f4fca4b198bc71c7bb4204019cd407b9447882af374bR547-R548) [[3]](diffhunk://#diff-fa07acc29a1aed946fd7f4fca4b198bc71c7bb4204019cd407b9447882af374bR846-R855) **Testing and Infrastructure Enhancements:** * Added new tests to `XmlSerializerTests.cs` to verify deserialization of XML fragments with and without the workaround, including a test that disables the switch and expects a timeout exception. * Implemented `BlockingAfterBufferStream`, a custom `Stream` that simulates blocking behavior after buffer exhaustion, to facilitate robust testing of streaming scenarios. * Improved `XmlSerializerAppContextSwitchScope` to properly unset AppContext switches and clear cached values, ensuring test isolation and reliability. Fixes: #47371 --- .../Xml/Core/LocalAppContextSwitches.cs | 10 ++ .../Serialization/XmlSerializationReader.cs | 20 +++- .../System/Xml/Serialization/XmlSerializer.cs | 14 +++ .../tests/XmlSerializer/XmlSerializerTests.cs | 109 +++++++++++++++++- 4 files changed, 149 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Core/LocalAppContextSwitches.cs b/src/libraries/System.Private.Xml/src/System/Xml/Core/LocalAppContextSwitches.cs index b41f05be289cce..48042a88c5b0df 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Core/LocalAppContextSwitches.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Core/LocalAppContextSwitches.cs @@ -39,6 +39,16 @@ public static bool IgnoreKindInUtcTimeSerialization } } + private static int s_useXmlSerializerReadEndElementWorkaround; + public static bool UseXmlSerializerReadEndElementWorkaround + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return SwitchesHelpers.GetCachedSwitchValue("Switch.System.Xml.UseXmlSerializerReadEndElementWorkaround", ref s_useXmlSerializerReadEndElementWorkaround, defaultValue: true); + } + } + private static int s_allowXsdTimeToTimeOnlyWithOffsetLoss; public static bool AllowXsdTimeToTimeOnlyWithOffsetLoss { diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cs b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cs index 79741d5e849316..df3b1134f08887 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cs @@ -1892,8 +1892,24 @@ protected void AddReadCallback(string name, string ns, Type type, XmlSerializati protected void ReadEndElement() { while (_r.NodeType == XmlNodeType.Whitespace) _r.Skip(); - if (_r.NodeType == XmlNodeType.None) _r.Skip(); - else _r.ReadEndElement(); + + if (LocalAppContextSwitches.UseXmlSerializerReadEndElementWorkaround) + { + if (_r.NodeType == XmlNodeType.None) + return; + + // Avoid forcing the reader to pull one more token after completing a top-level element. + // In fragment scenarios over streaming transports, additional data may not be immediately + // available even though deserialization of the current element is already complete. + if (_r.NodeType == XmlNodeType.EndElement && _r.Depth == 0) + return; + } + else if (_r.NodeType == XmlNodeType.None) + { + _r.Skip(); + } + + _r.ReadEndElement(); } private object ReadXmlNodes(bool elementCanBeType) diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializer.cs b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializer.cs index 51f3a7d779824f..e42e02d10f245e 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializer.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializer.cs @@ -486,6 +486,8 @@ private XmlMapping GetMapping() events.sender = this; try { + AdvancePastTopLevelEndElementIfNeeded(xmlReader); + if (_primitiveType != null) { if (encodingStyle != null && encodingStyle.Length > 0) @@ -542,6 +544,8 @@ private static bool ShouldUseReflectionBasedSerialization(XmlMapping mapping) public virtual bool CanDeserialize(XmlReader xmlReader) { + AdvancePastTopLevelEndElementIfNeeded(xmlReader); + if (_primitiveType != null) { TypeDesc typeDesc = (TypeDesc)TypeScope.PrimtiveTypes[_primitiveType]!; @@ -839,6 +843,16 @@ internal void SetTempAssembly(TempAssembly tempAssembly, XmlMapping mapping) _typedSerializer = true; } + private static void AdvancePastTopLevelEndElementIfNeeded(XmlReader xmlReader) + { + if (LocalAppContextSwitches.UseXmlSerializerReadEndElementWorkaround && + xmlReader.NodeType == XmlNodeType.EndElement && + xmlReader.Depth == 0) + { + xmlReader.Read(); + } + } + private static XmlTypeMapping? GetKnownMapping(Type type, string? ns) { if (ns != null && ns != string.Empty) diff --git a/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs b/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs index fc285987efc134..1451e7407d8148 100644 --- a/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs +++ b/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs @@ -950,6 +950,52 @@ public static void Xml_DifferentSerializeDeserializeOverloads() } } + [Fact] + public static void Xml_DeserializeFragmentsFromXmlReader() + { + const string payload = "one1two2"; + XmlSerializer serializer = new XmlSerializer(typeof(SimpleType)); + byte[] data = Encoding.UTF8.GetBytes(payload); + + // "Switch.System.Xml.UseXmlSerializerReadEndElementWorkaround" default should be true + using var stream = new BlockingAfterBufferStream(data); + using var reader = XmlReader.Create(stream, new XmlReaderSettings { ConformanceLevel = ConformanceLevel.Fragment }); + + Assert.True(serializer.CanDeserialize(reader)); + + SimpleType first = (SimpleType)serializer.Deserialize(reader); + Assert.Equal("one", first.P1); + Assert.Equal(1, first.P2); + + Assert.True(serializer.CanDeserialize(reader)); + + SimpleType second = (SimpleType)serializer.Deserialize(reader); + Assert.Equal("two", second.P1); + Assert.Equal(2, second.P2); + } + + [Fact] + public static void Xml_DeserializeFragmentsFromXmlReader_CanDisableWorkaround() + { + const string switchName = "Switch.System.Xml.UseXmlSerializerReadEndElementWorkaround"; + const string payload = "one1two2"; + XmlSerializer serializer = new XmlSerializer(typeof(SimpleType)); + byte[] data = Encoding.UTF8.GetBytes(payload); + + using var compatSwitch = new XmlSerializerAppContextSwitchScope(switchName, false); + Assert.False(compatSwitch.CurrentValue); + + using var stream = new BlockingAfterBufferStream(data); + using var reader = XmlReader.Create(stream, new XmlReaderSettings { ConformanceLevel = ConformanceLevel.Fragment }); + + SimpleType first = (SimpleType)serializer.Deserialize(reader); + Assert.Equal("one", first.P1); + Assert.Equal(1, first.P2); + + InvalidOperationException exception = Assert.Throws(() => serializer.Deserialize(reader)); + Assert.IsType(exception.InnerException); + } + [Fact] public static void Xml_TypeWithTimeSpanProperty() { @@ -3115,6 +3161,45 @@ private static string FormatTimeString(DateTime time, bool ignoreUtc) => time.ToString($"HH:mm:ss.fffffff{(!ignoreUtc && time.Kind == DateTimeKind.Utc ? "Z" : "zzzzzz")}", CultureInfo.InvariantCulture); } +internal sealed class BlockingAfterBufferStream : Stream +{ + private readonly byte[] _buffer; + private int _position; + + public BlockingAfterBufferStream(byte[] buffer) + { + _buffer = buffer; + } + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => _buffer.Length; + public override long Position + { + get => _position; + set => throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (_position < _buffer.Length) + { + int toCopy = Math.Min(count, _buffer.Length - _position); + Array.Copy(_buffer, _position, buffer, offset, toCopy); + _position += toCopy; + return toCopy; + } + + throw new TimeoutException("Simulated Exception - No additional data is available."); + } + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public override void Flush() { } +} + internal sealed class XmlSerializerAppContextSwitchScope : IDisposable { private readonly string _name; @@ -3139,11 +3224,31 @@ public void Dispose() if (_hadValue) AppContext.SetSwitch(_name, _originalValue); else - // There's no "unset", so pick a default or false - AppContext.SetSwitch(_name, false); + UnsetSwitch(_name); + ClearCachedSwitch(_cachedName); } + private static void UnsetSwitch(string name) + { + const BindingFlags Flags = BindingFlags.NonPublic | BindingFlags.Static; + + if (typeof(AppContext).GetField("s_switches", Flags)?.GetValue(null) is IDictionary switches) + { + lock (switches) + { + switches.Remove(name); + } + } + + if (typeof(AppContext).GetField("s_dataStore", Flags)?.GetValue(null) is IDictionary dataStore) + { + lock (dataStore) + { + dataStore.Remove(name); + } + } + } private static void ClearCachedSwitch(string name) { Type t = Type.GetType("System.Xml.LocalAppContextSwitches, System.Private.Xml"); From 0fa45b37b1c505faf0d02a6ad175f16ab10e4795 Mon Sep 17 00:00:00 2001 From: Jan Kotas Date: Thu, 30 Apr 2026 12:05:59 -0700 Subject: [PATCH 045/115] Improve GCHeap::Promote debug validation (#127595) - Mirrors the fix from #119403 in x86 GCInfo decoder - Improve GCHeap::Promote debug validation to repro this class of issues deterministically Fixes #127581 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- src/coreclr/gc/background.cpp | 22 +++++++++++++++++----- src/coreclr/gc/interface.cpp | 16 +++++++++++----- src/coreclr/vm/gc_unwind_x86.inl | 8 ++++++-- 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/coreclr/gc/background.cpp b/src/coreclr/gc/background.cpp index 3fc82188afa489..a1acaeced85432 100644 --- a/src/coreclr/gc/background.cpp +++ b/src/coreclr/gc/background.cpp @@ -544,17 +544,23 @@ void gc_heap::background_promote (Object** ppObject, ScanContext* sc, uint32_t f uint8_t* o = (uint8_t*)*ppObject; - if (!is_in_find_object_range (o)) - { - return; - } - #ifdef DEBUG_DestroyedHandleValue // we can race with destroy handle during concurrent scan if (o == (uint8_t*)DEBUG_DestroyedHandleValue) return; #endif //DEBUG_DestroyedHandleValue + if (!is_in_find_object_range (o)) + { +#ifdef _DEBUG + if ((o != NULL) && !(flags & GC_CALL_INTERIOR)) + { + ((CObjectHeader*)o)->Validate(); + } +#endif //_DEBUG + return; + } + HEAP_FROM_THREAD; gc_heap* hp = gc_heap::heap_of (o); @@ -2741,6 +2747,12 @@ void gc_heap::background_promote_callback (Object** ppObject, ScanContext* sc, if (!is_in_find_object_range (o)) { +#ifdef _DEBUG + if ((o != NULL) && !(flags & GC_CALL_INTERIOR)) + { + ((CObjectHeader*)o)->Validate(); + } +#endif //_DEBUG return; } diff --git a/src/coreclr/gc/interface.cpp b/src/coreclr/gc/interface.cpp index acdae1415cb403..980b595b993476 100644 --- a/src/coreclr/gc/interface.cpp +++ b/src/coreclr/gc/interface.cpp @@ -1054,17 +1054,23 @@ void GCHeap::Promote(Object** ppObject, ScanContext* sc, uint32_t flags) uint8_t* o = (uint8_t*)*ppObject; - if (!gc_heap::is_in_find_object_range (o)) - { - return; - } - #ifdef DEBUG_DestroyedHandleValue // we can race with destroy handle during concurrent scan if (o == (uint8_t*)DEBUG_DestroyedHandleValue) return; #endif //DEBUG_DestroyedHandleValue + if (!gc_heap::is_in_find_object_range (o)) + { +#ifdef _DEBUG + if ((o != NULL) && !(flags & GC_CALL_INTERIOR)) + { + ((CObjectHeader*)o)->Validate(); + } +#endif //_DEBUG + return; + } + HEAP_FROM_THREAD; gc_heap* hp = gc_heap::heap_of (o); diff --git a/src/coreclr/vm/gc_unwind_x86.inl b/src/coreclr/vm/gc_unwind_x86.inl index 1142df47a0527b..54b9c54700ea95 100644 --- a/src/coreclr/vm/gc_unwind_x86.inl +++ b/src/coreclr/vm/gc_unwind_x86.inl @@ -3082,10 +3082,14 @@ bool EnumGcRefsX86(PREGDISPLAY pContext, } #endif - /* Are we in the prolog or epilog of the method? */ + /* Are we in the prolog or epilog of the method, or is this a + * non-interruptible method that will not resume execution at this offset? + * In either case, GC slots may not be initialized at the current offset + * and we can simply skip all reporting. */ if (info.prologOffs != hdrInfo::NOT_IN_PROLOG || - info.epilogOffs != hdrInfo::NOT_IN_EPILOG) + info.epilogOffs != hdrInfo::NOT_IN_EPILOG || + ((flags & ExecutionAborted) && !info.interruptible)) { #if !DUMP_PTR_REFS From 550500a978b784658a04110d49b3335dcacf33e0 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:04:28 -0400 Subject: [PATCH 046/115] [cDAC] Use locally-built crossgen2 for dump test debuggees (#127561) > [!NOTE] > This PR was created with the assistance of GitHub Copilot (AI-generated content). ## Problem The `AsyncContinuationDumpTests` (and potentially other R2R dump tests) fail in CI because the debuggee apps are R2R-compiled with the SDK's bundled crossgen2 instead of the locally-built one. When the locally-built runtime removes or changes types between previews (e.g. PR #127336 removing `ExecutionAndSyncBlockStore`), the SDK's crossgen2 emits R2R thunks referencing types that no longer exist, causing `TypeLoadException` or null globals at runtime. ## Root Cause Two issues prevent the locally-built crossgen2 from being used: 1. **NuGet pack layout mismatch**: The SDK's `ResolveReadyToRunCompilers` task expects crossgen2 at `PackagePath/tools/crossgen2.exe` (NuGet pack layout), but the local build produces a flat layout with `crossgen2.exe` at the root of `Crossgen2InBuildDir`. `targetingpacks.targets` alone cannot fix this because it only updates `PackageDirectory` without restructuring the layout. 2. **Missing property propagation**: The child `dotnet publish` process doesn't inherit `RuntimeConfiguration`, `TargetArchitecture`, `TargetOS`, or `BuildArchitecture` from the outer build, causing crossgen2 and runtime pack paths to resolve incorrectly (especially in cross-build scenarios like building on linux-x64 for linux-arm). ## Fix ### `Debuggees/Directory.Build.targets` - Import `targetingpacks.targets` for runtime/targeting pack resolution - Import `tests.readytorun.targets` via `AfterMicrosoftNETSdkTargets` to override `ResolveReadyToRunCompilers` after the SDK defines it, pointing directly at the locally-built crossgen2 ### `DumpTests.targets` - Add `_DebuggeeRuntimeConfig` property (falls back from `RuntimeConfiguration` -> `Configuration` -> `Debug`) - Add `_DebuggeePublishProps` to propagate `RuntimeConfiguration`, `TargetArchitecture`, `TargetOS`, and `BuildArchitecture` to child `dotnet publish` calls ### `prepare-cdac-helix-steps.yml` - Add `runtimeConfiguration` parameter (default: `Checked`) and pass it to `BuildDebuggeesOnly` so crossgen2 is found under the correct `Checked` artifacts path Co-authored-by: Max Charlamb Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cdac/prepare-cdac-helix-steps.yml | 2 ++ .../Debuggees/Directory.Build.targets | 16 ++++++++++++++ .../cdac/tests/DumpTests/DumpTests.targets | 22 +++++++++++++++++-- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/eng/pipelines/cdac/prepare-cdac-helix-steps.yml b/eng/pipelines/cdac/prepare-cdac-helix-steps.yml index ce8e01605987ae..c3b61807915e56 100644 --- a/eng/pipelines/cdac/prepare-cdac-helix-steps.yml +++ b/eng/pipelines/cdac/prepare-cdac-helix-steps.yml @@ -6,6 +6,7 @@ parameters: buildDebuggees: true skipDebuggeeCopy: false + runtimeConfiguration: 'Checked' steps: - ${{ if parameters.buildDebuggees }}: @@ -14,6 +15,7 @@ steps: /t:BuildDebuggeesOnly /p:Configuration=$(_BuildConfig) /p:TargetArchitecture=$(archType) + /p:RuntimeConfiguration=${{ parameters.runtimeConfiguration }} -bl:$(Build.SourcesDirectory)/artifacts/log/BuildDebuggees.binlog displayName: 'Build Debuggees' diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/Directory.Build.targets b/src/native/managed/cdac/tests/DumpTests/Debuggees/Directory.Build.targets index 60cbea1938b738..8728cd5eac9853 100644 --- a/src/native/managed/cdac/tests/DumpTests/Debuggees/Directory.Build.targets +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/Directory.Build.targets @@ -1,6 +1,22 @@ + + + + + $(AfterMicrosoftNETSdkTargets);$(RepositoryEngineeringDir)testing\tests.readytorun.targets + + diff --git a/src/native/managed/cdac/tests/DumpTests/DumpTests.targets b/src/native/managed/cdac/tests/DumpTests/DumpTests.targets index 1485177bf16027..ae8228ffa7f7fd 100644 --- a/src/native/managed/cdac/tests/DumpTests/DumpTests.targets +++ b/src/native/managed/cdac/tests/DumpTests/DumpTests.targets @@ -39,6 +39,24 @@ $([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)', 'Debuggees')) <_DotNetExe>$([MSBuild]::NormalizePath('$(RepoRoot)', '.dotnet', 'dotnet$(ExeSuffix)')) + + <_DebuggeeRuntimeConfig Condition="'$(_DebuggeeRuntimeConfig)' == ''">$(RuntimeConfiguration) + <_DebuggeeRuntimeConfig Condition="'$(_DebuggeeRuntimeConfig)' == ''">$(Configuration) + <_DebuggeeRuntimeConfig Condition="'$(_DebuggeeRuntimeConfig)' == ''">Debug + + + <_DebuggeePublishProps>/p:RuntimeConfiguration=$(_DebuggeeRuntimeConfig) + <_DebuggeePublishProps Condition="'$(TargetArchitecture)' != ''">$(_DebuggeePublishProps) /p:TargetArchitecture=$(TargetArchitecture) + <_DebuggeePublishProps Condition="'$(TargetOS)' != ''">$(_DebuggeePublishProps) /p:TargetOS=$(TargetOS) + <_DebuggeePublishProps Condition="'$(BuildArchitecture)' != ''">$(_DebuggeePublishProps) /p:BuildArchitecture=$(BuildArchitecture) @@ -167,7 +185,7 @@ <_PublishOutDir>$([MSBuild]::NormalizeDirectory('$(RepoRoot)', 'artifacts', 'bin', 'DumpTests', '$(DebuggeeName)', '$(DebuggeeConfiguration)', '$(NetCoreAppCurrent)')) - + @@ -254,7 +272,7 @@ <_DebuggeeCsproj>$([MSBuild]::NormalizePath('$(_DebuggeeDir)', '$(DebuggeeName).csproj')) - From c77572bbcecf702b829460784fdfc1e02dd81592 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Thu, 30 Apr 2026 13:58:18 -0700 Subject: [PATCH 047/115] Fix Wasm ArgIterator SizeOfArgStack for methods with only hidden args (#127532) The ForceSigWalk method had two bugs in its Wasm-specific path for accounting for hidden arguments (this, retbuf, generic context, etc.) when no named arguments are present: 1. The check 'maxOffset == 0' could never be true because maxOffset is initialized to OffsetOfArgs (8 on Wasm32). Changed to compare against OffsetOfArgs. 2. The fallback 'maxOffset = _wasmOfsStack' was incorrect because _wasmOfsStack is relative to OffsetOfArgs, but maxOffset is an absolute offset. Changed to 'OffsetOfArgs + _wasmOfsStack'. These bugs caused GCRefMapBuilder to allocate a zero-length fake stack for methods with only unnamed arguments (e.g. parameterless instance methods), leading to IndexOutOfRangeException when writing the 'this' pointer GC ref at ThisOffset. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Compiler/DependencyAnalysis/ReadyToRun/ArgIterator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ArgIterator.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ArgIterator.cs index f98f8990a65d98..c6b25756045c50 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ArgIterator.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ArgIterator.cs @@ -1721,10 +1721,10 @@ private void ForceSigWalk() } } - if (maxOffset == 0 && _transitionBlock.IsWasm32) + if (maxOffset == _transitionBlock.OffsetOfArgs && _transitionBlock.IsWasm32) { // Wasm puts all arguments on the stack, even the unnamed ones like the param registers, this pointer and async continuation. If we didn't see any named arguments, then we need to account for the unnamed ones here. - maxOffset = _wasmOfsStack; + maxOffset = _transitionBlock.OffsetOfArgs + _wasmOfsStack; } // Clear the iterator started flag From 3dd3c881ad4c5803876fa95555a2baf3ff3780cf Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 21:31:11 +0000 Subject: [PATCH 048/115] Remove corert#2785 BadImageFormatException workaround from ILCompiler (#127591) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the long-standing hack in `ILCompiler/Program.cs` that silently swallowed `BadImageFormatException` when loading input files — originally added to tolerate native DLLs mixed into CoreCLR test trees (corert#2785). That scenario no longer applies. --- src/coreclr/tools/aot/ILCompiler/Program.cs | 25 +-------------------- 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler/Program.cs b/src/coreclr/tools/aot/ILCompiler/Program.cs index 9786ffc489b473..45e0a2ff444800 100644 --- a/src/coreclr/tools/aot/ILCompiler/Program.cs +++ b/src/coreclr/tools/aot/ILCompiler/Program.cs @@ -129,30 +129,7 @@ public int Run() genericCycleDepthCutoff: Get(_command.MaxGenericCycleDepth), genericCycleBreadthCutoff: Get(_command.MaxGenericCycleBreadth)); - // - // TODO: To support our pre-compiled test tree, allow input files that aren't managed assemblies since - // some tests contain a mixture of both managed and native binaries. - // - // See: https://github.com/dotnet/corert/issues/2785 - // - // When we undo this hack, replace the foreach with - // typeSystemContext.InputFilePaths = _command.Result.GetValueForArgument(inputFilePaths); - // - Dictionary inputFilePaths = new Dictionary(); - foreach (var inputFile in _command.Result.GetValue(_command.InputFilePaths)) - { - try - { - var module = typeSystemContext.GetModuleFromPath(inputFile.Value); - inputFilePaths.Add(inputFile.Key, inputFile.Value); - } - catch (TypeSystemException.BadImageFormatException) - { - // Keep calm and carry on. - } - } - - typeSystemContext.InputFilePaths = inputFilePaths; + typeSystemContext.InputFilePaths = _command.Result.GetValue(_command.InputFilePaths); typeSystemContext.ReferenceFilePaths = Get(_command.ReferenceFiles); if (!typeSystemContext.InputFilePaths.ContainsKey(systemModuleName) && !typeSystemContext.ReferenceFilePaths.ContainsKey(systemModuleName)) From e971c0ad9289befbd7e4ebf2af4186b96f3bada8 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 30 Apr 2026 14:42:58 -0700 Subject: [PATCH 049/115] Fix ChaCha20Poly1305 IsSupported test on Azure Linux 4 --- .../tests/TestUtilities/System/PlatformDetection.Unix.cs | 1 + .../tests/ChaCha20Poly1305Tests.cs | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.Unix.cs b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.Unix.cs index 1383c8f7e062d6..156b6a24185940 100644 --- a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.Unix.cs +++ b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.Unix.cs @@ -31,6 +31,7 @@ public static partial class PlatformDetection public static bool IsLinuxBionic => IsBionic(); public static bool IsRedHatFamily => IsRedHatFamilyAndVersion(); public static bool IsAzureLinux => IsDistroAndVersionOrHigher("azurelinux", 3); + public static bool IsAzureLinux4OrHigher => IsDistroAndVersionOrHigher("azurelinux", 4); public static bool IsMonoLinuxArm64 => IsMonoRuntime && IsLinux && IsArm64Process; public static bool IsNotMonoLinuxArm64 => !IsMonoLinuxArm64; diff --git a/src/libraries/System.Security.Cryptography/tests/ChaCha20Poly1305Tests.cs b/src/libraries/System.Security.Cryptography/tests/ChaCha20Poly1305Tests.cs index 43462335b888b4..ff618943d0546f 100644 --- a/src/libraries/System.Security.Cryptography/tests/ChaCha20Poly1305Tests.cs +++ b/src/libraries/System.Security.Cryptography/tests/ChaCha20Poly1305Tests.cs @@ -480,8 +480,9 @@ public static void CheckIsSupported() } else if (PlatformDetection.IsAzureLinux) { - // Though Azure Linux uses OpenSSL, they build OpenSSL without ChaCha20-Poly1305. - expectedIsSupported = false; + // Though Azure Linux uses OpenSSL, Azure Linux 3 built OpenSSL with ChaCha20Poly1305 disabled. + // It was re-enabled in Azure Linux 4. + expectedIsSupported = PlatformDetection.IsAzureLinux4OrHigher; } else if (PlatformDetection.OpenSslPresentOnSystem && PlatformDetection.IsOpenSslSupported) { From 9ef2b5e33807f72fecf426e44ca1f06efd25723b Mon Sep 17 00:00:00 2001 From: Barbara Rosiak <76071368+barosiak@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:55:15 -0700 Subject: [PATCH 050/115] [cDAC] Implement GetModuleSimpleName for cDAC (#127415) ## Summary Implement `GetModuleSimpleName` on `DacDbiImpl` using the `ILoader` contract, replacing legacy-only delegation. ## Changes - `DacDbiImpl.cs` - Implement `GetModuleSimpleName` via `ILoader.GetSimpleName` - `DacDbiLoaderDumpTests.cs` - Add dump test verifying all modules return `S_OK` with a non-empty simple name --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rcj1 --- docs/design/datacontracts/Loader.md | 9 ++--- .../Contracts/ILoader.cs | 2 +- .../Contracts/Loader_1.cs | 13 ++----- .../ClrDataModule.cs | 3 +- .../Dbi/DacDbiImpl.cs | 31 +++++++++++++++- .../NativeStringHolder.cs | 4 +- .../DumpTests/DacDbi/DacDbiLoaderDumpTests.cs | 28 +++++++++++++- src/native/managed/cdac/tests/LoaderTests.cs | 37 +++++++++++-------- 8 files changed, 88 insertions(+), 39 deletions(-) rename src/native/managed/cdac/{tests/DumpTests/DacDbi => Microsoft.Diagnostics.DataContractReader.Legacy}/NativeStringHolder.cs (95%) diff --git a/docs/design/datacontracts/Loader.md b/docs/design/datacontracts/Loader.md index cefb2d8d5548e4..ee3ef4f1b30eea 100644 --- a/docs/design/datacontracts/Loader.md +++ b/docs/design/datacontracts/Loader.md @@ -80,7 +80,7 @@ IEnumerable GetInstantiatedMethods(ModuleHandle handle); bool IsProbeExtensionResultValid(ModuleHandle handle); ModuleFlags GetFlags(ModuleHandle handle); bool IsReadyToRun(ModuleHandle handle); -bool TryGetSimpleName(ModuleHandle handle, out string simpleName); +string GetSimpleName(ModuleHandle handle); string GetPath(ModuleHandle handle); string GetFileName(ModuleHandle handle); TargetPointer GetLoaderAllocator(ModuleHandle handle); @@ -622,14 +622,11 @@ ModuleFlags GetFlags(ModuleHandle handle) return GetFlags(target.Read(handle.Address + /* Module::Flags offset */)); } -bool TryGetSimpleName(ModuleHandle handle, out string simpleName) +string GetSimpleName(ModuleHandle handle) { TargetPointer simpleNameStart = target.ReadPointer(handle.Address + /* Module::SimpleName offset */); - if (simpleNameStart == TargetPointer.Null) - return false; byte[] simpleNameBytes = // Read from target starting at simpleNameStart until null terminator - simpleName = // convert to string, throw on invalid UTF-8 - return true; + return // convert to string, throw on invalid UTF-8 } string GetPath(ModuleHandle handle) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ILoader.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ILoader.cs index 96310c327c262b..76a07d7753696f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ILoader.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ILoader.cs @@ -103,7 +103,7 @@ public interface ILoader : IContract bool IsProbeExtensionResultValid(ModuleHandle handle) => throw new NotImplementedException(); ModuleFlags GetFlags(ModuleHandle handle) => throw new NotImplementedException(); bool IsReadyToRun(ModuleHandle handle) => throw new NotImplementedException(); - bool TryGetSimpleName(ModuleHandle handle, out string simpleName) => throw new NotImplementedException(); + string GetSimpleName(ModuleHandle handle) => throw new NotImplementedException(); string GetPath(ModuleHandle handle) => throw new NotImplementedException(); string GetFileName(ModuleHandle handle) => throw new NotImplementedException(); TargetPointer GetLoaderAllocator(ModuleHandle handle) => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs index cc517fbacb60ee..1a4ddc04445b03 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs @@ -447,17 +447,12 @@ bool ILoader.IsReadyToRun(ModuleHandle handle) return module.ReadyToRunInfo != TargetPointer.Null; } - bool ILoader.TryGetSimpleName(ModuleHandle handle, out string simpleName) + string ILoader.GetSimpleName(ModuleHandle handle) { - simpleName = string.Empty; Data.Module module = _target.ProcessedData.GetOrAdd(handle.Address); - if (module.SimpleName != TargetPointer.Null) - { - simpleName = _target.ReadUtf8String(module.SimpleName, strict: true); - return true; - } - else - return false; + return module.SimpleName != TargetPointer.Null + ? _target.ReadUtf8String(module.SimpleName, strict: true) + : string.Empty; } string ILoader.GetPath(ModuleHandle handle) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataModule.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataModule.cs index 552c484239d96e..5bdd38d8a1a782 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataModule.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataModule.cs @@ -460,8 +460,7 @@ int IXCLRDataModule.GetName(uint bufLen, uint* nameLen, char* name) *nameLen = 0; Contracts.ILoader loader = _target.Contracts.Loader; Contracts.ModuleHandle handle = loader.GetModuleHandleFromModulePtr(_address); - if (!loader.TryGetSimpleName(handle, out string result)) - throw new ArgumentException("Module does not have a simple name"); + string result = loader.GetSimpleName(handle); uint nameLenLocal = 0; OutputBufferHelpers.CopyStringToBuffer(name, bufLen, &nameLenLocal, result); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs index dc3f6b7d93cdb9..2ea47ab69e8f94 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs @@ -146,7 +146,36 @@ public int GetAppDomainFullName(ulong vmAppDomain, nint pStrName) } public int GetModuleSimpleName(ulong vmModule, nint pStrFilename) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.GetModuleSimpleName(vmModule, pStrFilename) : HResults.E_NOTIMPL; + { + int hr = HResults.S_OK; + string? cdacSimpleName = null; + try + { + Contracts.ILoader loader = _target.Contracts.Loader; + Contracts.ModuleHandle handle = loader.GetModuleHandleFromModulePtr(new TargetPointer(vmModule)); + cdacSimpleName = loader.GetSimpleName(handle); + hr = StringHolderAssignCopy(pStrFilename, cdacSimpleName); + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacy is not null) + { + using var legacyHolder = new NativeStringHolder(); + int hrLocal = _legacy.GetModuleSimpleName(vmModule, legacyHolder.Ptr); + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + { + Debug.Assert( + string.Equals(cdacSimpleName, legacyHolder.Value, System.StringComparison.Ordinal), + $"GetModuleSimpleName string mismatch - cDAC: '{cdacSimpleName}', DAC: '{legacyHolder.Value}'"); + } + } +#endif + return hr; + } public int GetAssemblyPath(ulong vmAssembly, nint pStrFilename, Interop.BOOL* pResult) { diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/NativeStringHolder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/NativeStringHolder.cs similarity index 95% rename from src/native/managed/cdac/tests/DumpTests/DacDbi/NativeStringHolder.cs rename to src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/NativeStringHolder.cs index 8bca8e70b1bb24..9cf51b96b66c26 100644 --- a/src/native/managed/cdac/tests/DumpTests/DacDbi/NativeStringHolder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/NativeStringHolder.cs @@ -4,7 +4,7 @@ using System; using System.Runtime.InteropServices; -namespace Microsoft.Diagnostics.DataContractReader.DumpTests; +namespace Microsoft.Diagnostics.DataContractReader.Legacy; /// /// Creates a native-memory object that mimics the C++ IStringHolder vtable layout. @@ -52,7 +52,7 @@ public NativeStringHolder() private int AssignCopyImpl(IntPtr thisPtr, IntPtr psz) { Value = Marshal.PtrToStringUni(psz); - return System.HResults.S_OK; + return HResults.S_OK; } public void Dispose() diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiLoaderDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiLoaderDumpTests.cs index 8b7c92ca26066f..d82ddc0c29dc0d 100644 --- a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiLoaderDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiLoaderDumpTests.cs @@ -10,11 +10,11 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; /// /// Dump-based integration tests for DacDbiImpl loader, assembly, and module methods. -/// Uses the MultiModule debuggee (full dump), which loads assemblies from multiple ALCs. +/// Uses the StackRefs debuggee (full dump). /// public class DacDbiLoaderDumpTests : DumpTestBase { - protected override string DebuggeeName => "MultiModule"; + protected override string DebuggeeName => "StackRefs"; private DacDbiImpl CreateDacDbi() => new DacDbiImpl(Target, legacyObj: null); private IEnumerable GetAllModules() @@ -137,6 +137,30 @@ public unsafe void GetModuleData_ReturnsValidFields(TestConfiguration config) Assert.True(testedAtLeastOne, "Expected at least one module in the dump"); } + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public void GetModuleSimpleName_ReturnsNonEmpty(TestConfiguration config) + { + InitializeDumpTest(config); + DacDbiImpl dbi = CreateDacDbi(); + ILoader loader = Target.Contracts.Loader; + + bool testedAtLeastOne = false; + foreach (ModuleHandle module in GetAllModules()) + { + TargetPointer moduleAddr = loader.GetModule(module); + + using var holder = new NativeStringHolder(); + int hr = dbi.GetModuleSimpleName(moduleAddr.Value, holder.Ptr); + Assert.Equal(System.HResults.S_OK, hr); + Assert.False(string.IsNullOrEmpty(holder.Value), "Module simple name should not be empty"); + Assert.Equal(loader.GetSimpleName(module), holder.Value); + + testedAtLeastOne = true; + } + Assert.True(testedAtLeastOne, "Expected at least one module in the dump"); + } + [ConditionalTheory] [MemberData(nameof(TestConfigurations))] public unsafe void IsModuleMapped_ReturnsValidResult(TestConfiguration config) diff --git a/src/native/managed/cdac/tests/LoaderTests.cs b/src/native/managed/cdac/tests/LoaderTests.cs index 5150460f4ef698..cd3b2be5f8cc8e 100644 --- a/src/native/managed/cdac/tests/LoaderTests.cs +++ b/src/native/managed/cdac/tests/LoaderTests.cs @@ -112,35 +112,40 @@ public void GetFileName(MockTarget.Architecture arch) [Theory] [ClassData(typeof(MockTarget.StdArch))] - public void TryGetSimpleName(MockTarget.Architecture arch) + public void GetSimpleName(MockTarget.Architecture arch) { string expected = "TestModule"; TargetPointer moduleAddr = TargetPointer.Null; - TargetPointer moduleAddrEmptyName = TargetPointer.Null; ILoader contract = CreateLoaderContract(arch, loader => { moduleAddr = loader.AddModule(simpleName: expected).Address; - moduleAddrEmptyName = loader.AddModule().Address; }); + Contracts.ModuleHandle handle = contract.GetModuleHandleFromModulePtr(moduleAddr); + string actual = contract.GetSimpleName(handle); + Assert.Equal(expected, actual); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetSimpleName_NullSimpleName(MockTarget.Architecture arch) + { + TargetPointer moduleAddr = TargetPointer.Null; + + ILoader contract = CreateLoaderContract(arch, loader => { - Contracts.ModuleHandle handle = contract.GetModuleHandleFromModulePtr(moduleAddr); - bool result = contract.TryGetSimpleName(handle, out string actual); - Assert.True(result); - Assert.Equal(expected, actual); - } - { - Contracts.ModuleHandle handle = contract.GetModuleHandleFromModulePtr(moduleAddrEmptyName); - bool result = contract.TryGetSimpleName(handle, out string actual); - Assert.False(result); - Assert.Equal(string.Empty, actual); - } + moduleAddr = loader.AddModule().Address; + }); + + Contracts.ModuleHandle handle = contract.GetModuleHandleFromModulePtr(moduleAddr); + string actual = contract.GetSimpleName(handle); + Assert.Equal(string.Empty, actual); } [Theory] [ClassData(typeof(MockTarget.StdArch))] - public void TryGetSimpleName_InvalidUtf8(MockTarget.Architecture arch) + public void GetSimpleName_InvalidUtf8(MockTarget.Architecture arch) { // 0xFF is not valid UTF-8 byte[] invalidUtf8 = [0xFF, 0xFE]; @@ -151,7 +156,7 @@ public void TryGetSimpleName_InvalidUtf8(MockTarget.Architecture arch) }); Contracts.ModuleHandle handle = contract.GetModuleHandleFromModulePtr(moduleAddr); - Assert.Throws(() => contract.TryGetSimpleName(handle, out _)); + Assert.Throws(() => contract.GetSimpleName(handle)); } private static readonly Dictionary MockHeapDictionary = new() From 013ca46b740c861d66c3f783ada239c669f83c34 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Thu, 30 Apr 2026 17:25:41 -0700 Subject: [PATCH 051/115] Split the xarch specific data from register.h into its own file to match other platforms (#127528) Just improving consistency here. --- src/coreclr/jit/CMakeLists.txt | 19 +- src/coreclr/jit/register.h | 374 +------------------------- src/coreclr/jit/registeramd64.h | 288 ++++++++++++++++++++ src/coreclr/jit/registerarm.h | 36 +-- src/coreclr/jit/registerarm64.h | 10 +- src/coreclr/jit/registerloongarch64.h | 2 +- src/coreclr/jit/registerriscv64.h | 4 +- src/coreclr/jit/registerwasm.h | 23 +- src/coreclr/jit/registerx86.h | 140 ++++++++++ 9 files changed, 503 insertions(+), 393 deletions(-) create mode 100644 src/coreclr/jit/registeramd64.h create mode 100644 src/coreclr/jit/registerx86.h diff --git a/src/coreclr/jit/CMakeLists.txt b/src/coreclr/jit/CMakeLists.txt index e7e69887486830..2efb2b841bb04d 100644 --- a/src/coreclr/jit/CMakeLists.txt +++ b/src/coreclr/jit/CMakeLists.txt @@ -404,10 +404,6 @@ set( JIT_HEADERS stacklevelsetter.h target.h targetcommon.h - targetx86.h - targetamd64.h - targetarm.h - targetarm64.h treelifeupdater.h typelist.h unwind.h @@ -421,15 +417,24 @@ set( JIT_HEADERS ) # Arch specific headers + set( JIT_AMD64_HEADERS emitfmtsxarch.h emitxarch.h hwintrinsiclistxarch.h - hwintrinsic.h instrsxarch.h + registeramd64.h + targetamd64.h ) -set( JIT_I386_HEADERS ${JIT_AMD64_HEADERS} ) +set( JIT_I386_HEADERS + emitfmtsxarch.h + emitxarch.h + hwintrinsiclistxarch.h + instrsxarch.h + registerx86.h + targetx86.h +) set( JIT_ARM64_HEADERS emitarm64.h @@ -440,6 +445,7 @@ set( JIT_ARM64_HEADERS instrsarm64.h instrsarm64sve.h registerarm64.h + targetarm64.h ) set( JIT_ARM_HEADERS @@ -447,6 +453,7 @@ set( JIT_ARM_HEADERS emitfmtsarm.h instrsarm.h registerarm.h + targetarm.h ) set ( JIT_ARMV6_HEADERS diff --git a/src/coreclr/jit/register.h b/src/coreclr/jit/register.h index 5c9a03872e9740..4842f7264b51e3 100644 --- a/src/coreclr/jit/register.h +++ b/src/coreclr/jit/register.h @@ -1,376 +1,20 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// clang-format off - -/*****************************************************************************/ -/*****************************************************************************/ -#ifndef REGDEF -#error Must define REGDEF macro before including this file -#endif -#ifndef REGALIAS -#define REGALIAS(alias, realname) -#endif - -#if defined(TARGET_XARCH) - -#if defined(TARGET_X86) -/* -REGDEF(name, rnum, mask, sname) */ -REGDEF(EAX, 0, 0x01, "eax" ) -REGDEF(ECX, 1, 0x02, "ecx" ) -REGDEF(EDX, 2, 0x04, "edx" ) -REGDEF(EBX, 3, 0x08, "ebx" ) -REGDEF(ESP, 4, 0x10, "esp" ) -REGDEF(EBP, 5, 0x20, "ebp" ) -REGDEF(ESI, 6, 0x40, "esi" ) -REGDEF(EDI, 7, 0x80, "edi" ) -REGALIAS(RAX, EAX) -REGALIAS(RCX, ECX) -REGALIAS(RDX, EDX) -REGALIAS(RBX, EBX) -REGALIAS(RSP, ESP) -REGALIAS(RBP, EBP) -REGALIAS(RSI, ESI) -REGALIAS(RDI, EDI) - -#else // !defined(TARGET_X86) - -#define GPRMASK(x) (1ULL << (x)) -/* -REGDEF(name, rnum, mask, sname) */ -REGDEF(RAX, 0, GPRMASK(0), "rax" ) -REGDEF(RCX, 1, GPRMASK(1), "rcx" ) -REGDEF(RDX, 2, GPRMASK(2), "rdx" ) -REGDEF(RBX, 3, GPRMASK(3), "rbx" ) -REGDEF(RSP, 4, GPRMASK(4), "rsp" ) -REGDEF(RBP, 5, GPRMASK(5), "rbp" ) -REGDEF(RSI, 6, GPRMASK(6), "rsi" ) -REGDEF(RDI, 7, GPRMASK(7), "rdi" ) -REGDEF(R8, 8, GPRMASK(8), "r8" ) -REGDEF(R9, 9, GPRMASK(9), "r9" ) -REGDEF(R10, 10, GPRMASK(10), "r10" ) -REGDEF(R11, 11, GPRMASK(11), "r11" ) -REGDEF(R12, 12, GPRMASK(12), "r12" ) -REGDEF(R13, 13, GPRMASK(13), "r13" ) -REGDEF(R14, 14, GPRMASK(14), "r14" ) -REGDEF(R15, 15, GPRMASK(15), "r15" ) -REGDEF(R16, 16, GPRMASK(16), "r16" ) -REGDEF(R17, 17, GPRMASK(17), "r17" ) -REGDEF(R18, 18, GPRMASK(18), "r18" ) -REGDEF(R19, 19, GPRMASK(19), "r19" ) -REGDEF(R20, 20, GPRMASK(20), "r20" ) -REGDEF(R21, 21, GPRMASK(21), "r21" ) -REGDEF(R22, 22, GPRMASK(22), "r22" ) -REGDEF(R23, 23, GPRMASK(23), "r23" ) -REGDEF(R24, 24, GPRMASK(24), "r24" ) -REGDEF(R25, 25, GPRMASK(25), "r25" ) -REGDEF(R26, 26, GPRMASK(26), "r26" ) -REGDEF(R27, 27, GPRMASK(27), "r27" ) -REGDEF(R28, 28, GPRMASK(28), "r28" ) -REGDEF(R29, 29, GPRMASK(29), "r29" ) -REGDEF(R30, 30, GPRMASK(30), "r30" ) -REGDEF(R31, 31, GPRMASK(31), "r31" ) - -REGALIAS(EAX, RAX) -REGALIAS(ECX, RCX) -REGALIAS(EDX, RDX) -REGALIAS(EBX, RBX) -REGALIAS(ESP, RSP) -REGALIAS(EBP, RBP) -REGALIAS(ESI, RSI) -REGALIAS(EDI, RDI) - -#endif // !defined(TARGET_X86) - -#ifdef TARGET_AMD64 -#define XMMBASE 32 -#define XMMMASK(x) (1ULL << ((x)+XMMBASE)) - -#define KBASE 64 -#define KMASK(x) (1ULL << ((x))) - -#else // !TARGET_AMD64 -#define XMMBASE 8 -#define XMMMASK(x) ((int32_t)(1) << ((x)+XMMBASE)) - -#define KBASE 16 -#define KMASK(x) ((int32_t)(1) << ((x)+KBASE)) - - -#endif // !TARGET_AMD64 - -REGDEF(XMM0, 0+XMMBASE, XMMMASK(0), "mm0" ) -REGDEF(XMM1, 1+XMMBASE, XMMMASK(1), "mm1" ) -REGDEF(XMM2, 2+XMMBASE, XMMMASK(2), "mm2" ) -REGDEF(XMM3, 3+XMMBASE, XMMMASK(3), "mm3" ) -REGDEF(XMM4, 4+XMMBASE, XMMMASK(4), "mm4" ) -REGDEF(XMM5, 5+XMMBASE, XMMMASK(5), "mm5" ) -REGDEF(XMM6, 6+XMMBASE, XMMMASK(6), "mm6" ) -REGDEF(XMM7, 7+XMMBASE, XMMMASK(7), "mm7" ) - -#ifdef TARGET_AMD64 -REGDEF(XMM8, 8+XMMBASE, XMMMASK(8), "mm8" ) -REGDEF(XMM9, 9+XMMBASE, XMMMASK(9), "mm9" ) -REGDEF(XMM10, 10+XMMBASE, XMMMASK(10), "mm10" ) -REGDEF(XMM11, 11+XMMBASE, XMMMASK(11), "mm11" ) -REGDEF(XMM12, 12+XMMBASE, XMMMASK(12), "mm12" ) -REGDEF(XMM13, 13+XMMBASE, XMMMASK(13), "mm13" ) -REGDEF(XMM14, 14+XMMBASE, XMMMASK(14), "mm14" ) -REGDEF(XMM15, 15+XMMBASE, XMMMASK(15), "mm15" ) - -REGDEF(XMM16, 16+XMMBASE, XMMMASK(16), "mm16" ) -REGDEF(XMM17, 17+XMMBASE, XMMMASK(17), "mm17" ) -REGDEF(XMM18, 18+XMMBASE, XMMMASK(18), "mm18" ) -REGDEF(XMM19, 19+XMMBASE, XMMMASK(19), "mm19" ) -REGDEF(XMM20, 20+XMMBASE, XMMMASK(20), "mm20" ) -REGDEF(XMM21, 21+XMMBASE, XMMMASK(21), "mm21" ) -REGDEF(XMM22, 22+XMMBASE, XMMMASK(22), "mm22" ) -REGDEF(XMM23, 23+XMMBASE, XMMMASK(23), "mm23" ) - -REGDEF(XMM24, 24+XMMBASE, XMMMASK(24), "mm24" ) -REGDEF(XMM25, 25+XMMBASE, XMMMASK(25), "mm25" ) -REGDEF(XMM26, 26+XMMBASE, XMMMASK(26), "mm26" ) -REGDEF(XMM27, 27+XMMBASE, XMMMASK(27), "mm27" ) -REGDEF(XMM28, 28+XMMBASE, XMMMASK(28), "mm28" ) -REGDEF(XMM29, 29+XMMBASE, XMMMASK(29), "mm29" ) -REGDEF(XMM30, 30+XMMBASE, XMMMASK(30), "mm30" ) -REGDEF(XMM31, 31+XMMBASE, XMMMASK(31), "mm31" ) - -#endif // !TARGET_AMD64 - -REGDEF(K0, 0+KBASE, KMASK(0), "k0" ) -REGDEF(K1, 1+KBASE, KMASK(1), "k1" ) -REGDEF(K2, 2+KBASE, KMASK(2), "k2" ) -REGDEF(K3, 3+KBASE, KMASK(3), "k3" ) -REGDEF(K4, 4+KBASE, KMASK(4), "k4" ) -REGDEF(K5, 5+KBASE, KMASK(5), "k5" ) -REGDEF(K6, 6+KBASE, KMASK(6), "k6" ) -REGDEF(K7, 7+KBASE, KMASK(7), "k7" ) - -REGDEF(STK, 8+KBASE, 0x0000, "STK" ) - -// Ignore REG_* symbols defined in Android NDK #if defined(TARGET_X86) -#undef REG_EAX -#define REG_EAX JITREG_EAX -#undef REG_ECX -#define REG_ECX JITREG_ECX -#undef REG_EDX -#define REG_EDX JITREG_EDX -#undef REG_EBX -#define REG_EBX JITREG_EBX -#undef REG_ESP -#define REG_ESP JITREG_ESP -#undef REG_EBP -#define REG_EBP JITREG_EBP -#undef REG_ESI -#define REG_ESI JITREG_ESI -#undef REG_EDI -#define REG_EDI JITREG_EDI -#undef REG_RAX -#define REG_RAX JITREG_RAX -#undef REG_RCX -#define REG_RCX JITREG_RCX -#undef REG_RDX -#define REG_RDX JITREG_RDX -#undef REG_RBX -#define REG_RBX JITREG_RBX -#undef REG_RSP -#define REG_RSP JITREG_RSP -#undef REG_RBP -#define REG_RBP JITREG_RBP -#undef REG_RSI -#define REG_RSI JITREG_RSI -#undef REG_RDI -#define REG_RDI JITREG_RDI -#else // defined(TARGET_X86) -#undef REG_RAX -#define REG_RAX JITREG_RAX -#undef REG_RCX -#define REG_RCX JITREG_RCX -#undef REG_RDX -#define REG_RDX JITREG_RDX -#undef REG_RBX -#define REG_RBX JITREG_RBX -#undef REG_RSP -#define REG_RSP JITREG_RSP -#undef REG_RBP -#define REG_RBP JITREG_RBP -#undef REG_RSI -#define REG_RSI JITREG_RSI -#undef REG_RDI -#define REG_RDI JITREG_RDI -#undef REG_R8 -#define REG_R8 JITREG_R8 -#undef REG_R9 -#define REG_R9 JITREG_R9 -#undef REG_R10 -#define REG_R10 JITREG_R10 -#undef REG_R11 -#define REG_R11 JITREG_R11 -#undef REG_R12 -#define REG_R12 JITREG_R12 -#undef REG_R13 -#define REG_R13 JITREG_R13 -#undef REG_R14 -#define REG_R14 JITREG_R14 -#undef REG_R15 -#define REG_R15 JITREG_R15 -#undef REG_R16 -#define REG_R16 JITREG_R16 -#undef REG_R17 -#define REG_R17 JITREG_R17 -#undef REG_R18 -#define REG_R18 JITREG_R18 -#undef REG_R19 -#define REG_R19 JITREG_R19 -#undef REG_R20 -#define REG_R20 JITREG_R20 -#undef REG_R21 -#define REG_R21 JITREG_R21 -#undef REG_R22 -#define REG_R22 JITREG_R22 -#undef REG_R23 -#define REG_R23 JITREG_R23 -#undef REG_R24 -#define REG_R24 JITREG_R24 -#undef REG_R25 -#define REG_R25 JITREG_R25 -#undef REG_R26 -#define REG_R26 JITREG_R26 -#undef REG_R27 -#define REG_R27 JITREG_R27 -#undef REG_R28 -#define REG_R28 JITREG_R28 -#undef REG_R29 -#define REG_R29 JITREG_R29 -#undef REG_R30 -#define REG_R30 JITREG_R30 -#undef REG_R31 -#define REG_R31 JITREG_R31 -#undef REG_EAX -#define REG_EAX JITREG_EAX -#undef REG_ECX -#define REG_ECX JITREG_ECX -#undef REG_EDX -#define REG_EDX JITREG_EDX -#undef REG_EBX -#define REG_EBX JITREG_EBX -#undef REG_ESP -#define REG_ESP JITREG_ESP -#undef REG_EBP -#define REG_EBP JITREG_EBP -#undef REG_ESI -#define REG_ESI JITREG_ESI -#undef REG_EDI -#define REG_EDI JITREG_EDI -#endif // !defined(TARGET_X86) - -#undef REG_XMM0 -#define REG_XMM0 JITREG_XMM0 -#undef REG_XMM1 -#define REG_XMM1 JITREG_XMM1 -#undef REG_XMM2 -#define REG_XMM2 JITREG_XMM2 -#undef REG_XMM3 -#define REG_XMM3 JITREG_XMM3 -#undef REG_XMM4 -#define REG_XMM4 JITREG_XMM4 -#undef REG_XMM5 -#define REG_XMM5 JITREG_XMM5 -#undef REG_XMM6 -#define REG_XMM6 JITREG_XMM6 -#undef REG_XMM7 -#define REG_XMM7 JITREG_XMM7 - -#ifdef TARGET_AMD64 -#undef REG_XMM8 -#define REG_XMM8 JITREG_XMM8 -#undef REG_XMM9 -#define REG_XMM9 JITREG_XMM9 -#undef REG_XMM10 -#define REG_XMM10 JITREG_XMM10 -#undef REG_XMM11 -#define REG_XMM11 JITREG_XMM11 -#undef REG_XMM12 -#define REG_XMM12 JITREG_XMM12 -#undef REG_XMM13 -#define REG_XMM13 JITREG_XMM13 -#undef REG_XMM14 -#define REG_XMM14 JITREG_XMM14 -#undef REG_XMM15 -#define REG_XMM15 JITREG_XMM15 -#undef REG_XMM16 -#define REG_XMM16 JITREG_XMM16 -#undef REG_XMM17 -#define REG_XMM17 JITREG_XMM17 -#undef REG_XMM18 -#define REG_XMM18 JITREG_XMM18 -#undef REG_XMM19 -#define REG_XMM19 JITREG_XMM19 -#undef REG_XMM20 -#define REG_XMM20 JITREG_XMM20 -#undef REG_XMM21 -#define REG_XMM21 JITREG_XMM21 -#undef REG_XMM22 -#define REG_XMM22 JITREG_XMM22 -#undef REG_XMM23 -#define REG_XMM23 JITREG_XMM23 -#undef REG_XMM24 -#define REG_XMM24 JITREG_XMM24 -#undef REG_XMM25 -#define REG_XMM25 JITREG_XMM25 -#undef REG_XMM26 -#define REG_XMM26 JITREG_XMM26 -#undef REG_XMM27 -#define REG_XMM27 JITREG_XMM27 -#undef REG_XMM28 -#define REG_XMM28 JITREG_XMM28 -#undef REG_XMM29 -#define REG_XMM29 JITREG_XMM29 -#undef REG_XMM30 -#define REG_XMM30 JITREG_XMM30 -#undef REG_XMM31 -#define REG_XMM31 JITREG_XMM31 -#endif // TARGET_AMD64 - -#undef REG_K0 -#define REG_K0 JITREG_K0 -#undef REG_K1 -#define REG_K1 JITREG_K1 -#undef REG_K2 -#define REG_K2 JITREG_K2 -#undef REG_K3 -#define REG_K3 JITREG_K3 -#undef REG_K4 -#define REG_K4 JITREG_K4 -#undef REG_K5 -#define REG_K5 JITREG_K5 -#undef REG_K6 -#define REG_K6 JITREG_K6 -#undef REG_K7 -#define REG_K7 JITREG_K7 -#undef REG_STK -#define REG_STK JITREG_STK - +#include "registerx86.h" +#elif defined(TARGET_AMD64) +#include "registeramd64.h" #elif defined(TARGET_ARM) - #include "registerarm.h" +#include "registerarm.h" #elif defined(TARGET_ARM64) - #include "registerarm64.h" +#include "registerarm64.h" #elif defined(TARGET_LOONGARCH64) - #include "registerloongarch64.h" +#include "registerloongarch64.h" #elif defined(TARGET_RISCV64) - #include "registerriscv64.h" +#include "registerriscv64.h" #elif defined(TARGET_WASM) #include "registerwasm.h" #else - #error Unsupported or unset target architecture -#endif // target type -/*****************************************************************************/ -#undef REGDEF -#undef REGALIAS -#undef XMMMASK -/*****************************************************************************/ - -// clang-format on +#error Unsupported or unset target architecture +#endif diff --git a/src/coreclr/jit/registeramd64.h b/src/coreclr/jit/registeramd64.h new file mode 100644 index 00000000000000..e26fd1a6551c21 --- /dev/null +++ b/src/coreclr/jit/registeramd64.h @@ -0,0 +1,288 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// clang-format off + +/*****************************************************************************/ +/*****************************************************************************/ +#ifndef REGDEF +#error Must define REGDEF macro before including this file +#endif +#ifndef REGALIAS +#define REGALIAS(alias, realname) +#endif + +#define GPRMASK(x) (1ULL << (x)) + +/* +REGDEF(name, rnum, mask, sname) */ +REGDEF(RAX, 0, GPRMASK(0), "rax" ) +REGDEF(RCX, 1, GPRMASK(1), "rcx" ) +REGDEF(RDX, 2, GPRMASK(2), "rdx" ) +REGDEF(RBX, 3, GPRMASK(3), "rbx" ) +REGDEF(RSP, 4, GPRMASK(4), "rsp" ) +REGDEF(RBP, 5, GPRMASK(5), "rbp" ) +REGDEF(RSI, 6, GPRMASK(6), "rsi" ) +REGDEF(RDI, 7, GPRMASK(7), "rdi" ) +REGDEF(R8, 8, GPRMASK(8), "r8" ) +REGDEF(R9, 9, GPRMASK(9), "r9" ) +REGDEF(R10, 10, GPRMASK(10), "r10" ) +REGDEF(R11, 11, GPRMASK(11), "r11" ) +REGDEF(R12, 12, GPRMASK(12), "r12" ) +REGDEF(R13, 13, GPRMASK(13), "r13" ) +REGDEF(R14, 14, GPRMASK(14), "r14" ) +REGDEF(R15, 15, GPRMASK(15), "r15" ) +REGDEF(R16, 16, GPRMASK(16), "r16" ) +REGDEF(R17, 17, GPRMASK(17), "r17" ) +REGDEF(R18, 18, GPRMASK(18), "r18" ) +REGDEF(R19, 19, GPRMASK(19), "r19" ) +REGDEF(R20, 20, GPRMASK(20), "r20" ) +REGDEF(R21, 21, GPRMASK(21), "r21" ) +REGDEF(R22, 22, GPRMASK(22), "r22" ) +REGDEF(R23, 23, GPRMASK(23), "r23" ) +REGDEF(R24, 24, GPRMASK(24), "r24" ) +REGDEF(R25, 25, GPRMASK(25), "r25" ) +REGDEF(R26, 26, GPRMASK(26), "r26" ) +REGDEF(R27, 27, GPRMASK(27), "r27" ) +REGDEF(R28, 28, GPRMASK(28), "r28" ) +REGDEF(R29, 29, GPRMASK(29), "r29" ) +REGDEF(R30, 30, GPRMASK(30), "r30" ) +REGDEF(R31, 31, GPRMASK(31), "r31" ) + +REGALIAS(EAX, RAX) +REGALIAS(ECX, RCX) +REGALIAS(EDX, RDX) +REGALIAS(EBX, RBX) +REGALIAS(ESP, RSP) +REGALIAS(EBP, RBP) +REGALIAS(ESI, RSI) +REGALIAS(EDI, RDI) + +#define XMMBASE 32 +#define XMMMASK(x) (1ULL << ((x)+XMMBASE)) + +REGDEF(XMM0, 0+XMMBASE, XMMMASK(0), "mm0" ) +REGDEF(XMM1, 1+XMMBASE, XMMMASK(1), "mm1" ) +REGDEF(XMM2, 2+XMMBASE, XMMMASK(2), "mm2" ) +REGDEF(XMM3, 3+XMMBASE, XMMMASK(3), "mm3" ) +REGDEF(XMM4, 4+XMMBASE, XMMMASK(4), "mm4" ) +REGDEF(XMM5, 5+XMMBASE, XMMMASK(5), "mm5" ) +REGDEF(XMM6, 6+XMMBASE, XMMMASK(6), "mm6" ) +REGDEF(XMM7, 7+XMMBASE, XMMMASK(7), "mm7" ) +REGDEF(XMM8, 8+XMMBASE, XMMMASK(8), "mm8" ) +REGDEF(XMM9, 9+XMMBASE, XMMMASK(9), "mm9" ) +REGDEF(XMM10, 10+XMMBASE, XMMMASK(10), "mm10" ) +REGDEF(XMM11, 11+XMMBASE, XMMMASK(11), "mm11" ) +REGDEF(XMM12, 12+XMMBASE, XMMMASK(12), "mm12" ) +REGDEF(XMM13, 13+XMMBASE, XMMMASK(13), "mm13" ) +REGDEF(XMM14, 14+XMMBASE, XMMMASK(14), "mm14" ) +REGDEF(XMM15, 15+XMMBASE, XMMMASK(15), "mm15" ) +REGDEF(XMM16, 16+XMMBASE, XMMMASK(16), "mm16" ) +REGDEF(XMM17, 17+XMMBASE, XMMMASK(17), "mm17" ) +REGDEF(XMM18, 18+XMMBASE, XMMMASK(18), "mm18" ) +REGDEF(XMM19, 19+XMMBASE, XMMMASK(19), "mm19" ) +REGDEF(XMM20, 20+XMMBASE, XMMMASK(20), "mm20" ) +REGDEF(XMM21, 21+XMMBASE, XMMMASK(21), "mm21" ) +REGDEF(XMM22, 22+XMMBASE, XMMMASK(22), "mm22" ) +REGDEF(XMM23, 23+XMMBASE, XMMMASK(23), "mm23" ) +REGDEF(XMM24, 24+XMMBASE, XMMMASK(24), "mm24" ) +REGDEF(XMM25, 25+XMMBASE, XMMMASK(25), "mm25" ) +REGDEF(XMM26, 26+XMMBASE, XMMMASK(26), "mm26" ) +REGDEF(XMM27, 27+XMMBASE, XMMMASK(27), "mm27" ) +REGDEF(XMM28, 28+XMMBASE, XMMMASK(28), "mm28" ) +REGDEF(XMM29, 29+XMMBASE, XMMMASK(29), "mm29" ) +REGDEF(XMM30, 30+XMMBASE, XMMMASK(30), "mm30" ) +REGDEF(XMM31, 31+XMMBASE, XMMMASK(31), "mm31" ) + +#define KBASE 64 +#define KMASK(x) (1ULL << ((x))) + +REGDEF(K0, 0+KBASE, KMASK(0), "k0" ) +REGDEF(K1, 1+KBASE, KMASK(1), "k1" ) +REGDEF(K2, 2+KBASE, KMASK(2), "k2" ) +REGDEF(K3, 3+KBASE, KMASK(3), "k3" ) +REGDEF(K4, 4+KBASE, KMASK(4), "k4" ) +REGDEF(K5, 5+KBASE, KMASK(5), "k5" ) +REGDEF(K6, 6+KBASE, KMASK(6), "k6" ) +REGDEF(K7, 7+KBASE, KMASK(7), "k7" ) + +// This must be last! +REGDEF(STK, 8+KBASE, 0x0000, "STK" ) + +// Ignore REG_* symbols defined in Android NDK +#undef REG_RAX +#define REG_RAX JITREG_RAX +#undef REG_RCX +#define REG_RCX JITREG_RCX +#undef REG_RDX +#define REG_RDX JITREG_RDX +#undef REG_RBX +#define REG_RBX JITREG_RBX +#undef REG_RSP +#define REG_RSP JITREG_RSP +#undef REG_RBP +#define REG_RBP JITREG_RBP +#undef REG_RSI +#define REG_RSI JITREG_RSI +#undef REG_RDI +#define REG_RDI JITREG_RDI +#undef REG_R8 +#define REG_R8 JITREG_R8 +#undef REG_R9 +#define REG_R9 JITREG_R9 +#undef REG_R10 +#define REG_R10 JITREG_R10 +#undef REG_R11 +#define REG_R11 JITREG_R11 +#undef REG_R12 +#define REG_R12 JITREG_R12 +#undef REG_R13 +#define REG_R13 JITREG_R13 +#undef REG_R14 +#define REG_R14 JITREG_R14 +#undef REG_R15 +#define REG_R15 JITREG_R15 +#undef REG_R16 +#define REG_R16 JITREG_R16 +#undef REG_R17 +#define REG_R17 JITREG_R17 +#undef REG_R18 +#define REG_R18 JITREG_R18 +#undef REG_R19 +#define REG_R19 JITREG_R19 +#undef REG_R20 +#define REG_R20 JITREG_R20 +#undef REG_R21 +#define REG_R21 JITREG_R21 +#undef REG_R22 +#define REG_R22 JITREG_R22 +#undef REG_R23 +#define REG_R23 JITREG_R23 +#undef REG_R24 +#define REG_R24 JITREG_R24 +#undef REG_R25 +#define REG_R25 JITREG_R25 +#undef REG_R26 +#define REG_R26 JITREG_R26 +#undef REG_R27 +#define REG_R27 JITREG_R27 +#undef REG_R28 +#define REG_R28 JITREG_R28 +#undef REG_R29 +#define REG_R29 JITREG_R29 +#undef REG_R30 +#define REG_R30 JITREG_R30 +#undef REG_R31 +#define REG_R31 JITREG_R31 + +#undef REG_EAX +#define REG_EAX JITREG_EAX +#undef REG_ECX +#define REG_ECX JITREG_ECX +#undef REG_EDX +#define REG_EDX JITREG_EDX +#undef REG_EBX +#define REG_EBX JITREG_EBX +#undef REG_ESP +#define REG_ESP JITREG_ESP +#undef REG_EBP +#define REG_EBP JITREG_EBP +#undef REG_ESI +#define REG_ESI JITREG_ESI +#undef REG_EDI +#define REG_EDI JITREG_EDI + +#undef REG_XMM0 +#define REG_XMM0 JITREG_XMM0 +#undef REG_XMM1 +#define REG_XMM1 JITREG_XMM1 +#undef REG_XMM2 +#define REG_XMM2 JITREG_XMM2 +#undef REG_XMM3 +#define REG_XMM3 JITREG_XMM3 +#undef REG_XMM4 +#define REG_XMM4 JITREG_XMM4 +#undef REG_XMM5 +#define REG_XMM5 JITREG_XMM5 +#undef REG_XMM6 +#define REG_XMM6 JITREG_XMM6 +#undef REG_XMM7 +#define REG_XMM7 JITREG_XMM7 +#undef REG_XMM8 +#define REG_XMM8 JITREG_XMM8 +#undef REG_XMM9 +#define REG_XMM9 JITREG_XMM9 +#undef REG_XMM10 +#define REG_XMM10 JITREG_XMM10 +#undef REG_XMM11 +#define REG_XMM11 JITREG_XMM11 +#undef REG_XMM12 +#define REG_XMM12 JITREG_XMM12 +#undef REG_XMM13 +#define REG_XMM13 JITREG_XMM13 +#undef REG_XMM14 +#define REG_XMM14 JITREG_XMM14 +#undef REG_XMM15 +#define REG_XMM15 JITREG_XMM15 +#undef REG_XMM16 +#define REG_XMM16 JITREG_XMM16 +#undef REG_XMM17 +#define REG_XMM17 JITREG_XMM17 +#undef REG_XMM18 +#define REG_XMM18 JITREG_XMM18 +#undef REG_XMM19 +#define REG_XMM19 JITREG_XMM19 +#undef REG_XMM20 +#define REG_XMM20 JITREG_XMM20 +#undef REG_XMM21 +#define REG_XMM21 JITREG_XMM21 +#undef REG_XMM22 +#define REG_XMM22 JITREG_XMM22 +#undef REG_XMM23 +#define REG_XMM23 JITREG_XMM23 +#undef REG_XMM24 +#define REG_XMM24 JITREG_XMM24 +#undef REG_XMM25 +#define REG_XMM25 JITREG_XMM25 +#undef REG_XMM26 +#define REG_XMM26 JITREG_XMM26 +#undef REG_XMM27 +#define REG_XMM27 JITREG_XMM27 +#undef REG_XMM28 +#define REG_XMM28 JITREG_XMM28 +#undef REG_XMM29 +#define REG_XMM29 JITREG_XMM29 +#undef REG_XMM30 +#define REG_XMM30 JITREG_XMM30 +#undef REG_XMM31 +#define REG_XMM31 JITREG_XMM31 + +#undef REG_K0 +#define REG_K0 JITREG_K0 +#undef REG_K1 +#define REG_K1 JITREG_K1 +#undef REG_K2 +#define REG_K2 JITREG_K2 +#undef REG_K3 +#define REG_K3 JITREG_K3 +#undef REG_K4 +#define REG_K4 JITREG_K4 +#undef REG_K5 +#define REG_K5 JITREG_K5 +#undef REG_K6 +#define REG_K6 JITREG_K6 +#undef REG_K7 +#define REG_K7 JITREG_K7 + +#undef REG_STK +#define REG_STK JITREG_STK + +/*****************************************************************************/ +#undef GPRMASK +#undef XMMMASK +#undef KMASK +#undef REGDEF +#undef REGALIAS +/*****************************************************************************/ + +// clang-format on diff --git a/src/coreclr/jit/registerarm.h b/src/coreclr/jit/registerarm.h index e26319c0374afd..f1612bfe54d4dd 100644 --- a/src/coreclr/jit/registerarm.h +++ b/src/coreclr/jit/registerarm.h @@ -31,6 +31,12 @@ REGDEF(SP, 13, 0x2000, "sp" ) REGDEF(LR, 14, 0x4000, "lr" ) REGDEF(PC, 15, 0x8000, "pc" ) +// Allow us to call R11/FP, SP, LR and PC by their register number names +REGALIAS(FP, R11) +REGALIAS(R13, SP) +REGALIAS(R14, LR) +REGALIAS(R15, PC) + #define FPBASE 16 #define VFPMASK(x) (((int64_t)1) << (x+FPBASE)) @@ -67,12 +73,8 @@ REGDEF(F29, 29+FPBASE, VFPMASK(29), "f29") REGDEF(F30, 30+FPBASE, VFPMASK(30), "f30") REGDEF(F31, 31+FPBASE, VFPMASK(31), "f31") - -// Allow us to call R11/FP, SP, LR and PC by their register number names -REGALIAS(FP, R11) -REGALIAS(R13, SP) -REGALIAS(R14, LR) -REGALIAS(R15, PC) +// This must be last! +REGDEF(STK, 32+FPBASE, 0x0000, "STK") // Ignore REG_* symbols defined in Android NDK #undef REG_R0 @@ -107,6 +109,16 @@ REGALIAS(R15, PC) #define REG_LR JITREG_LR #undef REG_PC #define REG_PC JITREG_PC + +#undef REG_FP +#define REG_FP JITREG_FP +#undef REG_R13 +#define REG_R13 JITREG_R13 +#undef REG_R14 +#define REG_R14 JITREG_R14 +#undef REG_R15 +#define REG_R15 JITREG_R15 + #undef REG_F0 #define REG_F0 JITREG_F0 #undef REG_F1 @@ -171,20 +183,10 @@ REGALIAS(R15, PC) #define REG_F30 JITREG_F30 #undef REG_F31 #define REG_F31 JITREG_F31 -#undef REG_FP -#define REG_FP JITREG_FP -#undef REG_R13 -#define REG_R13 JITREG_R13 -#undef REG_R14 -#define REG_R14 JITREG_R14 -#undef REG_R15 -#define REG_R15 JITREG_R15 + #undef REG_STK #define REG_STK JITREG_STK -// This must be last! -REGDEF(STK, 32+FPBASE, 0x0000, "STK") - /*****************************************************************************/ #undef REGDEF #undef REGALIAS diff --git a/src/coreclr/jit/registerarm64.h b/src/coreclr/jit/registerarm64.h index 4f69628c8a0b34..312ef29ba4c175 100644 --- a/src/coreclr/jit/registerarm64.h +++ b/src/coreclr/jit/registerarm64.h @@ -121,6 +121,7 @@ REGDEF(P15, 15+PBASE, PMASK(15), "p15", "na") REGDEF(SP, 0+NBASE, 0x0000, "sp", "wsp?") REGDEF(FFR, 1+NBASE, 0x0000, "ffr", "na") + // This must be last! REGDEF(STK, 2+NBASE, 0x0000, "STK", "STK") @@ -189,6 +190,7 @@ REGDEF(STK, 2+NBASE, 0x0000, "STK", "STK") #define REG_LR JITREG_LR #undef REG_ZR #define REG_ZR JITREG_ZR + #undef REG_R16 #define REG_R16 JITREG_R16 #undef REG_R17 @@ -199,6 +201,7 @@ REGDEF(STK, 2+NBASE, 0x0000, "STK", "STK") #define REG_R29 JITREG_R29 #undef REG_R30 #define REG_R30 JITREG_R30 + #undef REG_V0 #define REG_V0 JITREG_V0 #undef REG_V1 @@ -263,6 +266,7 @@ REGDEF(STK, 2+NBASE, 0x0000, "STK", "STK") #define REG_V30 JITREG_V30 #undef REG_V31 #define REG_V31 JITREG_V31 + #undef REG_P0 #define REG_P0 JITREG_P0 #undef REG_P1 @@ -295,17 +299,21 @@ REGDEF(STK, 2+NBASE, 0x0000, "STK", "STK") #define REG_P14 JITREG_P14 #undef REG_P15 #define REG_P15 JITREG_P15 + #undef REG_SP #define REG_SP JITREG_SP #undef REG_FFR #define REG_FFR JITREG_FFR + #undef REG_STK #define REG_STK JITREG_STK /*****************************************************************************/ #undef RMASK -#undef VMASK #undef VBASE +#undef VMASK +#undef PBASE +#undef PMASK #undef NBASE #undef REGDEF #undef REGALIAS diff --git a/src/coreclr/jit/registerloongarch64.h b/src/coreclr/jit/registerloongarch64.h index 8f3cd157016bb2..cdb813a3fb0184 100644 --- a/src/coreclr/jit/registerloongarch64.h +++ b/src/coreclr/jit/registerloongarch64.h @@ -105,8 +105,8 @@ REGDEF(STK, 0+NBASE, 0x0000, "STK") /*****************************************************************************/ #undef RMASK -#undef FMASK #undef FBASE +#undef FMASK #undef NBASE #undef REGDEF #undef REGALIAS diff --git a/src/coreclr/jit/registerriscv64.h b/src/coreclr/jit/registerriscv64.h index be678d90148cae..8fa61870a7f3a9 100644 --- a/src/coreclr/jit/registerriscv64.h +++ b/src/coreclr/jit/registerriscv64.h @@ -97,8 +97,8 @@ REGDEF(STK, 0+NBASE, 0x0000, "STK") /*****************************************************************************/ #undef RMASK -#undef VMASK -#undef VBASE +#undef FBASE +#undef FMASK #undef NBASE #undef REGDEF #undef REGALIAS diff --git a/src/coreclr/jit/registerwasm.h b/src/coreclr/jit/registerwasm.h index 6001460c8896e1..fac983875be947 100644 --- a/src/coreclr/jit/registerwasm.h +++ b/src/coreclr/jit/registerwasm.h @@ -1,4 +1,25 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +// clang-format off -REGDEF(STK, 1, 0x0, "STK") +/*****************************************************************************/ +/*****************************************************************************/ +#ifndef REGDEF +#error Must define REGDEF macro before including this file +#endif +#ifndef REGALIAS +#define REGALIAS(alias, realname) +#endif + +/* +REGDEF(name, rnum, mask, sname) */ + +// This must be last! +REGDEF(STK, 1, 0x0000, "STK") + +/*****************************************************************************/ +#undef REGDEF +#undef REGALIAS +/*****************************************************************************/ + +// clang-format on diff --git a/src/coreclr/jit/registerx86.h b/src/coreclr/jit/registerx86.h new file mode 100644 index 00000000000000..0f3de489b95531 --- /dev/null +++ b/src/coreclr/jit/registerx86.h @@ -0,0 +1,140 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// clang-format off + +/*****************************************************************************/ +/*****************************************************************************/ +#ifndef REGDEF +#error Must define REGDEF macro before including this file +#endif +#ifndef REGALIAS +#define REGALIAS(alias, realname) +#endif + +/* +REGDEF(name, rnum, mask, sname) */ +REGDEF(EAX, 0, 0x01, "eax" ) +REGDEF(ECX, 1, 0x02, "ecx" ) +REGDEF(EDX, 2, 0x04, "edx" ) +REGDEF(EBX, 3, 0x08, "ebx" ) +REGDEF(ESP, 4, 0x10, "esp" ) +REGDEF(EBP, 5, 0x20, "ebp" ) +REGDEF(ESI, 6, 0x40, "esi" ) +REGDEF(EDI, 7, 0x80, "edi" ) + +REGALIAS(RAX, EAX) +REGALIAS(RCX, ECX) +REGALIAS(RDX, EDX) +REGALIAS(RBX, EBX) +REGALIAS(RSP, ESP) +REGALIAS(RBP, EBP) +REGALIAS(RSI, ESI) +REGALIAS(RDI, EDI) + +#define XMMBASE 8 +#define XMMMASK(x) ((int32_t)(1) << ((x)+XMMBASE)) + +REGDEF(XMM0, 0+XMMBASE, XMMMASK(0), "mm0" ) +REGDEF(XMM1, 1+XMMBASE, XMMMASK(1), "mm1" ) +REGDEF(XMM2, 2+XMMBASE, XMMMASK(2), "mm2" ) +REGDEF(XMM3, 3+XMMBASE, XMMMASK(3), "mm3" ) +REGDEF(XMM4, 4+XMMBASE, XMMMASK(4), "mm4" ) +REGDEF(XMM5, 5+XMMBASE, XMMMASK(5), "mm5" ) +REGDEF(XMM6, 6+XMMBASE, XMMMASK(6), "mm6" ) +REGDEF(XMM7, 7+XMMBASE, XMMMASK(7), "mm7" ) + +#define KBASE 16 +#define KMASK(x) ((int32_t)(1) << ((x)+KBASE)) + +REGDEF(K0, 0+KBASE, KMASK(0), "k0" ) +REGDEF(K1, 1+KBASE, KMASK(1), "k1" ) +REGDEF(K2, 2+KBASE, KMASK(2), "k2" ) +REGDEF(K3, 3+KBASE, KMASK(3), "k3" ) +REGDEF(K4, 4+KBASE, KMASK(4), "k4" ) +REGDEF(K5, 5+KBASE, KMASK(5), "k5" ) +REGDEF(K6, 6+KBASE, KMASK(6), "k6" ) +REGDEF(K7, 7+KBASE, KMASK(7), "k7" ) + +REGDEF(STK, 8+KBASE, 0x0000, "STK" ) + +// Ignore REG_* symbols defined in Android NDK +#undef REG_EAX +#define REG_EAX JITREG_EAX +#undef REG_ECX +#define REG_ECX JITREG_ECX +#undef REG_EDX +#define REG_EDX JITREG_EDX +#undef REG_EBX +#define REG_EBX JITREG_EBX +#undef REG_ESP +#define REG_ESP JITREG_ESP +#undef REG_EBP +#define REG_EBP JITREG_EBP +#undef REG_ESI +#define REG_ESI JITREG_ESI +#undef REG_EDI +#define REG_EDI JITREG_EDI + +#undef REG_RAX +#define REG_RAX JITREG_RAX +#undef REG_RCX +#define REG_RCX JITREG_RCX +#undef REG_RDX +#define REG_RDX JITREG_RDX +#undef REG_RBX +#define REG_RBX JITREG_RBX +#undef REG_RSP +#define REG_RSP JITREG_RSP +#undef REG_RBP +#define REG_RBP JITREG_RBP +#undef REG_RSI +#define REG_RSI JITREG_RSI +#undef REG_RDI +#define REG_RDI JITREG_RDI + +#undef REG_XMM0 +#define REG_XMM0 JITREG_XMM0 +#undef REG_XMM1 +#define REG_XMM1 JITREG_XMM1 +#undef REG_XMM2 +#define REG_XMM2 JITREG_XMM2 +#undef REG_XMM3 +#define REG_XMM3 JITREG_XMM3 +#undef REG_XMM4 +#define REG_XMM4 JITREG_XMM4 +#undef REG_XMM5 +#define REG_XMM5 JITREG_XMM5 +#undef REG_XMM6 +#define REG_XMM6 JITREG_XMM6 +#undef REG_XMM7 +#define REG_XMM7 JITREG_XMM7 + +#undef REG_K0 +#define REG_K0 JITREG_K0 +#undef REG_K1 +#define REG_K1 JITREG_K1 +#undef REG_K2 +#define REG_K2 JITREG_K2 +#undef REG_K3 +#define REG_K3 JITREG_K3 +#undef REG_K4 +#define REG_K4 JITREG_K4 +#undef REG_K5 +#define REG_K5 JITREG_K5 +#undef REG_K6 +#define REG_K6 JITREG_K6 +#undef REG_K7 +#define REG_K7 JITREG_K7 + +#undef REG_STK +#define REG_STK JITREG_STK + +/*****************************************************************************/ +#undef XMMMASK +#undef KMASK +#undef REGDEF +#undef REGALIAS +/*****************************************************************************/ + +// clang-format on From 84e5e272bf2c09d7a683025429b9d0db7a2ac579 Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Thu, 30 Apr 2026 19:02:02 -0700 Subject: [PATCH 052/115] [Wasm RyuJit] Use local's register type when loading it's value (#127619) Fixes an assert loading some register-sized struct locals. --- src/coreclr/jit/codegenwasm.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index dc6b2946c22ebe..caaec17c160446 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -2302,8 +2302,9 @@ void CodeGen::genCodeForLclVar(GenTreeLclVar* tree) else { assert(genIsValidReg(varDsc->GetRegNum())); - unsigned wasmLclIndex = WasmRegToIndex(varDsc->GetRegNum()); - GetEmitter()->emitIns_I(INS_local_get, emitTypeSize(tree), wasmLclIndex); + var_types type = varDsc->GetRegisterType(tree); + unsigned wasmLclIndex = WasmRegToIndex(varDsc->GetRegNum()); + GetEmitter()->emitIns_I(INS_local_get, emitTypeSize(type), wasmLclIndex); // In this case, the resulting tree type may be different from the local var type where the value originates, // and so we need an explicit conversion since we can't "load" // the value with a different type like we can if the value is on the shadow stack. From 0e1b3d90831f6770d98ad8e1c00c15bfd39d1583 Mon Sep 17 00:00:00 2001 From: Rich Lander <2608468+richlander@users.noreply.github.com> Date: Thu, 30 Apr 2026 20:54:56 -0700 Subject: [PATCH 053/115] Change commit message topic to 'container image digests' (#127614) "dependencies" is confusing or misleading. That's the default renovate terminology. "digests" is much clearer to anyone that is in a position to merge the generated PRs. PR examples: https://github.com/dotnet/runtime/pulls?q=is%3Apr+author%3Adotnet-renovate-bot+ --- eng/renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/renovate.json b/eng/renovate.json index f0f90be8c4c86c..b1efc3c669c073 100644 --- a/eng/renovate.json +++ b/eng/renovate.json @@ -37,7 +37,7 @@ "eng/pipelines/libraries/helix-queues-setup.yml" ], "pinDigests": true, - "commitMessageTopic": "container image dependencies" + "commitMessageTopic": "container image digests" } ] } From 6e51115e318cfce2984cc4d58ba6f7953297dcba Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Fri, 1 May 2026 00:24:33 -0400 Subject: [PATCH 054/115] [cDAC] Stack walk GC reference scanning and bug fixes (1/5) (#127395) ## Summary Part 1 of 5 stacked PRs splitting [#126408](https://github.com/dotnet/runtime/pull/126408) into reviewable pieces. ### What this PR contains **Stack Walk GC Reference Scanning:** - `PromoteCallerStack` / `PromoteCallerStackUsingGCRefMap` for transition frames - `GCRefMapDecoder` + `FindGCRefMap` with ReadyToRun import section resolution - `GcSignatureTypeProvider` for GC type classification - `SOSDacImpl.GetStackReferences` fully implemented using cDAC contracts - `GCInfoDecoder.EnumerateLiveSlots` promoted to `IGCInfo` contract (returns `IReadOnlyList`) - `GcSlotEnumerationOptions` replaces native `CodeManagerFlags` with descriptive boolean properties **Stack Walker Fixes:** - `IsFirst` preserved for skipped frames (matches native SFITER_SKIPPED_FRAME_FUNCTION) - `IsInterrupted` state tracking for exception frames (FaultingExceptionFrame, SoftwareExceptionFrame) - `GetReturnAddress` gating in SW_FRAME (only UpdateRegDisplay if return address non-null) - Catch handler offset override via `GetInterruptibleRanges` for EH resumption **Contract API Additions:** - `IGCInfo`: `EnumerateLiveSlots`, `GetStackBaseRegister`, `GetInterruptibleRanges` - `IExecutionManager`: `FindReadyToRunModule` - `IRuntimeTypeSystem`: `RequiresInstArg`, `IsAsyncMethod` - `IStackWalk`: `WalkStackReferences` **Data Descriptor Changes:** - Removed `ZapModule` and `GCRefMap` cached pointers (always resolve via `FindReadyToRunModule`) - Added `Indirection` for StubDispatchFrame, ExternalMethodFrame - Added `DynamicHelperFrame.DynamicHelperFrameFlags` - Added TransitionBlock fields (`OffsetOfArgs`, `ArgumentRegistersOffset`, `FirstGCRefMapSlot`) - Added ReadyToRunInfo fields (`ImportSections`, `NumImportSections`) - Added ExceptionInfo catch clause fields (`ClauseForCatchHandlerStartPC`, `ClauseForCatchHandlerEndPC`) **Documentation:** - GCInfo.md: Comprehensive implementation docs (header/body decoding, slot table, EnumerateLiveSlots algorithm, type definitions for `LiveSlot`, `InterruptibleRange`, `GcSlotEnumerationOptions`) - StackWalk.md: GC scanning algorithm, GCRefMap resolution flow, return address per frame type, `WalkStackReferences` API - ExecutionManager.md: `FindReadyToRunModule` API and implementation - RuntimeTypeSystem.md: `RequiresInstArg`, `IsAsyncMethod` APIs ### Stack overview | PR | Content | Status | |----|---------|--------| | **This PR** | Stack walk fixes + GC scanning | Open | | PR 2 | RuntimeSignatureDecoder (ELEMENT_TYPE_INTERNAL) | Pending | | PR 3 | ArgIterator port from crossgen2 | Pending | | PR 4 | Native stress framework (cdacstress.cpp) | Pending | | PR 5 | Managed stress tests + CI pipeline | Pending | ### Testing - 1727/1751 unit tests pass (24 pre-existing ThreadTests failures on main) - Dump tests (StackWalkDumpTests, StackReferenceDumpTests) validate end-to-end > [!NOTE] > This PR description was created with AI assistance from Copilot. --------- Co-authored-by: Max Charlamb Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/ExecutionManager.md | 21 + docs/design/datacontracts/GCInfo.md | 270 +++++++++- .../design/datacontracts/RuntimeTypeSystem.md | 60 +++ docs/design/datacontracts/StackWalk.md | 106 +++- .../vm/datadescriptor/datadescriptor.inc | 32 +- src/coreclr/vm/frames.h | 17 + src/coreclr/vm/readytoruninfo.h | 2 + .../Contracts/IExecutionManager.cs | 1 + .../Contracts/IGCInfo.cs | 39 ++ .../Contracts/IRuntimeTypeSystem.cs | 8 + .../DataType.cs | 3 + .../ExecutionManager/ExecutionManagerCore.cs | 13 + .../ExecutionManager/ExecutionManager_1.cs | 1 + .../ExecutionManager/ExecutionManager_2.cs | 1 + .../Contracts/GCInfo/GCInfoDecoder.cs | 77 ++- .../Contracts/GCInfo/GCInfo_1.cs | 19 + .../Contracts/GCInfo/IGCInfoDecoder.cs | 33 +- .../Contracts/RuntimeTypeSystem_1.cs | 64 +++ .../FrameHandling/ARMFrameHandler.cs | 7 +- .../StackWalk/FrameHandling/FrameIterator.cs | 101 +++- .../Contracts/StackWalk/GC/GCRefMapDecoder.cs | 123 +++++ .../Contracts/StackWalk/GC/GcScanner.cs | 500 ++++++++++++++++-- .../StackWalk/GC/GcSignatureTypeProvider.cs | 64 +++ .../Contracts/StackWalk/StackWalk_1.cs | 266 +++++++--- .../Data/ExceptionInfo.cs | 4 + .../Data/Frames/DynamicHelperFrame.cs | 18 + .../Data/Frames/ExternalMethodFrame.cs | 18 + .../Data/Frames/StubDispatchFrame.cs | 2 + .../Data/Frames/TransitionBlock.cs | 16 +- .../Data/ReadyToRunInfo.cs | 7 + .../MethodDescFlags_1.cs | 3 +- .../SOSDacImpl.cs | 70 ++- ...iagnostics.DataContractReader.Tests.csproj | 2 +- .../MockDescriptors.ExecutionManager.cs | 5 + .../MockDescriptors/MockDescriptors.Thread.cs | 4 + 35 files changed, 1705 insertions(+), 272 deletions(-) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DynamicHelperFrame.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs diff --git a/docs/design/datacontracts/ExecutionManager.md b/docs/design/datacontracts/ExecutionManager.md index cf7163569789f9..dc906398b10867 100644 --- a/docs/design/datacontracts/ExecutionManager.md +++ b/docs/design/datacontracts/ExecutionManager.md @@ -57,6 +57,9 @@ struct CodeBlockHandle bool IsFunclet(CodeBlockHandle codeInfoHandle); // Returns true if the code block is specifically a filter funclet bool IsFilterFunclet(CodeBlockHandle codeInfoHandle); + + // Finds the ReadyToRun module that contains the given address. + TargetPointer FindReadyToRunModule(TargetPointer address); ``` ```csharp @@ -501,6 +504,24 @@ After obtaining the clause array bounds, the common iteration logic classifies e `IsFilterFunclet` first checks `IsFunclet`. If the code block is a funclet, it retrieves the EH clauses for the method and checks whether any filter clause's handler offset matches the funclet's relative offset. If a match is found, the funclet is a filter funclet. +### FindReadyToRunModule + +`FindReadyToRunModule` locates the ReadyToRun module whose PE image contains the given address. Unlike `GetCodeBlockHandle` (which only matches code regions), this API matches against the full PE image range - including data sections such as import tables. This is used in GCRefMap resolution as it requires finding the module that owns an import section indirection address, which is in the data section rather than the code section. + +```csharp +TargetPointer IExecutionManager.FindReadyToRunModule(TargetPointer address) +{ + // Use the RangeSectionMap to find the RangeSection containing the address. + // ReadyToRun range sections cover the entire PE image (code + data), + // so this works for import section addresses used by GCRefMap lookup. + RangeSection range = RangeSection.Find(target, topRangeSectionMap, address); + if (range.Data is null) + return TargetPointer.Null; + + return range.Data.R2RModule; +} +``` + ### EE JIT Manager and Code Heap Info ```csharp diff --git a/docs/design/datacontracts/GCInfo.md b/docs/design/datacontracts/GCInfo.md index c878957aea5001..963d7eb21514bc 100644 --- a/docs/design/datacontracts/GCInfo.md +++ b/docs/design/datacontracts/GCInfo.md @@ -2,6 +2,8 @@ This contract is for fetching information related to GCInfo associated with native code. Currently, this contract does not support x86 architecture. +The GCInfo contract has platform specific implementations as GCInfo differs per architecture. With the exception of x86, all platforms have a common encoding scheme with different encoding lengths and normalization functions for data. x86 uses an entirely different scheme which is not currently supported by this contract. + ## APIs of contract ```csharp @@ -19,6 +21,40 @@ IGCInfoHandle DecodeInterpreterGCInfo(TargetPointer gcInfoAddress, uint gcVersio // Fetches length of code as reported in GCInfo uint GetCodeLength(IGCInfoHandle handle); + +// Returns the stack base register number decoded from GCInfo +uint GetStackBaseRegister(IGCInfoHandle handle); + +// Returns the list of interruptible code offset ranges from the GCInfo +IReadOnlyList GetInterruptibleRanges(IGCInfoHandle handle); + +// Returns all live GC slots at the given instruction offset +IReadOnlyList EnumerateLiveSlots(IGCInfoHandle handle, uint instructionOffset, GcSlotEnumerationOptions options); +``` + +```csharp +// Describes a code region where the GC can safely interrupt execution. +public readonly record struct InterruptibleRange( + uint StartOffset, // Start of the interruptible region (byte offset from method start) + uint EndOffset); // End of the interruptible region, exclusive (byte offset from method start) + +// Describes a live GC slot at a given instruction offset. +public readonly record struct LiveSlot( + bool IsRegister, // True if the slot is a CPU register; false if stack location + uint RegisterNumber, // Register number (meaningful only when IsRegister is true) + int SpOffset, // Stack offset from the base (meaningful only when IsRegister is false) + uint SpBase, // Stack base: 0 = CALLER_SP_REL, 1 = SP_REL, 2 = FRAMEREG_REL + uint GcFlags); // GC slot flags: 0x1 = interior pointer, 0x2 = pinned + +// Options controlling which GC slots are reported by EnumerateLiveSlots. +public record struct GcSlotEnumerationOptions +{ + bool IsActiveFrame; // True if this is the active (leaf) stack frame + bool IsExecutionAborted; // True if execution was interrupted by an exception + bool IsParentOfFuncletStackFrame; // True if a funclet already reported GC references + bool SuppressUntrackedSlots; // True to suppress untracked slots (e.g., filter funclets) + bool ReportFPBasedSlotsOnly; // True to report only frame-register-relative stack slots +} ``` ## Version 1 @@ -44,10 +80,6 @@ Constants: | `NO_PSP_SYM` | Indicates no PSP symbol | -1 | -## Implementation - -The GCInfo contract has platform specific implementations as GCInfo differs per architecture. With the exception of x86, all platforms have a common encoding scheme with different encoding lengths and normalization functions for data. x86 uses an entirely different scheme which is not currently supported by this contract. - ### GCInfo Format The GCInfo format consists of a header structure and following data types. The header is either 'slim' for simple methods that can use the compact encoding scheme or a 'fat' header containing more details. @@ -312,7 +344,7 @@ Signed values use the same encoding as unsigned, but with sign considerations: ### Implementation -The GCInfo contract implementation follows this process: +The GCInfo decoder uses **lazy sequential decoding** — data is decoded on demand as APIs are called, and each section of the bitstream is decoded at most once. The decoder tracks a set of `DecodePoints` that represent completion of each section. When an API like `GetCodeLength()` or `GetInterruptibleRanges()` is called, the decoder advances through the bitstream until the requested data has been decoded. ```csharp IGCInfoHandle DecodePlatformSpecificGCInfo(TargetPointer gcInfoAddress, uint gcVersion) @@ -326,13 +358,231 @@ IGCInfoHandle DecodeInterpreterGCInfo(TargetPointer gcInfoAddress, uint gcVersio // Create a new decoder instance using the interpreter encoding return new GcInfoDecoder(target, gcInfoAddress, gcVersion); } +``` + +#### Header Decoding + +The first bit of the GCInfo bitstream determines whether the header is **slim** or **fat**. + +**Slim Header** (first bit = 0): + +The slim header is a compact encoding for simple methods. It reads only a few fields: + +``` +isSlimHeader = ReadBits(1) // 0 = slim +usingStackBaseRegister = ReadBits(1) +if usingStackBaseRegister: + stackBaseRegister = DenormalizeStackBaseRegister(0) +codeLength = DenormalizeCodeLength(DecodeVarLengthUnsigned(CODE_LENGTH_ENCBASE)) +numSafePoints = DecodeVarLengthUnsigned(NUM_SAFE_POINTS_ENCBASE) +numInterruptibleRanges = 0 // slim header never has interruptible ranges +``` + +All optional fields (GS cookie, PSP symbol, generics context, EnC info, reverse P/Invoke) default to their sentinel "not present" values. + +**Fat Header** (first bit = 1): + +The fat header contains a full flags bitfield and conditionally-present optional fields: + +``` +isSlimHeader = ReadBits(1) // 1 = fat +headerFlags = ReadBits(GC_INFO_FLAGS_BIT_SIZE) // 10 bits +codeLength = DenormalizeCodeLength(DecodeVarLengthUnsigned(CODE_LENGTH_ENCBASE)) + +// Prolog/epilog sizes (conditional on GS cookie or generics context) +if HAS_GS_COOKIE: + normPrologSize = DecodeVarLengthUnsigned(NORM_PROLOG_SIZE_ENCBASE) + 1 + normEpilogSize = DecodeVarLengthUnsigned(NORM_EPILOG_SIZE_ENCBASE) +elif HAS_GENERICS_INST_CONTEXT: + normPrologSize = DecodeVarLengthUnsigned(NORM_PROLOG_SIZE_ENCBASE) + 1 + +// Optional fields (each conditional on its header flag) +if HAS_GS_COOKIE: + gsCookieStackSlot = DenormalizeStackSlot(DecodeVarLengthSigned(GS_COOKIE_STACK_SLOT_ENCBASE)) +if HAS_GENERICS_INST_CONTEXT: + genericsInstContextStackSlot = DenormalizeStackSlot(DecodeVarLengthSigned(...)) +if HAS_STACK_BASE_REGISTER: + stackBaseRegister = DenormalizeStackBaseRegister(DecodeVarLengthUnsigned(...)) +if HAS_EDIT_AND_CONTINUE_INFO: + sizeOfEnCPreservedArea = DecodeVarLengthUnsigned(...) + if ARM64: sizeOfEnCFixedStackFrame = DecodeVarLengthUnsigned(...) +if REVERSE_PINVOKE_FRAME: + reversePInvokeFrameStackSlot = DenormalizeStackSlot(DecodeVarLengthSigned(...)) +if HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA: // platform-dependent + fixedStackParameterScratchArea = DenormalizeSizeOfStackArea(DecodeVarLengthUnsigned(...)) + +numSafePoints = DecodeVarLengthUnsigned(NUM_SAFE_POINTS_ENCBASE) +numInterruptibleRanges = DecodeVarLengthUnsigned(NUM_INTERRUPTIBLE_RANGES_ENCBASE) +``` + +#### Body Decoding + +Following the header, the GCInfo body contains data sections that must be decoded in strict order: + +##### 1. Safe Point Offsets + +Safe points (also called call sites) are code offsets where the GC can safely interrupt execution for partially-interruptible methods. Each offset is encoded as a fixed-width bitfield: + +``` +numBitsPerOffset = CeilOfLog2(NormalizeCodeOffset(codeLength)) +for each safe point: + offset = ReadBits(numBitsPerOffset) // normalized code offset +``` + +The offsets are stored in sorted order to enable binary search during `EnumerateLiveSlots`. + +##### 2. Interruptible Ranges + +Interruptible ranges define code regions where the method is **fully interruptible** — the GC can interrupt at any instruction within these ranges. Each range is encoded as a pair of delta-compressed, normalized offsets: + +``` +lastStopNormalized = 0 + +for each range: + startDelta = DecodeVarLengthUnsigned(INTERRUPTIBLE_RANGE_DELTA1_ENCBASE) + stopDelta = DecodeVarLengthUnsigned(INTERRUPTIBLE_RANGE_DELTA2_ENCBASE) + 1 + + startNormalized = lastStopNormalized + startDelta + stopNormalized = startNormalized + stopDelta + + startOffset = DenormalizeCodeOffset(startNormalized) + stopOffset = DenormalizeCodeOffset(stopNormalized) + + emit InterruptibleRange(startOffset, stopOffset) + lastStopNormalized = stopNormalized +``` + +##### 3. Slot Table + +The slot table describes all GC-tracked locations used by the method. It has three sections decoded in order: register slots, tracked stack slots, and untracked stack slots. +**Slot counts** are encoded with presence bits: + +``` +if ReadBits(1): // has register slots + numRegisters = DecodeVarLengthUnsigned(NUM_REGISTERS_ENCBASE) +if ReadBits(1): // has stack/untracked slots + numStackSlots = DecodeVarLengthUnsigned(NUM_STACK_SLOTS_ENCBASE) + numUntrackedSlots = DecodeVarLengthUnsigned(NUM_UNTRACKED_SLOTS_ENCBASE) +``` + +**Register slots** use delta encoding when consecutive slots share the same flags: + +``` +// First slot: absolute register number + 2-bit flags +regNum = DecodeVarLengthUnsigned(REGISTER_ENCBASE) +flags = ReadBits(2) + +// Subsequent slots: +if previousFlags != 0: + regNum = DecodeVarLengthUnsigned(REGISTER_ENCBASE) // absolute + flags = ReadBits(2) +else: + regNum += DecodeVarLengthUnsigned(REGISTER_DELTA_ENCBASE) + 1 // delta + // flags inherited from previous +``` + +**Stack slots** follow a similar delta encoding pattern: + +``` +// First slot: base (2 bits) + normalized offset + flags (2 bits) +spBase = ReadBits(2) // CALLER_SP_REL, SP_REL, or FRAMEREG_REL +normSpOffset = DecodeVarLengthSigned(STACK_SLOT_ENCBASE) +spOffset = DenormalizeStackSlot(normSpOffset) +flags = ReadBits(2) + +// Subsequent slots: +spBase = ReadBits(2) +if previousFlags != 0: + normSpOffset = DecodeVarLengthSigned(STACK_SLOT_ENCBASE) // absolute + flags = ReadBits(2) +else: + normSpOffset += DecodeVarLengthUnsigned(STACK_SLOT_DELTA_ENCBASE) // delta + // flags inherited from previous +``` + +Untracked slots use the same encoding as tracked stack slots. + +The 2-bit slot flags are: + +| Flag | Value | Meaning | +| --- | --- | --- | +| `GC_SLOT_BASE` | 0x0 | Normal object reference | +| `GC_SLOT_INTERIOR` | 0x1 | Interior pointer (points inside an object) | +| `GC_SLOT_PINNED` | 0x2 | Pinned object reference | + +##### 4. Live State Data + +Following the slot table, the remaining bitstream contains per-safe-point and per-chunk liveness information used by `EnumerateLiveSlots` to determine which slots are live at a given instruction offset. This data uses either a direct 1-bit-per-slot encoding or RLE (run-length encoding) compression for methods with many tracked slots. + +For **partially interruptible** methods (at safe points), each safe point has a bitvector indicating which tracked slots are live. An optional indirection table allows sharing identical bitvectors across safe points. + +For **fully interruptible** methods (within interruptible ranges), the interruptible region is divided into fixed-size chunks (`NUM_NORM_CODE_OFFSETS_PER_CHUNK = 64` normalized offsets). Each chunk records a "could be live" bitvector, a final state bitvector, and transition points within the chunk where slot liveness changes. + +### EnumerateLiveSlots + +`EnumerateLiveSlots` determines which GC-tracked slots (registers and stack locations) are live at a given instruction offset, then reports each live slot via a callback. The algorithm handles two distinct cases depending on whether the instruction offset falls at a **safe point** (partially-interruptible) or within an **interruptible range** (fully-interruptible). + +**Input**: instruction offset, `GcSlotEnumerationOptions`, slot report callback. + +**Step 1 — Find safe point**: Search the safe point offset table for an exact match against the normalized instruction offset. If found, the safe point index is used for the partially-interruptible path. + +**Step 2 — Partially-interruptible path** (safe point found, not `ExecutionAborted`): + +Each safe point has a bitvector with one bit per tracked slot. If the bit is set, the slot is live. An optional **indirection table** allows sharing identical bitvectors across safe points — when present, each safe point stores an offset into a deduplicated bitvector table. The bitvectors may use either direct 1-bit-per-slot encoding or **RLE** (run-length encoding) for methods with many tracked slots. + +**Step 3 — Fully-interruptible path** (no safe point match, offset is within an interruptible range): + +The total interruptible length is computed by summing all interruptible range sizes. A **pseudo-offset** maps the instruction offset into this linear space. The interruptible region is divided into fixed-size **chunks** of 64 normalized offsets each. + +For each chunk, the encoding stores: +- A **couldBeLive** bitvector identifying which slots may be live anywhere in the chunk (1-bit-per-slot or RLE). +- A **finalState** bit per couldBeLive slot indicating liveness at the end of the chunk. +- **Transition points** within the chunk where each slot's liveness toggles. + +To determine liveness at the target offset: start from the chunk's final state, then apply any transitions that occur *after* the target offset (toggling the state backwards). A slot is live if its final state (after toggle adjustment) is 1. + +**Step 4 — Report untracked slots**: Untracked slots are always live (they represent stack locations the JIT doesn't track at each safe point). They are reported unconditionally unless `ParentOfFuncletStackFrame` or `NoReportUntracked` flags are set. Untracked slots are reported with `reportScratchSlots=true` since the JIT may produce untracked scratch register slots for interior pointers. + +**Slot filtering**: Before reporting any slot, the algorithm checks: +- **Scratch registers**: Only reported for the active/leaf frame (`ActiveStackFrame` flag). +- **Scratch stack slots**: Only reported for the active/leaf frame (slots in the outgoing/scratch area). +- **FP-based-only mode** (`ReportFPBasedSlotsOnly`): Only frame-register-relative stack slots are reported; all register slots and non-frame-relative stack slots are skipped. + +#### API Implementations + +All APIs use lazy decoding — the GCInfo bitstream is decoded up to the required point on first access, and cached for subsequent calls. + +```csharp uint GetCodeLength(IGCInfoHandle handle) { - // Cast to the appropriate decoder type and return the decoded code length - GcInfoDecoder decoder = (GcInfoDecoder)handle; - return decoder.GetCodeLength(); + // Ensure header is decoded, then return the code length field. +} + +uint GetStackBaseRegister(IGCInfoHandle handle) +{ + // Ensure header is decoded through the stack base register field, + // then return the denormalized register number (e.g., RBP on x64). } -``` -The decoder reads and parses the GCInfo data structure sequentially, using the platform-specific encoding bases and normalization rules to reconstruct the original method metadata. +IReadOnlyList GetInterruptibleRanges(IGCInfoHandle handle) +{ + // Ensure header and body are decoded through interruptible ranges, + // then return the decoded range list. +} + +IReadOnlyList EnumerateLiveSlots(IGCInfoHandle handle, + uint instructionOffset, GcSlotEnumerationOptions options) +{ + // Ensure header, body, and slot table are fully decoded. + // Then execute the EnumerateLiveSlots algorithm described above: + // 1. Find safe point match for the normalized instruction offset + // 2. If found: read the per-safe-point bitvector (partially-interruptible path) + // 3. If not found: compute pseudo-offset into interruptible ranges, + // locate the chunk, read couldBeLive/finalState/transitions + // (fully-interruptible path) + // 4. Report untracked slots unconditionally (unless SuppressUntrackedSlots) + // 5. Apply slot filtering (scratch registers, FP-based-only mode) + // Collect each live slot into a list and return it. +} +``` diff --git a/docs/design/datacontracts/RuntimeTypeSystem.md b/docs/design/datacontracts/RuntimeTypeSystem.md index d3f86c8bf0d74d..08adc08c19855b 100644 --- a/docs/design/datacontracts/RuntimeTypeSystem.md +++ b/docs/design/datacontracts/RuntimeTypeSystem.md @@ -190,6 +190,14 @@ partial interface IRuntimeTypeSystem : IContract // Return true if a MethodDesc represents an IL stub with a special MethodDesc context arg public virtual bool HasMDContextArg(MethodDescHandle); + // Return true if the method requires a hidden instantiation argument (generic context parameter). + // Corresponds to native MethodDesc::RequiresInstArg(). + public virtual bool RequiresInstArg(MethodDescHandle methodDesc); + + // Return true if the method uses the async calling convention. + // Corresponds to native MethodDesc::IsAsyncMethod(). + public virtual bool IsAsyncMethod(MethodDescHandle methodDesc); + // Return true if a MethodDesc is in a collectible module public virtual bool IsCollectibleMethod(MethodDescHandle methodDesc); @@ -1150,6 +1158,7 @@ And the following enumeration definitions HasMethodImpl = 0x0010, HasNativeCodeSlot = 0x0020, HasAsyncMethodData = 0x0040, + Static = 0x0080, // Mask for the above flags MethodDescAdditionalPointersMask = 0x0038, #endredion Additional pointers @@ -1600,6 +1609,57 @@ Determining if a method is an async thunk method: } ``` +Determining if a method requires a hidden instantiation argument (generic context parameter): + +```csharp + public bool RequiresInstArg(MethodDescHandle methodDescHandle) + { + MethodDesc md = _methodDescs[methodDescHandle.Address]; + + // RequiresInstArg = IsSharedByGenericInstantiations && (HasMethodInstantiation || IsStatic || IsValueType || IsInterface) + if (!IsSharedByGenericInstantiations(md)) + return false; + + if (HasMethodInstantiation(md)) + return true; + + // md.IsStatic reads from MethodDescFlags.Static (0x0080) + if (md.IsStatic) + return true; + + MethodTable mt = _methodTables[md.MethodTable]; + return mt.Flags.IsInterface || mt.Flags.IsValueType; + } + + private bool IsSharedByGenericInstantiations(MethodDesc md) + { + if (md.Classification == MethodClassification.Instantiated) + { + InstantiatedMethodDesc imd = AsInstantiatedMethodDesc(md); + if (imd.IsWrapperStubWithInstantiations) + return false; + if (/* Flags2 of InstantiatedMethodDesc has SharedMethodInstantiation set */) + return true; + } + MethodTable mt = _methodTables[md.MethodTable]; + return mt.IsCanonMT && mt.Flags.HasInstantiation; + } +``` + +Determining if a method uses the async calling convention: + +```csharp + public bool IsAsyncMethod(MethodDescHandle methodDescHandle) + { + MethodDesc md = _methodDescs[methodDescHandle.Address]; + if (!md.HasAsyncMethodData) + return false; + + Data.AsyncMethodData asyncData = // Read AsyncMethodData from the async method data optional slot + return ((AsyncMethodFlags)asyncData.Flags).HasFlag(AsyncMethodFlags.AsyncCall); + } +``` + Determining if a method is a wrapper stub (unboxing or instantiating): ```csharp diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index 9d254591ddb0ac..35c8a33e00c2c7 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -28,6 +28,10 @@ TargetPointer GetMethodDescPtr(IStackDataFrameHandle stackDataFrameHandle); // Gets the instruction pointer from the current frame's context. TargetPointer GetInstructionPointer(IStackDataFrameHandle stackDataFrameHandle); + +// Walks the stack and returns all GC references found on each frame. +// This is the primary API for GC reference enumeration, used by SOSDacImpl.GetStackReferences. +IReadOnlyList WalkStackReferences(ThreadData threadData); ``` ## Version 1 @@ -60,12 +64,19 @@ This contract depends on the following descriptors: | `StubDispatchFrame` | `MethodDescPtr` | Pointer to Frame's method desc | | `StubDispatchFrame` | `RepresentativeMTPtr` | Pointer to Frame's method table pointer | | `StubDispatchFrame` | `RepresentativeSlot` | Frame's method table slot | +| `StubDispatchFrame` | `Indirection` | Import slot pointer for GCRefMap resolution via `FindReadyToRunModule` | +| `ExternalMethodFrame` | `Indirection` | Import slot pointer for GCRefMap resolution via `FindReadyToRunModule` | +| `DynamicHelperFrame` | `DynamicHelperFrameFlags` | Flags indicating which argument registers contain GC references | | `TransitionBlock` | `ReturnAddress` | Return address associated with the TransitionBlock | | `TransitionBlock` | `CalleeSavedRegisters` | Platform specific CalleeSavedRegisters struct associated with the TransitionBlock | -| `TransitionBlock` (arm) | `ArgumentRegisters` | ARM specific `ArgumentRegisters` struct | +| `TransitionBlock` | `OffsetOfArgs` | Byte offset of stack arguments (first arg after registers) = `sizeof(TransitionBlock)` | +| `TransitionBlock` | `ArgumentRegisters` | Byte offset of the argument registers area within the TransitionBlock | +| `TransitionBlock` | `FirstGCRefMapSlot` | Byte offset where GCRefMap slot enumeration begins. ARM64: RetBuffArgReg offset; others: ArgumentRegisters offset | +| `ReadyToRunInfo` | `ImportSections` | Pointer to array of `READYTORUN_IMPORT_SECTION` structs for GCRefMap resolution | +| `ReadyToRunInfo` | `NumImportSections` | Count of import sections in the array | | `FuncEvalFrame` | `DebuggerEvalPtr` | Pointer to the Frame's DebuggerEval object | | `DebuggerEval` | `TargetContext` | Context saved inside DebuggerEval | -| `DebuggerEval` | `EvalDuringException` | Flag used in processing FuncEvalFrame | +| `DebuggerEval` | `EvalUsesHijack` | Flag used in processing FuncEvalFrame | | `ResumableFrame` | `TargetContextPtr` | Pointer to the Frame's Target Context | | `FaultingExceptionFrame` | `TargetContext` | Frame's Target Context | | `HijackFrame` | `ReturnAddress` | Frame's stored instruction pointer | @@ -85,6 +96,8 @@ This contract depends on the following descriptors: | `ExceptionInfo` | `CallerOfActualHandlerFrame` | Stack frame of the caller of the catch handler | | `ExceptionInfo` | `PreviousNestedInfo` | Pointer to previous nested ExInfo | | `ExceptionInfo` | `PassNumber` | Exception handling pass (1 or 2) | +| `ExceptionInfo` | `ClauseForCatchHandlerStartPC` | Start PC offset of the catch handler clause, used for interruptible offset override | +| `ExceptionInfo` | `ClauseForCatchHandlerEndPC` | End PC offset of the catch handler clause, used for interruptible offset override | Global variables used: | Global Name | Type | Purpose | @@ -102,6 +115,7 @@ Contracts used: | `ExecutionManager` | | `Thread` | | `RuntimeTypeSystem` | +| `GCInfo` | ### Stackwalk Algorithm @@ -277,10 +291,14 @@ InlinedCallFrames store and update only the IP, SP, and FP of a given context. I * On ARM, the InlinedCallFrame stores the value of the SP after the prolog (`SPAfterProlog`) to allow unwinding for functions with stackalloc. When a function uses stackalloc, the CallSiteSP can already have been adjusted. This value should be placed in R9. +**Return Address**: `CallerReturnAddress`, but only when the frame has an active call (i.e., `CallerReturnAddress != 0`). Returns null otherwise. + #### SoftwareExceptionFrame SoftwareExceptionFrames store a copy of the context struct. The IP, SP, and all ABI specified (platform specific) callee-saved registers are copied from the stored context to the working context. +**Return Address**: Read from the `ReturnAddress` field on the frame. + #### TransitionFrame TransitionFrames hold a pointer to a `TransitionBlock`. The TransitionBlock holds a return address along with a `CalleeSavedRegisters` struct which has values for all ABI specified callee-saved registers. The SP can be found using the address of the TransitionBlock. Since the TransitionBlock will be the lowest element on the stack, the SP is the address of the TransitionBlock + sizeof(TransitionBlock). @@ -289,6 +307,8 @@ When updating the context from a TransitionFrame, the IP, SP, and all ABI specif * On ARM, the additional register values stored in `ArgumentRegisters` are copied over. The `TransitionBlock` holds a pointer to the `ArgumentRegister` struct containing these values. +**Return Address**: Read from `TransitionBlock.ReturnAddress`. This applies to all frame types that use the TransitionFrame mechanism. + The following Frame types also use this mechanism: * FramedMethodFrame * PInvokeCallIFrame @@ -302,12 +322,16 @@ The following Frame types also use this mechanism: FuncEvalFrames hold a pointer to a `DebuggerEval`. The DebuggerEval holds a full context which is completely copied over to the working context when updating. +**Return Address**: Returns null when using hijack evaluation (`EvalUsesHijack`). Otherwise, read from `TransitionBlock.ReturnAddress` like other TransitionFrame types. + #### ResumableFrame ResumableFrames hold a pointer to a context object (Note this is different from SoftwareExceptionFrames which hold the context directly). The entire context object is copied over to the working context when updating. RedirectedThreadFrames also use this mechanism. +**Return Address**: Extracted from the saved context's instruction pointer (`TargetContextPtr` -> context IP). + #### FaultingExceptionFrame FaultingExceptionFrames have two different implementations. One for Windows x86 and another for all other builds (with funclets). @@ -316,10 +340,14 @@ Given the cDAC does not yet support Windows x86, this version is not supported. The other version stores a context struct. To update the working context, the entire stored context is copied over. In addition the `ContextFlags` are updated to ensure the `CONTEXT_XSTATE` bit is not set given the debug version of the contexts can not store extended state. This bit is architecture specific. +**Return Address**: Extracted from the saved context's instruction pointer (`TargetContext` -> context IP). + #### HijackFrame HijackFrames carry a IP (ReturnAddress) and a pointer to `HijackArgs`. All platforms update the IP and use the platform specific HijackArgs to update further registers. The following details currently implemented platforms. +**Return Address**: Read from the `ReturnAddress` field directly. + * x64 - On x64, HijackArgs contains a CalleeSavedRegister struct. The saved registers values contained in the struct are copied over to the working context. * Windows - On Windows, HijackArgs also contains the SP value directly which is copied over to the working context. * Non-Windows - On OS's other than Windows, HijackArgs does not contain an SP value. Instead since the HijackArgs struct lives on the stack, the SP is `&hijackArgs + sizeof(HijackArgs)`. This value is also copied over. @@ -331,6 +359,8 @@ HijackFrames carry a IP (ReturnAddress) and a pointer to `HijackArgs`. All platf TailCallFrames only appear on x86 Windows. They hold a `CalleeSavedRegisters` struct as well as a `ReturnAddress`. While the stack pointer is not directly contained in the TailCallFrame structure, it will be on the stack immediately following the Frame (found at the address of the Frame + size of the Frame). To process these Frames, update all of the registers in `CalleeSavedRegisters`, the instruction pointer from the stored return address, and the stack pointer from the address saved on the stack. +**Return Address**: Read from the `ReturnAddress` field directly. + ### APIs The majority of the contract's complexity is the stack walking algorithm (detailed above) implemented as part of `CreateStackWalk`. @@ -399,6 +429,78 @@ TargetPointer GetMethodDescPtr(IStackDataFrameHandle stackDataFrameHandle) TargetPointer GetInstructionPointer(IStackDataFrameHandle stackDataFrameHandle) ``` +`WalkStackReferences` walks the entire managed stack and enumerates all live GC references at each frame. It returns a list of `StackReferenceData` describing each GC-tracked slot (its address, whether it's an interior pointer, and the register/stack location). This API is the primary consumer for `SOSDacImpl.GetStackReferences`. + +```csharp +IReadOnlyList WalkStackReferences(ThreadData threadData) +``` + +The implementation uses the same stack walk algorithm as `CreateStackWalk`, but integrates the GC-aware `Filter` directly (rather than consuming pre-generated frames) and performs GC reference enumeration at each frame. See [GC Stack Reference Scanning](#gc-stack-reference-scanning) for details. + +### GC Stack Reference Scanning + +`WalkStackReferences` scans the stack for GC references by walking through each frame and reporting live object references and interior pointers. The native equivalent is `DacStackReferenceWalker` which calls `GcStackCrawlCallBack` at each frame. + +#### Stack Walk Integration + +The GC reference walk uses the `Filter` function to drive the stack walk. `Filter` is a port of native `StackFrameIterator::Filter` (with `GC_FUNCLET_REFERENCE_REPORTING` mode) that handles funclet-to-parent frame transitions, exception tracker correlation, and determines whether each frame should report GC references. Unlike `CreateStackWalk` which yields all frames, `Filter` calls `Next()` directly and may skip frames that don't contribute GC roots. + +Key state tracked during the walk: + +- **IsInterrupted**: Set when transitioning to a managed frame from a `FaultingExceptionFrame` or `SoftwareExceptionFrame` (frames with `FRAME_ATTR_EXCEPTION`). When true, the managed frame's GC enumeration uses `ExecutionAborted` mode, which causes the GcInfoDecoder to skip live slot reporting at non-interruptible offsets. +- **LastProcessedFrameType**: Records the frame type when processing `SW_FRAME` state, so `UpdateState` can detect exception frames during the transition to `SW_FRAMELESS`. +- **IsFirst**: Preserved during skipped frame processing (native `SFITER_SKIPPED_FRAME_FUNCTION` does not modify `IsFirst`), ensuring the subsequent managed frame is still treated as the leaf/active frame. +- **GetReturnAddress gating**: In `SW_FRAME` state, `UpdateContextFromFrame` is only called when `GetReturnAddress()` returns a non-null value. This matches native behavior where the context is only updated when the frame has a valid return address. + +#### Per-Frame GC Enumeration + +At each frame yielded by `Filter`, the walk determines whether to scan for GC references: + +**Managed (frameless) frames** use `EnumGcRefsForManagedFrame`: + +1. Get the code block handle and relative offset from the `ExecutionManager` contract. +2. Decode the GCInfo for the code block via the `GCInfo` contract. +3. Determine `GcSlotEnumerationOptions`: set `IsActiveFrame` if this is the leaf frame (`IsFirst`), `IsExecutionAborted` if the frame was interrupted, `IsParentOfFuncletStackFrame` if funclet GC reporting was delegated to the parent, `SuppressUntrackedSlots` if the code block is a filter funclet. +4. **Catch handler offset override**: When `ShouldParentFrameUseUnwindTargetPCforGCReporting` is set (parent frame resuming from a catch handler), the GC liveness offset is overridden to the first interruptible point within the catch handler clause range. This uses `GetInterruptibleRanges` from the `GCInfo` contract. See [How EH affects GC info/reporting](../coreclr/botr/clr-abi.md#how-eh-affects-gc-inforeporting) for background on why this override is needed. +5. Call `GcInfoDecoder.EnumerateLiveSlots` with the computed offset and flags to report all live register and stack slots. See the [GCInfo contract — EnumerateLiveSlots](./GCInfo.md#enumerateliveslots) for details on the algorithm. + +**Capital "F" Frames** use `GcScanRoots`, which dispatches based on frame type: + +- **StubDispatchFrame / ExternalMethodFrame**: Resolve GCRefMap via `FindGCRefMap` using the frame's `Indirection` pointer, otherwise fall back to signature-based scanning. +- **DynamicHelperFrame**: Use flag-based scanning (`DynamicHelperFrameFlags`). +- **PrestubMethodFrame / CallCountingHelperFrame**: Use signature-based scanning. +- Other frame types: No GC roots to report. + +See [GCRefMap Format and Resolution](#gcrefmap-format-and-resolution) for the GCRefMap scanning path details. + +### GCRefMap Format and Resolution + +A **GCRefMap** is a compact per-callsite encoding that describes which stack slots in a `TransitionBlock` contain GC references. GCRefMaps are pre-computed by the ReadyToRun compiler and stored in the PE image's import section auxiliary data. + +The GCRefMap encoding format — including token values, bit encoding, lookup table structure, and per-architecture position semantics — is documented in the [ReadyToRun format specification](../coreclr/botr/readytorun-format.md#readytorun_import_sectionsauxiliarydata). + +#### Resolution Flow + +GCRefMap resolution from a frame's `Indirection` pointer proceeds as follows: + +1. Call `FindReadyToRunModule(indirection)` (see [ExecutionManager contract](./ExecutionManager.md)) to find the ReadyToRun module containing the import slot. +2. Load the module's `ReadyToRunInfo` to access the import section array. +3. Compute the RVA of the indirection address: `rva = indirection - imageBase`. +4. Search through `READYTORUN_IMPORT_SECTION` entries to find the section containing the RVA. +5. Compute the slot index within the section: `index = (rva - sectionVA) / entrySize`. +6. Use the section's `AuxiliaryData` RVA to locate the GCRefMap lookup table. +7. Use stride-based lookup (stride = 1024) plus linear scan to find the specific GCRefMap entry. + +#### Slot Mapping + +GCRefMap positions map to `TransitionBlock` offsets using the formula: + +```csharp +slotAddress = transitionBlockPtr + FirstGCRefMapSlot + (position * pointerSize) +``` + +Where `FirstGCRefMapSlot` is the byte offset in the `TransitionBlock` where GCRefMap slot enumeration begins (platform-dependent: on ARM64 it is the return buffer argument register offset; on other platforms it is the argument registers offset). + ### x86 Specifics The x86 platform has some major differences to other platforms. In general this stems from the platform being older and not having a defined unwinding codes. Instead, to unwind managed frames, we rely on GCInfo associated with JITted code. For the unwind, we do not defer to a 'Windows like' native unwinder, instead the custom unwinder implementation was ported to managed code. diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 40a9397767d289..ff8738e9860743 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -146,6 +146,8 @@ CDAC_TYPE_FIELD(ExceptionInfo, T_UINT8, PassNumber, offsetof(ExInfo, m_passNumbe CDAC_TYPE_FIELD(ExceptionInfo, T_POINTER, CSFEHClause, offsetof(ExInfo, m_csfEHClause)) CDAC_TYPE_FIELD(ExceptionInfo, T_POINTER, CSFEnclosingClause, offsetof(ExInfo, m_csfEnclosingClause)) CDAC_TYPE_FIELD(ExceptionInfo, T_POINTER, CallerOfActualHandlerFrame, offsetof(ExInfo, m_sfCallerOfActualHandlerFrame)) +CDAC_TYPE_FIELD(ExceptionInfo, T_UINT32, ClauseForCatchHandlerStartPC, offsetof(ExInfo, m_ClauseForCatch) + offsetof(EE_ILEXCEPTION_CLAUSE, HandlerStartPC)) +CDAC_TYPE_FIELD(ExceptionInfo, T_UINT32, ClauseForCatchHandlerEndPC, offsetof(ExInfo, m_ClauseForCatch) + offsetof(EE_ILEXCEPTION_CLAUSE, HandlerEndPC)) CDAC_TYPE_END(ExceptionInfo) CDAC_TYPE_BEGIN(ObjectHandle) @@ -732,6 +734,8 @@ CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, HotColdMap, cdac_data CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, DelayLoadMethodCallThunks, cdac_data::DelayLoadMethodCallThunks) CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, DebugInfoSection, cdac_data::DebugInfoSection) CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, ExceptionInfoSection, cdac_data::ExceptionInfoSection) +CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, ImportSections, cdac_data::ImportSections) +CDAC_TYPE_FIELD(ReadyToRunInfo, T_UINT32, NumImportSections, cdac_data::NumImportSections) CDAC_TYPE_FIELD(ReadyToRunInfo, TYPE(HashMap), EntryPointToMethodDescMap, cdac_data::EntryPointToMethodDescMap) CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, LoadedImageBase, cdac_data::LoadedImageBase) CDAC_TYPE_FIELD(ReadyToRunInfo, T_POINTER, Composite, cdac_data::Composite) @@ -976,9 +980,19 @@ CDAC_TYPE_BEGIN(TransitionBlock) CDAC_TYPE_SIZE(sizeof(TransitionBlock)) CDAC_TYPE_FIELD(TransitionBlock, T_POINTER, ReturnAddress, offsetof(TransitionBlock, m_ReturnAddress)) CDAC_TYPE_FIELD(TransitionBlock, TYPE(CalleeSavedRegisters), CalleeSavedRegisters, offsetof(TransitionBlock, m_calleeSavedRegisters)) -#ifdef TARGET_ARM -CDAC_TYPE_FIELD(TransitionBlock, TYPE(ArgumentRegisters), ArgumentRegisters, offsetof(TransitionBlock, m_argumentRegisters)) -#endif // TARGET_ARM +// Offset to where stack arguments begin (just past the end of the TransitionBlock) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, OffsetOfArgs, sizeof(TransitionBlock)) +// Offset to argument registers and first GCRefMap slot (platform-specific) +#if (defined(TARGET_AMD64) && !defined(UNIX_AMD64_ABI)) || defined(TARGET_WASM) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, ArgumentRegisters, sizeof(TransitionBlock)) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, FirstGCRefMapSlot, sizeof(TransitionBlock)) +#elif defined(TARGET_ARM64) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, ArgumentRegisters, offsetof(TransitionBlock, m_argumentRegisters)) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, FirstGCRefMapSlot, offsetof(TransitionBlock, m_x8RetBuffReg)) +#else +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, ArgumentRegisters, offsetof(TransitionBlock, m_argumentRegisters)) +CDAC_TYPE_FIELD(TransitionBlock, T_UINT32, FirstGCRefMapSlot, offsetof(TransitionBlock, m_argumentRegisters)) +#endif CDAC_TYPE_END(TransitionBlock) #ifdef DEBUGGING_SUPPORTED @@ -999,8 +1013,19 @@ CDAC_TYPE_SIZE(sizeof(StubDispatchFrame)) CDAC_TYPE_FIELD(StubDispatchFrame, T_POINTER, RepresentativeMTPtr, cdac_data::RepresentativeMTPtr) CDAC_TYPE_FIELD(StubDispatchFrame, T_POINTER, MethodDescPtr, cdac_data::MethodDescPtr) CDAC_TYPE_FIELD(StubDispatchFrame, T_UINT32, RepresentativeSlot, cdac_data::RepresentativeSlot) +CDAC_TYPE_FIELD(StubDispatchFrame, T_POINTER, Indirection, cdac_data::Indirection) CDAC_TYPE_END(StubDispatchFrame) +CDAC_TYPE_BEGIN(ExternalMethodFrame) +CDAC_TYPE_SIZE(sizeof(ExternalMethodFrame)) +CDAC_TYPE_FIELD(ExternalMethodFrame, T_POINTER, Indirection, cdac_data::Indirection) +CDAC_TYPE_END(ExternalMethodFrame) + +CDAC_TYPE_BEGIN(DynamicHelperFrame) +CDAC_TYPE_SIZE(sizeof(DynamicHelperFrame)) +CDAC_TYPE_FIELD(DynamicHelperFrame, T_INT32, DynamicHelperFrameFlags, cdac_data::DynamicHelperFrameFlags) +CDAC_TYPE_END(DynamicHelperFrame) + #ifdef FEATURE_HIJACK CDAC_TYPE_BEGIN(ResumableFrame) CDAC_TYPE_SIZE(sizeof(ResumableFrame)) @@ -1385,6 +1410,7 @@ CDAC_GLOBAL_POINTER(MetadataUpdatesApplied, &::g_metadataUpdatesApplied) #undef FRAME_TYPE_NAME CDAC_GLOBAL(MethodDescTokenRemainderBitCount, T_UINT8, METHOD_TOKEN_REMAINDER_BIT_COUNT) + #if FEATURE_COMINTEROP CDAC_GLOBAL(FeatureCOMInterop, T_UINT8, 1) #else diff --git a/src/coreclr/vm/frames.h b/src/coreclr/vm/frames.h index f3fccab5615efa..00f4d86f2578c3 100644 --- a/src/coreclr/vm/frames.h +++ b/src/coreclr/vm/frames.h @@ -1490,6 +1490,7 @@ struct cdac_data { static constexpr size_t RepresentativeMTPtr = offsetof(StubDispatchFrame, m_pRepresentativeMT); static constexpr uint32_t RepresentativeSlot = offsetof(StubDispatchFrame, m_representativeSlot); + static constexpr size_t Indirection = offsetof(StubDispatchFrame, m_pIndirection); }; typedef DPTR(class StubDispatchFrame) PTR_StubDispatchFrame; @@ -1561,10 +1562,18 @@ class ExternalMethodFrame : public FramedMethodFrame #ifdef TARGET_X86 void UpdateRegDisplay_Impl(const PREGDISPLAY pRD, bool updateFloats = false); #endif + + friend struct ::cdac_data; }; typedef DPTR(class ExternalMethodFrame) PTR_ExternalMethodFrame; +template <> +struct cdac_data +{ + static constexpr size_t Indirection = offsetof(ExternalMethodFrame, m_pIndirection); +}; + class DynamicHelperFrame : public FramedMethodFrame { int m_dynamicHelperFrameFlags; @@ -1583,10 +1592,18 @@ class DynamicHelperFrame : public FramedMethodFrame LIMITED_METHOD_DAC_CONTRACT; return TT_InternalCall; } + + friend struct ::cdac_data; }; typedef DPTR(class DynamicHelperFrame) PTR_DynamicHelperFrame; +template <> +struct cdac_data +{ + static constexpr size_t DynamicHelperFrameFlags = offsetof(DynamicHelperFrame, m_dynamicHelperFrameFlags); +}; + //------------------------------------------------------------------------ // This frame protects object references for the EE's convenience. // This frame type actually is created from C++. diff --git a/src/coreclr/vm/readytoruninfo.h b/src/coreclr/vm/readytoruninfo.h index 6963a5000311e7..64c3324d9b2acc 100644 --- a/src/coreclr/vm/readytoruninfo.h +++ b/src/coreclr/vm/readytoruninfo.h @@ -406,6 +406,8 @@ struct cdac_data static constexpr size_t DelayLoadMethodCallThunks = offsetof(ReadyToRunInfo, m_pSectionDelayLoadMethodCallThunks); static constexpr size_t DebugInfoSection = offsetof(ReadyToRunInfo, m_pSectionDebugInfo); static constexpr size_t ExceptionInfoSection = offsetof(ReadyToRunInfo, m_pSectionExceptionInfo); + static constexpr size_t ImportSections = offsetof(ReadyToRunInfo, m_pImportSections); + static constexpr size_t NumImportSections = offsetof(ReadyToRunInfo, m_nImportSections); static constexpr size_t EntryPointToMethodDescMap = offsetof(ReadyToRunInfo, m_entryPointToMethodDescMap); static constexpr size_t LoadedImageBase = offsetof(ReadyToRunInfo, m_pLoadedImageBase); static constexpr size_t Composite = offsetof(ReadyToRunInfo, m_pComposite); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs index 0dddd31417e5ad..1856c2c3a4ea9d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs @@ -101,6 +101,7 @@ public interface IExecutionManager : IContract List GetExceptionClauses(CodeBlockHandle codeInfoHandle) => throw new NotImplementedException(); JitManagerInfo GetEEJitManagerInfo() => throw new NotImplementedException(); IEnumerable GetCodeHeapInfos() => throw new NotImplementedException(); + TargetPointer FindReadyToRunModule(TargetPointer address) => throw new NotImplementedException(); } public readonly struct ExecutionManager : IExecutionManager diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs index d94ed45048b256..8a70ba34aae189 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs @@ -2,18 +2,57 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; namespace Microsoft.Diagnostics.DataContractReader.Contracts; public interface IGCInfoHandle { } +/// +/// Describes a code region where the GC can safely interrupt execution. +/// +/// Start of the interruptible region, as a byte offset from the method start. +/// End of the interruptible region (exclusive), as a byte offset from the method start. +public readonly record struct InterruptibleRange(uint StartOffset, uint EndOffset); + +/// +/// Describes a live GC slot at a given instruction offset. +/// +/// True if the slot is a CPU register; false if it is a stack location. +/// Register number (meaningful only when IsRegister is true). +/// Stack offset from the base (meaningful only when IsRegister is false). +/// Stack base: 0 = CALLER_SP_REL, 1 = SP_REL, 2 = FRAMEREG_REL. +/// GC slot flags: 0x1 = interior pointer, 0x2 = pinned. +public readonly record struct LiveSlot(bool IsRegister, uint RegisterNumber, int SpOffset, uint SpBase, uint GcFlags); + +/// +/// Options controlling which GC slots are reported by . +/// +public record struct GcSlotEnumerationOptions +{ + /// True if this is the active (leaf) stack frame. When false, scratch register and stack slots are excluded. + public bool IsActiveFrame { get; set; } + /// True if execution was aborted (e.g., interrupted by exception). Skips live slot reporting at non-interruptible offsets. + public bool IsExecutionAborted { get; set; } + /// True if the frame is a parent of a funclet that already reported GC references. + public bool IsParentOfFuncletStackFrame { get; set; } + /// True to suppress reporting of untracked slots (e.g., for filter funclets). + public bool SuppressUntrackedSlots { get; set; } + /// True to report only frame-register-relative stack slots (skips all register slots and non-frame-relative stack slots). + public bool ReportFPBasedSlotsOnly { get; set; } +} + public interface IGCInfo : IContract { static string IContract.Name { get; } = nameof(GCInfo); IGCInfoHandle DecodePlatformSpecificGCInfo(TargetPointer gcInfoAddress, uint gcVersion) => throw new NotImplementedException(); IGCInfoHandle DecodeInterpreterGCInfo(TargetPointer gcInfoAddress, uint gcVersion) => throw new NotImplementedException(); + uint GetCodeLength(IGCInfoHandle handle) => throw new NotImplementedException(); + uint GetStackBaseRegister(IGCInfoHandle handle) => throw new NotImplementedException(); + IReadOnlyList GetInterruptibleRanges(IGCInfoHandle handle) => throw new NotImplementedException(); + IReadOnlyList EnumerateLiveSlots(IGCInfoHandle handle, uint instructionOffset, GcSlotEnumerationOptions options) => throw new NotImplementedException(); } public readonly struct GCInfo : IGCInfo diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs index 7b81723becd276..6bdde7301068bb 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs @@ -185,6 +185,14 @@ public interface IRuntimeTypeSystem : IContract bool IsGenericMethodDefinition(MethodDescHandle methodDesc) => throw new NotImplementedException(); ReadOnlySpan GetGenericMethodInstantiation(MethodDescHandle methodDesc) => throw new NotImplementedException(); + // Return true if the method requires a hidden instantiation argument (generic context parameter). + // This corresponds to native MethodDesc::RequiresInstArg(). + bool RequiresInstArg(MethodDescHandle methodDesc) => throw new NotImplementedException(); + + // Return true if the method uses the async calling convention (CORINFO_CALLCONV_ASYNCCALL). + // This corresponds to native MethodDesc::IsAsyncMethod(). + bool IsAsyncMethod(MethodDescHandle methodDesc) => throw new NotImplementedException(); + // Return mdtMethodDef (0x06000000) if the method doesn't have a token, otherwise return the token of the method uint GetMethodToken(MethodDescHandle methodDesc) => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs index edd009d638a924..9440afccfff014 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs @@ -162,6 +162,9 @@ public enum DataType HijackFrame, TailCallFrame, StubDispatchFrame, + ExternalMethodFrame, + DynamicHelperFrame, + ComCallWrapper, SimpleComCallWrapper, ComMethodTable, diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs index 43e89fbe2237e7..47f629ffd5700b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs @@ -390,6 +390,19 @@ TargetNUInt IExecutionManager.GetRelativeOffset(CodeBlockHandle codeInfoHandle) return info.RelativeOffset; } + TargetPointer IExecutionManager.FindReadyToRunModule(TargetPointer address) + { + // Use the range section map to find the RangeSection containing the address. + // The R2R range section covers the entire PE image (code + data), so this + // works for import section addresses used by FindGCRefMap. + TargetCodePointer codeAddr = CodePointerUtils.CodePointerFromAddress(address, _target); + RangeSection range = RangeSection.Find(_target, _topRangeSectionMap, _rangeSectionMapLookup, codeAddr); + if (range.Data is null) + return TargetPointer.Null; + + return range.Data.R2RModule; + } + JitManagerInfo IExecutionManager.GetEEJitManagerInfo() { TargetPointer eeJitManagerPtr = _target.ReadGlobalPointer(Constants.Globals.EEJitManagerAddress); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs index b636a2914c36ec..c082eb9ccdc969 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs @@ -34,5 +34,6 @@ internal ExecutionManager_1(Target target) public List GetExceptionClauses(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetExceptionClauses(codeInfoHandle); public JitManagerInfo GetEEJitManagerInfo() => _executionManagerCore.GetEEJitManagerInfo(); public IEnumerable GetCodeHeapInfos() => _executionManagerCore.GetCodeHeapInfos(); + public TargetPointer FindReadyToRunModule(TargetPointer address) => _executionManagerCore.FindReadyToRunModule(address); public void Flush() => _executionManagerCore.Flush(); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_2.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_2.cs index 6b84fda982ab5e..da5b1d6dc71f93 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_2.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_2.cs @@ -34,5 +34,6 @@ internal ExecutionManager_2(Target target) public List GetExceptionClauses(CodeBlockHandle codeInfoHandle) => _executionManagerCore.GetExceptionClauses(codeInfoHandle); public JitManagerInfo GetEEJitManagerInfo() => _executionManagerCore.GetEEJitManagerInfo(); public IEnumerable GetCodeHeapInfos() => _executionManagerCore.GetCodeHeapInfos(); + public TargetPointer FindReadyToRunModule(TargetPointer address) => _executionManagerCore.FindReadyToRunModule(address); public void Flush() => _executionManagerCore.Flush(); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs index d6a6a0da8b39f4..aa7d919b8aa8d6 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs @@ -68,8 +68,6 @@ internal enum GcStackSlotBase : uint GC_SPBASE_LAST = GC_FRAMEREG_REL, } - public readonly record struct InterruptibleRange(uint StartOffset, uint EndOffset); - public readonly record struct GcSlotDesc { /* Register Slot */ @@ -514,62 +512,55 @@ public uint GetCodeLength() return _codeLength; } - public IReadOnlyList GetInterruptibleRanges() + public uint GetStackBaseRegister() { - EnsureDecodedTo(DecodePoints.InterruptibleRanges); - return _interruptibleRanges; + EnsureDecodedTo(DecodePoints.ReversePInvoke); + return _stackBaseRegister; } - public uint StackBaseRegister + public IReadOnlyList GetInterruptibleRanges() { - get - { - EnsureDecodedTo(DecodePoints.ReversePInvoke); - return _stackBaseRegister; - } + EnsureDecodedTo(DecodePoints.InterruptibleRanges); + return _interruptibleRanges; } public uint NumTrackedSlots => _numSlots - _numUntrackedSlots; - bool IGCInfoDecoder.EnumerateLiveSlots( + IReadOnlyList IGCInfoDecoder.EnumerateLiveSlots( uint instructionOffset, - CodeManagerFlags flags, - LiveSlotCallback reportSlot) + GcSlotEnumerationOptions options) { - return EnumerateLiveSlots(instructionOffset, flags, + List result = []; + EnumerateLiveSlots(instructionOffset, options, (uint slotIndex, GcSlotDesc slot, uint gcFlags) => { - reportSlot(slot.IsRegister, slot.RegisterNumber, slot.SpOffset, (uint)slot.Base, gcFlags); + result.Add(new LiveSlot(slot.IsRegister, slot.RegisterNumber, slot.SpOffset, (uint)slot.Base, gcFlags)); }); + return result; } /// /// Enumerates all GC slots that are live at the given instruction offset, invoking the callback for each. /// This is the managed equivalent of the native GcInfoDecoder::EnumerateLiveSlots. /// - /// The current instruction offset (relative to method start). - /// CodeManagerFlags controlling reporting behavior. - /// Called for each live slot with (slotIndex, slotDesc, gcFlags). - /// gcFlags contains GC_SLOT_INTERIOR/GC_SLOT_PINNED from the slot descriptor. - /// True if enumeration succeeded. - public bool EnumerateLiveSlots( + private bool EnumerateLiveSlots( uint instructionOffset, - CodeManagerFlags flags, + GcSlotEnumerationOptions options, Action reportSlot) { EnsureDecodedTo(DecodePoints.SlotTable); - bool executionAborted = flags.HasFlag(CodeManagerFlags.ExecutionAborted); - bool reportScratchSlots = flags.HasFlag(CodeManagerFlags.ActiveStackFrame); - bool reportFpBasedSlotsOnly = flags.HasFlag(CodeManagerFlags.ReportFPBasedSlotsOnly); + bool executionAborted = options.IsExecutionAborted; + bool reportScratchSlots = options.IsActiveFrame; + bool reportFpBasedSlotsOnly = options.ReportFPBasedSlotsOnly; // WantsReportOnlyLeaf is always true for non-legacy formats - if (flags.HasFlag(CodeManagerFlags.ParentOfFuncletStackFrame)) + if (options.IsParentOfFuncletStackFrame) return true; uint numTracked = NumTrackedSlots; if (numTracked == 0) - goto ReportUntracked; + return ReportUntrackedAndSucceed(); uint normBreakOffset = TTraits.NormalizeCodeOffset(instructionOffset); @@ -655,7 +646,7 @@ public bool EnumerateLiveSlots( fReport = !fReport; } Debug.Assert(readSlots == numTracked); - goto ReportUntracked; + return ReportUntrackedAndSucceed(); } // Normal 1-bit-per-slot encoding follows } @@ -669,7 +660,7 @@ public bool EnumerateLiveSlots( if (_reader.ReadBits(1, ref bitOffset) != 0) ReportSlot(slotIndex, reportScratchSlots, reportFpBasedSlotsOnly, reportSlot); } - goto ReportUntracked; + return ReportUntrackedAndSucceed(); } else { @@ -682,7 +673,7 @@ public bool EnumerateLiveSlots( bitOffset += (int)(_numSafePoints * numTracked); if (_numInterruptibleRanges == 0) - goto ReportUntracked; + return ReportUntrackedAndSucceed(); } // ---- Fully-interruptible path ---- @@ -695,7 +686,7 @@ public bool EnumerateLiveSlots( uint numBitsPerPointer = (uint)_reader.DecodeVarLengthUnsigned(TTraits.POINTER_SIZE_ENCBASE, ref bitOffset); if (numBitsPerPointer == 0) - goto ReportUntracked; + return ReportUntrackedAndSucceed(); int pointerTablePos = bitOffset; @@ -709,7 +700,7 @@ public bool EnumerateLiveSlots( if (chunkPointer != 0) break; if (chunk-- == 0) - goto ReportUntracked; + return ReportUntrackedAndSucceed(); } int chunksStartPos = (int)(((uint)pointerTablePos + numChunks * numBitsPerPointer + 7) & (~7u)); @@ -815,14 +806,22 @@ public bool EnumerateLiveSlots( } } - ReportUntracked: - if (_numUntrackedSlots > 0 && (flags & (CodeManagerFlags.ParentOfFuncletStackFrame | CodeManagerFlags.NoReportUntracked)) == 0) + return ReportUntrackedAndSucceed(); + + bool ReportUntrackedAndSucceed() { - for (uint slotIndex = numTracked; slotIndex < _numSlots; slotIndex++) - ReportSlot(slotIndex, reportScratchSlots, reportFpBasedSlotsOnly, reportSlot); + if (_numUntrackedSlots > 0 && !options.IsParentOfFuncletStackFrame && !options.SuppressUntrackedSlots) + { + // Native passes reportScratchSlots=true for untracked slots (see native + // ReportUntrackedSlots: "Report everything (although there should *never* + // be any scratch slots that are untracked)"). In practice the JIT can + // produce untracked scratch register slots for interior pointers, so they + // must be reported regardless of whether this is a leaf frame. + for (uint slotIndex = numTracked; slotIndex < _numSlots; slotIndex++) + ReportSlot(slotIndex, reportScratchSlots: true, reportFpBasedSlotsOnly, reportSlot); + } + return true; } - - return true; } private void ReportSlot(uint slotIndex, bool reportScratchSlots, bool reportFpBasedSlotsOnly, Action reportSlot) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfo_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfo_1.cs index f34292572a936e..db1dc4dd79d519 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfo_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfo_1.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; namespace Microsoft.Diagnostics.DataContractReader.Contracts; @@ -27,6 +28,24 @@ uint IGCInfo.GetCodeLength(IGCInfoHandle gcInfoHandle) return handle.GetCodeLength(); } + uint IGCInfo.GetStackBaseRegister(IGCInfoHandle gcInfoHandle) + { + IGCInfoDecoder handle = AssertCorrectHandle(gcInfoHandle); + return handle.GetStackBaseRegister(); + } + + IReadOnlyList IGCInfo.GetInterruptibleRanges(IGCInfoHandle gcInfoHandle) + { + IGCInfoDecoder handle = AssertCorrectHandle(gcInfoHandle); + return handle.GetInterruptibleRanges(); + } + + IReadOnlyList IGCInfo.EnumerateLiveSlots(IGCInfoHandle gcInfoHandle, uint instructionOffset, GcSlotEnumerationOptions options) + { + IGCInfoDecoder handle = AssertCorrectHandle(gcInfoHandle); + return handle.EnumerateLiveSlots(instructionOffset, options); + } + private static IGCInfoDecoder AssertCorrectHandle(IGCInfoHandle gcInfoHandle) { if (gcInfoHandle is not IGCInfoDecoder handle) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs index 86f4210a7cb91d..fcf7aa46c691bb 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs @@ -2,38 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; namespace Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; -/// -/// Flags controlling GC reference reporting behavior. -/// These match the native ICodeManager flags in eetwain.h. -/// -[Flags] -internal enum CodeManagerFlags : uint -{ - ActiveStackFrame = 0x1, - ExecutionAborted = 0x2, - ParentOfFuncletStackFrame = 0x40, - NoReportUntracked = 0x80, - ReportFPBasedSlotsOnly = 0x200, -} - internal interface IGCInfoDecoder : IGCInfoHandle { uint GetCodeLength(); - uint StackBaseRegister { get; } - - /// - /// Enumerates all live GC slots at the given instruction offset. - /// - /// Relative offset from method start. - /// CodeManagerFlags controlling reporting. - /// Callback: (isRegister, registerNumber, spOffset, spBase, gcFlags). - bool EnumerateLiveSlots( - uint instructionOffset, - CodeManagerFlags flags, - LiveSlotCallback reportSlot); + uint GetStackBaseRegister(); + IReadOnlyList GetInterruptibleRanges(); + IReadOnlyList EnumerateLiveSlots(uint instructionOffset, GcSlotEnumerationOptions options); } - -internal delegate void LiveSlotCallback(bool isRegister, uint registerNumber, int spOffset, uint spBase, uint gcFlags); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs index a0fe5b5eeb3a54..706c016420e3ba 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs @@ -155,6 +155,7 @@ internal enum DynamicMethodDescExtendedFlags : uint internal enum AsyncMethodFlags : uint { None = 0, + AsyncCall = 0x1, Thunk = 16, } @@ -314,6 +315,7 @@ private static uint ComputeSize(Target target, Data.MethodDesc desc) internal bool HasStableEntryPoint => HasFlags(MethodDescFlags_1.MethodDescFlags3.HasStableEntryPoint); internal bool HasPrecode => HasFlags(MethodDescFlags_1.MethodDescFlags3.HasPrecode); + internal bool IsStatic => HasFlags(MethodDescFlags_1.MethodDescFlags.Static); internal TargetPointer GetAddressOfNonVtableSlot() => MethodDescOptionalSlots.GetAddressOfNonVtableSlot(Address, Classification, _desc.Flags, _target); internal TargetPointer GetAddressOfNativeCodeSlot() => MethodDescOptionalSlots.GetAddressOfNativeCodeSlot(Address, Classification, _desc.Flags, _target); @@ -1326,6 +1328,68 @@ public ReadOnlySpan GetGenericMethodInstantiation(MethodDescHandle m return AsInstantiatedMethodDesc(methodDesc).Instantiation; } + /// + /// Returns true if the method requires a hidden instantiation argument (generic context parameter). + /// Matches native MethodDesc::RequiresInstArg(). + /// + public bool RequiresInstArg(MethodDescHandle methodDescHandle) + { + MethodDesc methodDesc = _methodDescs[methodDescHandle.Address]; + + // RequiresInstArg = IsSharedByGenericInstantiations && (HasMethodInstantiation || IsStatic || IsValueType || IsInterface) + if (!IsSharedByGenericInstantiations(methodDesc)) + return false; + + if (HasMethodInstantiation(methodDesc)) + return true; + + if (methodDesc.IsStatic) + return true; + + MethodTable mt = _methodTables[methodDesc.MethodTable]; + if (mt.Flags.IsInterface) + return true; + + if (mt.Flags.IsValueType) + return true; + + return false; + } + + private bool IsSharedByGenericInstantiations(MethodDesc methodDesc) + { + // Check method-level sharing: InstantiatedMethodDesc with SharedMethodInstantiation + if (methodDesc.Classification == MethodClassification.Instantiated) + { + InstantiatedMethodDesc imd = AsInstantiatedMethodDesc(methodDesc); + if (imd.IsWrapperStubWithInstantiations) + return false; + + // Check SharedMethodInstantiation flag + Data.InstantiatedMethodDesc imdData = _target.ProcessedData.GetOrAdd(methodDesc.Address); + if ((imdData.Flags2 & (ushort)InstantiatedMethodDescFlags2.KindMask) + == (ushort)InstantiatedMethodDescFlags2.SharedMethodInstantiation) + return true; + } + + // Check class-level sharing: canonical MethodTable with generic instantiation + MethodTable mt = _methodTables[methodDesc.MethodTable]; + return mt.IsCanonMT && mt.Flags.HasInstantiation; + } + + public bool IsAsyncMethod(MethodDescHandle methodDescHandle) + { + MethodDesc methodDesc = _methodDescs[methodDescHandle.Address]; + if (!methodDesc.HasAsyncMethodData) + return false; + + // AsyncMethodData is the last optional slot, placed after NativeCodeSlot. + // Read the AsyncMethodFlags (first field) and check for AsyncCall. + TargetPointer asyncDataAddr = methodDesc.GetAddressOfAsyncMethodData(); + uint asyncFlags = _target.Read(asyncDataAddr); + return (asyncFlags & (uint)AsyncMethodFlags.AsyncCall) != 0; + } + public uint GetMethodToken(MethodDescHandle methodDescHandle) { MethodDesc methodDesc = _methodDescs[methodDescHandle.Address]; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/ARMFrameHandler.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/ARMFrameHandler.cs index 55b712edb05ad4..4c254304b1e3e5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/ARMFrameHandler.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/ARMFrameHandler.cs @@ -52,13 +52,8 @@ public override void HandleTransitionFrame(FramedMethodFrame framedMethodFrame) Data.TransitionBlock transitionBlock = _target.ProcessedData.GetOrAdd(framedMethodFrame.TransitionBlockPtr); - if (transitionBlock.ArgumentRegisters is not TargetPointer argumentRegistersPtr) - { - throw new InvalidOperationException("ARM TransitionBlock does not have ArgumentRegisters set"); - } - // On ARM, TransitionFrames update the argument registers - Data.ArgumentRegisters argumentRegisters = _target.ProcessedData.GetOrAdd(argumentRegistersPtr); + Data.ArgumentRegisters argumentRegisters = _target.ProcessedData.GetOrAdd(transitionBlock.ArgumentRegisters); UpdateFromRegisterDict(argumentRegisters.Registers); } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs index 4edd821c203dc9..915e8504082d69 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameIterator.cs @@ -2,6 +2,7 @@ // 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; @@ -132,20 +133,81 @@ public void UpdateContextFromFrame(IPlatformAgnosticContext context) } } - public bool IsInlineCallFrameWithActiveCall() + /// + /// Returns the return address for the current Frame, matching native Frame::GetReturnAddress(). + /// Returns TargetPointer.Null if the Frame has no return address (e.g., non-active ICF, + /// base Frame types, FuncEvalFrame during exception eval). + /// + public TargetPointer GetReturnAddress() { - if (GetFrameType(target, CurrentFrame.Identifier) != FrameType.InlinedCallFrame) + FrameType frameType = GetCurrentFrameType(); + switch (frameType) { - return false; - } - Data.InlinedCallFrame inlinedCallFrame = target.ProcessedData.GetOrAdd(currentFramePointer); - return InlinedCallFrameHasActiveCall(inlinedCallFrame); - } + // InlinedCallFrame: returns 0 if inactive, else m_pCallerReturnAddress + case FrameType.InlinedCallFrame: + Data.InlinedCallFrame icf = target.ProcessedData.GetOrAdd(currentFramePointer); + return InlinedCallFrameHasActiveCall(icf) ? icf.CallerReturnAddress : TargetPointer.Null; - public static bool IsInlinedCallFrame(Target target, TargetPointer framePointer) - { - Data.Frame frame = target.ProcessedData.GetOrAdd(framePointer); - return GetFrameType(target, frame.Identifier) == FrameType.InlinedCallFrame; + // TransitionFrame types: read return address from the transition block + case FrameType.FramedMethodFrame: + case FrameType.PInvokeCalliFrame: + case FrameType.PrestubMethodFrame: + case FrameType.StubDispatchFrame: + case FrameType.CallCountingHelperFrame: + case FrameType.ExternalMethodFrame: + case FrameType.DynamicHelperFrame: + Data.FramedMethodFrame fmf = target.ProcessedData.GetOrAdd(currentFramePointer); + Data.TransitionBlock tb = target.ProcessedData.GetOrAdd(fmf.TransitionBlockPtr); + return tb.ReturnAddress; + + // SoftwareExceptionFrame: stored m_ReturnAddress + case FrameType.SoftwareExceptionFrame: + Data.SoftwareExceptionFrame sef = target.ProcessedData.GetOrAdd(currentFramePointer); + return sef.ReturnAddress; + + // ResumableFrame / RedirectedThreadFrame: RIP from captured context + case FrameType.ResumableFrame: + case FrameType.RedirectedThreadFrame: + { + Data.ResumableFrame rf = target.ProcessedData.GetOrAdd(currentFramePointer); + IPlatformAgnosticContext ctx = IPlatformAgnosticContext.GetContextForPlatform(target); + ctx.ReadFromAddress(target, rf.TargetContextPtr); + return ctx.InstructionPointer; + } + + // FaultingExceptionFrame: RIP from embedded context + case FrameType.FaultingExceptionFrame: + { + Data.FaultingExceptionFrame fef = target.ProcessedData.GetOrAdd(currentFramePointer); + IPlatformAgnosticContext ctx = IPlatformAgnosticContext.GetContextForPlatform(target); + ctx.ReadFromAddress(target, fef.TargetContext); + return ctx.InstructionPointer; + } + + // HijackFrame: stored m_ReturnAddress + case FrameType.HijackFrame: + Data.HijackFrame hf = target.ProcessedData.GetOrAdd(currentFramePointer); + return hf.ReturnAddress; + + // TailCallFrame: stored m_ReturnAddress + case FrameType.TailCallFrame: + Data.TailCallFrame tcf = target.ProcessedData.GetOrAdd(currentFramePointer); + return tcf.ReturnAddress; + + // FuncEvalFrame: returns 0 during exception eval, else from transition block + case FrameType.FuncEvalFrame: + Data.FuncEvalFrame funcEval = target.ProcessedData.GetOrAdd(currentFramePointer); + Data.DebuggerEval dbgEval = target.ProcessedData.GetOrAdd(funcEval.DebuggerEvalPtr); + if (dbgEval.EvalUsesHijack) + return TargetPointer.Null; + Data.FramedMethodFrame funcEvalFmf = target.ProcessedData.GetOrAdd(currentFramePointer); + Data.TransitionBlock funcEvalTb = target.ProcessedData.GetOrAdd(funcEvalFmf.TransitionBlockPtr); + return funcEvalTb.ReturnAddress; + + // Base Frame and unknown types: return 0 (matches native Frame::GetReturnAddressPtr_Impl) + default: + return TargetPointer.Null; + } } public static string GetFrameName(Target target, TargetPointer frameIdentifier) @@ -160,7 +222,7 @@ public static string GetFrameName(Target target, TargetPointer frameIdentifier) public FrameType GetCurrentFrameType() => GetFrameType(target, CurrentFrame.Identifier); - private static FrameType GetFrameType(Target target, TargetPointer frameIdentifier) + internal static FrameType GetFrameType(Target target, TargetPointer frameIdentifier) { foreach (FrameType frameType in Enum.GetValues()) { @@ -233,21 +295,6 @@ public static TargetPointer GetMethodDescPtr(Target target, TargetPointer frameP } } - public static TargetPointer GetReturnAddress(Target target, TargetPointer framePtr) - { - Data.Frame frame = target.ProcessedData.GetOrAdd(framePtr); - FrameType frameType = GetFrameType(target, frame.Identifier); - switch (frameType) - { - case FrameType.InlinedCallFrame: - Data.InlinedCallFrame inlinedCallFrame = target.ProcessedData.GetOrAdd(frame.Address); - return InlinedCallFrameHasActiveCall(inlinedCallFrame) ? inlinedCallFrame.CallerReturnAddress : TargetPointer.Null; - default: - // NotImplemented for other frame types - return TargetPointer.Null; - } - } - private static bool InlinedCallFrameHasFunction(Data.InlinedCallFrame frame, Target target) { if (target.PointerSize == sizeof(ulong)) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs new file mode 100644 index 00000000000000..6815878ec65c86 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs @@ -0,0 +1,123 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +/// +/// Token values from CORCOMPILE_GCREFMAP_TOKENS (corcompile.h). +/// These indicate the type of GC reference at each transition block slot. +/// +internal enum GCRefMapToken +{ + Skip = 0, + Ref = 1, + Interior = 2, + MethodParam = 3, + TypeParam = 4, + VASigCookie = 5, +} + +/// +/// Managed port of the native GCRefMapDecoder (gcrefmap.h). +/// +/// A GCRefMap is a compact bitstream that describes which transition block slots +/// contain GC references for a given call site (e.g., in ReadyToRun stubs). +/// It is used by ExternalMethodFrame and StubDispatchFrame to report GC roots +/// without needing the full MethodDesc/signature decoding path. +/// +/// Encoding: each slot is encoded as a variable-length integer using 3 bits per +/// token (see ), with a high-bit continuation flag. +/// A "skip" token advances the slot position without reporting. The stream ends +/// when all slots have been consumed (indicated by a zero byte after the last token). +/// +/// The native implementation lives in coreclr/inc/gcrefmap.h (GCRefMapDecoder class). +/// +internal ref struct GCRefMapDecoder +{ + private readonly Target _target; + private TargetPointer _currentByte; + private int _pendingByte; + private int _pos; + + public GCRefMapDecoder(Target target, TargetPointer blob) + { + _target = target; + _currentByte = blob; + _pendingByte = 0x80; // Forces first byte read + _pos = 0; + } + + public readonly bool AtEnd => _pendingByte == 0; + + public readonly int CurrentPos => _pos; + + private int GetBit() + { + int x = _pendingByte; + if ((x & 0x80) != 0) + { + x = _target.Read(_currentByte); + _currentByte = new TargetPointer(_currentByte.Value + 1); + x |= (x & 0x80) << 7; + } + _pendingByte = x >> 1; + return x & 1; + } + + private int GetTwoBit() + { + int result = GetBit(); + result |= GetBit() << 1; + return result; + } + + private int GetInt() + { + int result = 0; + int bit = 0; + do + { + result |= GetBit() << (bit++); + result |= GetBit() << (bit++); + result |= GetBit() << (bit++); + } + while (GetBit() != 0); + return result; + } + + /// + /// x86 only: Read the stack pop count from the stream. + /// + public uint ReadStackPop() + { + int x = GetTwoBit(); + if (x == 3) + x = GetInt() + 3; + return (uint)x; + } + + /// + /// Read the next GC reference token from the stream. + /// Advances CurrentPos as appropriate. + /// + public GCRefMapToken ReadToken() + { + int val = GetTwoBit(); + if (val == 3) + { + int ext = GetInt(); + if ((ext & 1) == 0) + { + _pos += (ext >> 1) + 4; + return GCRefMapToken.Skip; + } + else + { + _pos++; + return (GCRefMapToken)((ext >> 1) + 3); + } + } + _pos++; + return (GCRefMapToken)val; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs index fa72eb606fad75..d050adb8617be2 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs @@ -2,10 +2,17 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; +using System.Collections.Generic; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; +/// +/// Handles all GC reference scanning for stack frames. +/// Covers both managed (frameless) frames via GCInfo and +/// capital "F" Frames via GCRefMap/signature-based scanning. +/// internal class GcScanner { private readonly Target _target; @@ -19,74 +26,460 @@ internal GcScanner(Target target) _gcInfo = target.Contracts.GCInfo; } - public bool EnumGcRefs( + /// + /// Enumerates live GC slots for a managed (frameless) code frame. + /// Port of native EECodeManager::EnumGcRefs (eetwain.cpp). + /// + public void EnumGcRefsForManagedFrame( IPlatformAgnosticContext context, CodeBlockHandle cbh, - CodeManagerFlags flags, - GcScanContext scanContext) + GcSlotEnumerationOptions options, + GcScanContext scanContext, + uint? relOffsetOverride = null) { TargetNUInt relativeOffset = _eman.GetRelativeOffset(cbh); _eman.GetGCInfo(cbh, out TargetPointer gcInfoAddr, out uint gcVersion); - if (_eman.IsFilterFunclet(cbh)) - flags |= CodeManagerFlags.NoReportUntracked; - IGCInfoHandle handle = _gcInfo.DecodePlatformSpecificGCInfo(gcInfoAddr, gcVersion); - if (handle is not IGCInfoDecoder decoder) - return false; - - uint stackBaseRegister = decoder.StackBaseRegister; - // Lazily compute the caller SP for GC_CALLER_SP_REL slots. - // The native code uses GET_CALLER_SP(pRD) which comes from EnsureCallerContextIsValid. + uint stackBaseRegister = _gcInfo.GetStackBaseRegister(handle); TargetPointer? callerSP = null; + uint offsetToUse = relOffsetOverride ?? (uint)relativeOffset.Value; - return decoder.EnumerateLiveSlots( - (uint)relativeOffset.Value, - flags, - (bool isRegister, uint registerNumber, int spOffset, uint spBase, uint gcFlags) => + IReadOnlyList liveSlots = _gcInfo.EnumerateLiveSlots(handle, offsetToUse, options); + foreach (LiveSlot slot in liveSlots) + { + GcScanFlags scanFlags = GcScanFlags.None; + if ((slot.GcFlags & 0x1) != 0) + scanFlags |= GcScanFlags.GC_CALL_INTERIOR; + if ((slot.GcFlags & 0x2) != 0) + scanFlags |= GcScanFlags.GC_CALL_PINNED; + + if (slot.IsRegister) + { + if (!context.TryReadRegister((int)slot.RegisterNumber, out TargetNUInt regValue)) + continue; + GcScanSlotLocation loc = new((int)slot.RegisterNumber, 0, false); + scanContext.GCEnumCallback(new TargetPointer(regValue.Value), scanFlags, loc); + } + else { - GcScanFlags scanFlags = GcScanFlags.None; - if ((gcFlags & 0x1) != 0) // GC_SLOT_INTERIOR - scanFlags |= GcScanFlags.GC_CALL_INTERIOR; - if ((gcFlags & 0x2) != 0) // GC_SLOT_PINNED - scanFlags |= GcScanFlags.GC_CALL_PINNED; + int spReg = context.StackPointerRegister; + int reg = slot.SpBase switch + { + 1 => spReg, + 2 => (int)stackBaseRegister, + 0 => -(spReg + 1), + _ => throw new InvalidOperationException($"Unknown stack slot base: {slot.SpBase}"), + }; + TargetPointer baseAddr = slot.SpBase switch + { + 1 => context.StackPointer, + 2 => context.TryReadRegister((int)stackBaseRegister, out TargetNUInt val) + ? new TargetPointer(val.Value) + : throw new InvalidOperationException($"Failed to read register {stackBaseRegister}"), + 0 => GetCallerSP(context, ref callerSP), + _ => throw new InvalidOperationException($"Unknown stack slot base: {slot.SpBase}"), + }; - if (isRegister) + TargetPointer addr = new(baseAddr.Value + (ulong)(long)slot.SpOffset); + GcScanSlotLocation loc = new(reg, slot.SpOffset, true); + scanContext.GCEnumCallback(addr, scanFlags, loc); + } + } + } + + /// + /// Scans GC roots for a capital "F" Frame based on its type. + /// Port of native Frame::GcScanRoots (frames.cpp). + /// + public void GcScanRoots(TargetPointer frameAddress, GcScanContext scanContext) + { + if (frameAddress == TargetPointer.Null) + return; + + Data.Frame frameData = _target.ProcessedData.GetOrAdd(frameAddress); + FrameIterator.FrameType frameType = FrameIterator.GetFrameType(_target, frameData.Identifier); + + switch (frameType) + { + case FrameIterator.FrameType.StubDispatchFrame: + { + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); + Data.StubDispatchFrame sdf = _target.ProcessedData.GetOrAdd(frameAddress); + + TargetPointer gcRefMap = sdf.Indirection != TargetPointer.Null + ? FindGCRefMap(sdf.Indirection) + : TargetPointer.Null; + + if (gcRefMap != TargetPointer.Null) + PromoteCallerStackUsingGCRefMap(fmf.TransitionBlockPtr, gcRefMap, scanContext); + else + PromoteCallerStack(frameAddress, fmf.TransitionBlockPtr, scanContext); + break; + } + + case FrameIterator.FrameType.ExternalMethodFrame: + { + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); + Data.ExternalMethodFrame emf = _target.ProcessedData.GetOrAdd(frameAddress); + + TargetPointer gcRefMap = emf.Indirection != TargetPointer.Null + ? FindGCRefMap(emf.Indirection) + : TargetPointer.Null; + + if (gcRefMap != TargetPointer.Null) + PromoteCallerStackUsingGCRefMap(fmf.TransitionBlockPtr, gcRefMap, scanContext); + else + PromoteCallerStack(frameAddress, fmf.TransitionBlockPtr, scanContext); + break; + } + + case FrameIterator.FrameType.DynamicHelperFrame: + { + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); + Data.DynamicHelperFrame dhf = _target.ProcessedData.GetOrAdd(frameAddress); + ScanDynamicHelperFrame(fmf.TransitionBlockPtr, dhf.DynamicHelperFrameFlags, scanContext); + break; + } + + case FrameIterator.FrameType.CallCountingHelperFrame: + case FrameIterator.FrameType.PrestubMethodFrame: + { + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); + PromoteCallerStack(frameAddress, fmf.TransitionBlockPtr, scanContext); + break; + } + + case FrameIterator.FrameType.HijackFrame: + // TODO(stackref): Implement HijackFrame scanning (X86 only with FEATURE_HIJACK) + break; + + case FrameIterator.FrameType.ProtectValueClassFrame: + // TODO(stackref): Implement ProtectValueClassFrame scanning + break; + + default: + break; + } + } + + /// + /// Decodes a GCRefMap bitstream and reports GC references in the transition block. + /// Port of native TransitionFrame::PromoteCallerStackUsingGCRefMap (frames.cpp). + /// + private void PromoteCallerStackUsingGCRefMap( + TargetPointer transitionBlock, + TargetPointer gcRefMapBlob, + GcScanContext scanContext) + { + Data.TransitionBlock tb = _target.ProcessedData.GetOrAdd(transitionBlock); + GCRefMapDecoder decoder = new(_target, gcRefMapBlob); + + if (_target.Contracts.RuntimeInfo.GetTargetArchitecture() is RuntimeInfoArchitecture.X86) + decoder.ReadStackPop(); + + while (!decoder.AtEnd) + { + int pos = decoder.CurrentPos; + GCRefMapToken token = decoder.ReadToken(); + TargetPointer slotAddress = AddressFromGCRefMapPos(tb, pos); + + switch (token) + { + case GCRefMapToken.Skip: + break; + case GCRefMapToken.Ref: + scanContext.GCReportCallback(slotAddress, GcScanFlags.None); + break; + case GCRefMapToken.Interior: + scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); + break; + case GCRefMapToken.MethodParam: + case GCRefMapToken.TypeParam: + break; + case GCRefMapToken.VASigCookie: + break; + } + } + } + + /// + /// Scans GC roots for a DynamicHelperFrame based on its flags. + /// Port of native DynamicHelperFrame::GcScanRoots_Impl (frames.cpp). + /// + private void ScanDynamicHelperFrame( + TargetPointer transitionBlock, + int dynamicHelperFrameFlags, + GcScanContext scanContext) + { + const int DynamicHelperFrameFlags_ObjectArg = 1; + const int DynamicHelperFrameFlags_ObjectArg2 = 2; + + Data.TransitionBlock tb = _target.ProcessedData.GetOrAdd(transitionBlock); + TargetPointer argRegStart = tb.ArgumentRegisters; + + if ((dynamicHelperFrameFlags & DynamicHelperFrameFlags_ObjectArg) != 0) + { + scanContext.GCReportCallback(argRegStart, GcScanFlags.None); + } + + if ((dynamicHelperFrameFlags & DynamicHelperFrameFlags_ObjectArg2) != 0) + { + TargetPointer argAddr = new(argRegStart.Value + (uint)_target.PointerSize); + scanContext.GCReportCallback(argAddr, GcScanFlags.None); + } + } + + /// + /// Resolves the GCRefMap for a Frame with m_pIndirection. + /// Port of native FindGCRefMap (frames.cpp). + /// Always resolves the module via FindReadyToRunModule. + /// + private TargetPointer FindGCRefMap(TargetPointer indirection) + { + if (indirection == TargetPointer.Null) + return TargetPointer.Null; + + TargetPointer zapModule = _eman.FindReadyToRunModule(indirection); + if (zapModule == TargetPointer.Null) + return TargetPointer.Null; + + Data.Module module = _target.ProcessedData.GetOrAdd(zapModule); + if (module.ReadyToRunInfo == TargetPointer.Null) + return TargetPointer.Null; + + Data.ReadyToRunInfo r2rInfo = _target.ProcessedData.GetOrAdd(module.ReadyToRunInfo); + if (r2rInfo.ImportSections == TargetPointer.Null || r2rInfo.NumImportSections == 0) + return TargetPointer.Null; + + ulong imageBase = r2rInfo.LoadedImageBase.Value; + if (indirection.Value < imageBase) + return TargetPointer.Null; + ulong diff = indirection.Value - imageBase; + if (diff > uint.MaxValue) + return TargetPointer.Null; + uint rva = (uint)diff; + + const int ImportSectionSize = 20; + const int SectionVAOffset = 0; + const int SectionSizeOffset = 4; + const int EntrySizeOffset = 11; + const int AuxiliaryDataOffset = 16; + + TargetPointer sectionsBase = r2rInfo.ImportSections; + for (uint i = 0; i < r2rInfo.NumImportSections; i++) + { + TargetPointer sectionAddr = new(sectionsBase.Value + i * ImportSectionSize); + uint sectionVA = _target.Read(sectionAddr + SectionVAOffset); + uint sectionSize = _target.Read(sectionAddr + SectionSizeOffset); + + if (rva >= sectionVA && rva < sectionVA + sectionSize) + { + byte entrySize = _target.Read(sectionAddr + EntrySizeOffset); + if (entrySize == 0) + return TargetPointer.Null; + + uint index = (rva - sectionVA) / entrySize; + uint auxDataRva = _target.Read(sectionAddr + AuxiliaryDataOffset); + if (auxDataRva == 0) + return TargetPointer.Null; + + TargetPointer gcRefMapBase = new(imageBase + auxDataRva); + + const uint GCREFMAP_LOOKUP_STRIDE = 1024; + uint lookupIndex = index / GCREFMAP_LOOKUP_STRIDE; + uint remaining = index % GCREFMAP_LOOKUP_STRIDE; + + uint lookupOffset = _target.Read(new TargetPointer(gcRefMapBase.Value + lookupIndex * 4)); + TargetPointer p = new(gcRefMapBase.Value + lookupOffset); + + while (remaining > 0) { - TargetPointer regValue = ReadRegisterValue(context, (int)registerNumber); - GcScanSlotLocation loc = new((int)registerNumber, 0, false); - scanContext.GCEnumCallback(regValue, scanFlags, loc); + while ((_target.Read(p) & 0x80) != 0) + p = new(p.Value + 1); + p = new(p.Value + 1); + remaining--; } - else + + return p; + } + } + + return TargetPointer.Null; + } + + /// + /// Entry point for promoting caller stack GC references via method signature. + /// Matches native TransitionFrame::PromoteCallerStack (frames.cpp:1494). + /// + private void PromoteCallerStack( + TargetPointer frameAddress, + TargetPointer transitionBlock, + GcScanContext scanContext) + { + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); + TargetPointer methodDescPtr = fmf.MethodDescPtr; + if (methodDescPtr == TargetPointer.Null) + return; + + ReadOnlySpan signature; + try + { + signature = GetMethodSignatureBytes(methodDescPtr); + } + catch (System.Exception) + { + return; + } + + if (signature.IsEmpty) + return; + + MethodSignature methodSig; + try + { + unsafe + { + fixed (byte* pSig = signature) { - int spReg = context.StackPointerRegister; - int reg = spBase switch - { - 1 => spReg, // GC_SP_REL → SP register number - 2 => (int)stackBaseRegister, // GC_FRAMEREG_REL → frame base register - 0 => -(spReg + 1), // GC_CALLER_SP_REL → -(SP + 1) - _ => throw new InvalidOperationException($"Unknown stack slot base: {spBase}"), - }; - TargetPointer baseAddr = spBase switch - { - 1 => context.StackPointer, // GC_SP_REL - 2 => ReadRegisterValue(context, (int)stackBaseRegister), // GC_FRAMEREG_REL - 0 => GetCallerSP(context, ref callerSP), // GC_CALLER_SP_REL - _ => throw new InvalidOperationException($"Unknown stack slot base: {spBase}"), - }; - - TargetPointer addr = new(baseAddr.Value + (ulong)(long)spOffset); - GcScanSlotLocation loc = new(reg, spOffset, true); - scanContext.GCEnumCallback(addr, scanFlags, loc); + BlobReader blobReader = new(pSig, signature.Length); + SignatureDecoder decoder = new( + GcSignatureTypeProvider.Instance, metadataReader: null!, genericContext: null); + methodSig = decoder.DecodeMethodSignature(ref blobReader); } - }); + } + } + catch (System.Exception) + { + return; + } + + if (methodSig.Header.CallingConvention is SignatureCallingConvention.VarArgs) + return; + + IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; + MethodDescHandle mdh = rts.GetMethodDescHandle(methodDescPtr); + + bool hasThis = methodSig.Header.IsInstance; + bool hasRetBuf = methodSig.ReturnType is GcTypeKind.Other; + bool requiresInstArg = false; + bool isAsync = false; + bool isValueTypeThis = false; + + try + { + requiresInstArg = rts.RequiresInstArg(mdh); + isAsync = rts.IsAsyncMethod(mdh); + } + catch + { + } + + PromoteCallerStackHelper(transitionBlock, methodSig, hasThis, hasRetBuf, + requiresInstArg, isAsync, isValueTypeThis, scanContext); } /// - /// Compute the caller's SP by unwinding the current context one frame. - /// Cached in to avoid repeated unwinds for the same frame. + /// Core logic for promoting caller stack GC references. + /// Matches native TransitionFrame::PromoteCallerStackHelper (frames.cpp:1560). /// + private void PromoteCallerStackHelper( + TargetPointer transitionBlock, + MethodSignature methodSig, + bool hasThis, + bool hasRetBuf, + bool requiresInstArg, + bool isAsync, + bool isValueTypeThis, + GcScanContext scanContext) + { + Data.TransitionBlock tb = _target.ProcessedData.GetOrAdd(transitionBlock); + + int numRegistersUsed = 0; + if (hasThis) + numRegistersUsed++; + if (hasRetBuf) + numRegistersUsed++; + if (requiresInstArg) + numRegistersUsed++; + if (isAsync) + numRegistersUsed++; + + bool isArm64 = IsTargetArm64(); + if (isArm64) + numRegistersUsed++; + + if (hasThis) + { + int thisPos = isArm64 ? 1 : 0; + TargetPointer thisAddr = AddressFromGCRefMapPos(tb, thisPos); + GcScanFlags thisFlags = isValueTypeThis ? GcScanFlags.GC_CALL_INTERIOR : GcScanFlags.None; + scanContext.GCReportCallback(thisAddr, thisFlags); + } + + int pos = numRegistersUsed; + foreach (GcTypeKind kind in methodSig.ParameterTypes) + { + TargetPointer slotAddress = AddressFromGCRefMapPos(tb, pos); + + switch (kind) + { + case GcTypeKind.Ref: + scanContext.GCReportCallback(slotAddress, GcScanFlags.None); + break; + case GcTypeKind.Interior: + scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); + break; + case GcTypeKind.Other: + break; + case GcTypeKind.None: + break; + } + pos++; + } + } + + private ReadOnlySpan GetMethodSignatureBytes(TargetPointer methodDescPtr) + { + IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; + MethodDescHandle mdh = rts.GetMethodDescHandle(methodDescPtr); + + if (rts.IsStoredSigMethodDesc(mdh, out ReadOnlySpan storedSig)) + return storedSig; + + uint methodToken = rts.GetMethodToken(mdh); + if (methodToken == 0x06000000) + return default; + + TargetPointer methodTablePtr = rts.GetMethodTable(mdh); + TypeHandle typeHandle = rts.GetTypeHandle(methodTablePtr); + TargetPointer modulePtr = rts.GetModule(typeHandle); + + ILoader loader = _target.Contracts.Loader; + ModuleHandle moduleHandle = loader.GetModuleHandleFromModulePtr(modulePtr); + + IEcmaMetadata ecmaMetadata = _target.Contracts.EcmaMetadata; + MetadataReader? mdReader = ecmaMetadata.GetMetadata(moduleHandle); + if (mdReader is null) + return default; + + MethodDefinitionHandle methodDefHandle = MetadataTokens.MethodDefinitionHandle((int)(methodToken & 0x00FFFFFF)); + MethodDefinition methodDef = mdReader.GetMethodDefinition(methodDefHandle); + BlobReader blobReader = mdReader.GetBlobReader(methodDef.Signature); + return blobReader.ReadBytes(blobReader.Length); + } + + private TargetPointer AddressFromGCRefMapPos(Data.TransitionBlock tb, int pos) + { + return new TargetPointer(tb.FirstGCRefMapSlot.Value + (ulong)(pos * _target.PointerSize)); + } + + private bool IsTargetArm64() + { + return _target.Contracts.RuntimeInfo.GetTargetArchitecture() is RuntimeInfoArchitecture.Arm64; + } + private TargetPointer GetCallerSP(IPlatformAgnosticContext context, ref TargetPointer? cached) { if (cached is null) @@ -97,13 +490,4 @@ private TargetPointer GetCallerSP(IPlatformAgnosticContext context, ref TargetPo } return cached.Value; } - - private static TargetPointer ReadRegisterValue(IPlatformAgnosticContext context, int registerNumber) - { - if (!context.TryReadRegister(registerNumber, out TargetNUInt value)) - throw new ArgumentOutOfRangeException(nameof(registerNumber), $"Register number {registerNumber} not found"); - - return new TargetPointer(value.Value); - } - } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs new file mode 100644 index 00000000000000..46e6c8af2de24c --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcSignatureTypeProvider.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.Reflection.Metadata; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +/// +/// Classification of a signature type for GC scanning purposes. +/// +internal enum GcTypeKind +{ + /// Not a GC reference (primitives, pointers). + None, + /// Object reference (class, string, array). + Ref, + /// Interior pointer (byref). + Interior, + /// Value type that may contain embedded GC references. + Other, +} + +/// +/// Classifies signature types for GC scanning purposes. +/// Implements for use +/// with SRM's . +/// +internal sealed class GcSignatureTypeProvider + : ISignatureTypeProvider +{ + public static readonly GcSignatureTypeProvider Instance = new(); + + public GcTypeKind GetPrimitiveType(PrimitiveTypeCode typeCode) + => typeCode switch + { + PrimitiveTypeCode.String or PrimitiveTypeCode.Object => GcTypeKind.Ref, + PrimitiveTypeCode.TypedReference => GcTypeKind.Other, + _ => GcTypeKind.None, + }; + + public GcTypeKind GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) + => rawTypeKind == (byte)SignatureTypeKind.ValueType ? GcTypeKind.Other : GcTypeKind.Ref; + + public GcTypeKind GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) + => rawTypeKind == (byte)SignatureTypeKind.ValueType ? GcTypeKind.Other : GcTypeKind.Ref; + + public GcTypeKind GetTypeFromSpecification(MetadataReader reader, object? genericContext, TypeSpecificationHandle handle, byte rawTypeKind) + => rawTypeKind == (byte)SignatureTypeKind.ValueType ? GcTypeKind.Other : GcTypeKind.Ref; + + public GcTypeKind GetSZArrayType(GcTypeKind elementType) => GcTypeKind.Ref; + public GcTypeKind GetArrayType(GcTypeKind elementType, ArrayShape shape) => GcTypeKind.Ref; + public GcTypeKind GetByReferenceType(GcTypeKind elementType) => GcTypeKind.Interior; + public GcTypeKind GetPointerType(GcTypeKind elementType) => GcTypeKind.None; + + public GcTypeKind GetGenericInstantiation(GcTypeKind genericType, ImmutableArray typeArguments) + => genericType; + + public GcTypeKind GetGenericMethodParameter(object? genericContext, int index) => GcTypeKind.Ref; + public GcTypeKind GetGenericTypeParameter(object? genericContext, int index) => GcTypeKind.Ref; + public GcTypeKind GetFunctionPointerType(MethodSignature signature) => GcTypeKind.None; + public GcTypeKind GetModifiedType(GcTypeKind modifier, GcTypeKind unmodifiedType, bool isRequired) => unmodifiedType; + public GcTypeKind GetPinnedType(GcTypeKind elementType) => elementType; +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs index 3a09197254291a..c11974119d131b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs @@ -8,7 +8,6 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; -using Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; using Microsoft.Diagnostics.DataContractReader.Data; using System.Linq; @@ -18,11 +17,13 @@ internal partial class StackWalk_1 : IStackWalk { private readonly Target _target; private readonly IExecutionManager _eman; + private readonly GcScanner _gcScanner; internal StackWalk_1(Target target) { _target = target; _eman = target.Contracts.ExecutionManager; + _gcScanner = new GcScanner(target); } public enum StackWalkState @@ -68,6 +69,19 @@ private class StackWalkData(IPlatformAgnosticContext context, StackWalkState sta // set back to true when encountering a ResumableFrame (FRAME_ATTR_RESUMABLE). public bool IsFirst { get; set; } = true; + // Track isInterrupted like native CrawlFrame::isInterrupted. + // Set in UpdateState when transitioning to SW_FRAMELESS after processing a Frame + // with FRAME_ATTR_EXCEPTION (e.g., FaultingExceptionFrame). When true, the managed + // frame reached via that Frame's return address was interrupted by an exception, + // and EnumGcRefs should use ExecutionAborted to skip live slot reporting at + // non-interruptible offsets. + public bool IsInterrupted { get; set; } + + // The frame type of the last SW_FRAME processed by Next(). + // Used by UpdateState to detect exception frames (FRAME_ATTR_EXCEPTION) and + // set IsInterrupted when transitioning to a managed frame. + public FrameIterator.FrameType? LastProcessedFrameType { get; set; } + public bool IsCurrentFrameResumable() { if (State is not (StackWalkState.SW_FRAME or StackWalkState.SW_SKIPPED_FRAME)) @@ -77,9 +91,10 @@ public bool IsCurrentFrameResumable() // Only frame types with FRAME_ATTR_RESUMABLE set isFirst=true. // FaultingExceptionFrame has FRAME_ATTR_FAULTED (sets hasFaulted) // but NOT FRAME_ATTR_RESUMABLE, so it must not be included here. - // TODO: HijackFrame only has FRAME_ATTR_RESUMABLE on non-x86 platforms. - // When x86 stack walking is supported, this should be conditioned on - // the target architecture. + // Note: HijackFrame only has FRAME_ATTR_RESUMABLE on non-x86 platforms + // (see frames.h). On x86 it uses GcScanRoots_Impl instead of the + // resumable frame pattern. When x86 cDAC stack walking is supported, + // HijackFrame should be conditioned on the target architecture. return ft is FrameIterator.FrameType.ResumableFrame or FrameIterator.FrameType.RedirectedThreadFrame or FrameIterator.FrameType.HijackFrame; @@ -87,9 +102,11 @@ or FrameIterator.FrameType.RedirectedThreadFrame /// /// Update the IsFirst state for the NEXT frame, matching native stackwalk.cpp: - /// - After a frameless frame: isFirst = false (line 2202) - /// - After a ResumableFrame: isFirst = true (line 2235) - /// - After other Frames: isFirst = false (implicit in line 2235 assignment) + /// - After a frameless frame: isFirst = false + /// - After a ResumableFrame: isFirst = true + /// - After other Frames: isFirst = false + /// - After a skipped frame: isFirst unchanged (native never modifies isFirst + /// in the SFITER_SKIPPED_FRAME_FUNCTION path — it keeps the value from Init) /// public void AdvanceIsFirst() { @@ -97,6 +114,14 @@ public void AdvanceIsFirst() { IsFirst = false; } + else if (State == StackWalkState.SW_SKIPPED_FRAME) + { + // Native SFITER_SKIPPED_FRAME_FUNCTION (stackwalk.cpp:2086-2128) does NOT + // modify isFirst. It stays true from Init() so the subsequent managed frame + // gets IsActiveFunc()=true. This is important because skipped frames are + // explicit Frames embedded within the active managed frame (e.g. InlinedCallFrame + // from PInvoke), and the managed frame should still be treated as the leaf. + } else { IsFirst = IsCurrentFrameResumable(); @@ -112,53 +137,13 @@ public StackDataFrameHandle ToDataFrame() } IEnumerable IStackWalk.CreateStackWalk(ThreadData threadData) - => CreateStackWalkCore(threadData, skipInitialFrames: false, flags: ContextFlags.All); - - /// - /// Core stack walk implementation. - /// - /// Thread to walk. - /// - /// When true, pre-advances the FrameIterator past explicit Frames below the initial - /// managed frame's caller SP. This matches the native DacStackReferenceWalker behavior - /// for GC reference enumeration, where these frames are within the current managed - /// frame's stack range and don't contribute additional GC roots. - /// - /// Must be false for ClrDataStackWalk, which advances the cDAC and legacy DAC in - /// lockstep and must yield the same frame sequence (including initial skipped frames). - /// - private IEnumerable CreateStackWalkCore(ThreadData threadData, bool skipInitialFrames, ContextFlags flags) { IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(_target); - uint contextFlags = flags switch - { - ContextFlags.Full => context.FullContextFlags, - ContextFlags.All => context.AllContextFlags, - _ => throw new ArgumentOutOfRangeException(nameof(flags)), - }; + uint contextFlags = context.AllContextFlags; FillContextFromThread(context, threadData, contextFlags); StackWalkState state = IsManaged(context.InstructionPointer, out _) ? StackWalkState.SW_FRAMELESS : StackWalkState.SW_FRAME; FrameIterator frameIterator = new(_target, threadData); - if (skipInitialFrames) - { - TargetPointer skipBelowSP; - if (state == StackWalkState.SW_FRAMELESS) - { - IPlatformAgnosticContext callerCtx = context.Clone(); - callerCtx.Unwind(_target); - skipBelowSP = callerCtx.StackPointer; - } - else - { - skipBelowSP = context.StackPointer; - } - while (frameIterator.IsValid() && frameIterator.CurrentFrameAddress.Value < skipBelowSP.Value) - { - frameIterator.Next(); - } - } - // if the next Frame is not valid and we are not in managed code, there is nothing to return if (state == StackWalkState.SW_FRAME && !frameIterator.IsValid()) { @@ -170,7 +155,7 @@ private IEnumerable CreateStackWalkCore(ThreadData thread // Mirror native Init() -> ProcessCurrentFrame() -> CheckForSkippedFrames(): // When the initial frame is managed (SW_FRAMELESS), check if there are explicit // Frames below the caller SP that should be reported first. The native walker - // yields skipped frames BEFORE the containing managed frame on non-x86. + // yields skipped frames BEFORE the containing managed frame. if (state == StackWalkState.SW_FRAMELESS && CheckForSkippedFrames(stackWalkData)) { stackWalkData.State = StackWalkState.SW_SKIPPED_FRAME; @@ -188,18 +173,32 @@ private IEnumerable CreateStackWalkCore(ThreadData thread IReadOnlyList IStackWalk.WalkStackReferences(ThreadData threadData) { - IEnumerable stackFrames = CreateStackWalkCore(threadData, skipInitialFrames: true, flags: ContextFlags.Full); - IEnumerable frames = stackFrames.Select(AssertCorrectHandle); - IEnumerable gcFrames = Filter(frames); + // Initialize the walk data directly + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(_target); + FillContextFromThread(context, threadData, context.FullContextFlags); + StackWalkState state = IsManaged(context.InstructionPointer, out _) ? StackWalkState.SW_FRAMELESS : StackWalkState.SW_FRAME; + FrameIterator frameIterator = new(_target, threadData); + + if (state == StackWalkState.SW_FRAME && !frameIterator.IsValid()) + return []; + + StackWalkData walkData = new(context, state, frameIterator, threadData); + + // Mirror native Init() -> ProcessCurrentFrame() -> CheckForSkippedFrames(): + // When the initial frame is managed (SW_FRAMELESS), check if there are explicit + // Frames below the caller SP that should be reported first. The native walker + // yields skipped frames BEFORE the containing managed frame. + if (walkData.State == StackWalkState.SW_FRAMELESS && CheckForSkippedFrames(walkData)) + walkData.State = StackWalkState.SW_SKIPPED_FRAME; GcScanContext scanContext = new(_target, resolveInteriorPointers: false); - foreach (GCFrameData gcFrame in gcFrames) + // Filter drives Next() directly, matching native Filter()+NextRaw() integration. + // This prevents funclet-to-parent transitions from re-visiting already-walked frames. + foreach (GCFrameData gcFrame in Filter(walkData)) { try { - _ = ((IStackWalk)this).GetMethodDescPtr(gcFrame.Frame); - bool reportGcReferences = gcFrame.ShouldCrawlFrameReportGCReferences; TargetPointer pFrame = ((IStackWalk)this).GetFrameAddress(gcFrame.Frame); @@ -215,26 +214,48 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre if (!IsManaged(gcFrame.Frame.Context.InstructionPointer, out CodeBlockHandle? cbh)) throw new InvalidOperationException("Expected managed code"); - CodeManagerFlags codeManagerFlags = gcFrame.Frame.IsActiveFrame - ? CodeManagerFlags.ActiveStackFrame - : 0; - - if (gcFrame.ShouldParentToFuncletSkipReportingGCReferences) - codeManagerFlags |= CodeManagerFlags.ParentOfFuncletStackFrame; - - // TODO: When ShouldParentFrameUseUnwindTargetPCforGCReporting is set, - // use FindFirstInterruptiblePoint on the catch handler clause range - // to override the relOffset for GC liveness lookup. This mirrors - // native gcenv.ee.common.cpp behavior for catch-handler resumption. + GcSlotEnumerationOptions gcOptions = new() + { + IsActiveFrame = gcFrame.Frame.IsActiveFrame, + + // If the frame was interrupted by an exception (reached via a + // FaultingExceptionFrame), set ExecutionAborted so the GcInfoDecoder + // skips live slot reporting at non-interruptible offsets. This matches + // native CrawlFrame::GetCodeManagerFlags (stackwalk.h). + IsExecutionAborted = gcFrame.IsInterrupted, + IsParentOfFuncletStackFrame = gcFrame.ShouldParentToFuncletSkipReportingGCReferences, + SuppressUntrackedSlots = _eman.IsFilterFunclet(cbh.Value), + }; + + uint? relOffsetOverride = null; + if (gcFrame.ShouldParentFrameUseUnwindTargetPCforGCReporting) + { + _eman.GetGCInfo(cbh.Value, out TargetPointer gcInfoAddr, out uint gcVersion); + IGCInfoHandle gcHandle = _target.Contracts.GCInfo.DecodePlatformSpecificGCInfo(gcInfoAddr, gcVersion); + uint startPC = gcFrame.ClauseForCatchHandlerStartPC; + uint endPC = gcFrame.ClauseForCatchHandlerEndPC; + foreach (var range in _target.Contracts.GCInfo.GetInterruptibleRanges(gcHandle)) + { + if (range.EndOffset <= startPC) + continue; + if (startPC >= range.StartOffset && startPC < range.EndOffset) + { + relOffsetOverride = startPC; + break; + } + if (range.StartOffset < endPC) + { + relOffsetOverride = range.StartOffset; + break; + } + } + } - GcScanner gcScanner = new(_target); - gcScanner.EnumGcRefs(gcFrame.Frame.Context, cbh.Value, codeManagerFlags, scanContext); + _gcScanner.EnumGcRefsForManagedFrame(gcFrame.Frame.Context, cbh.Value, gcOptions, scanContext, relOffsetOverride); } else { - // TODO: Frame-based GC root scanning (ScanFrameRoots) not yet implemented. - // Frames that call PromoteCallerStack (StubDispatchFrame, ExternalMethodFrame, - // DynamicHelperFrame, etc.) will be handled in a follow-up PR. + _gcScanner.GcScanRoots(gcFrame.Frame.FrameAddress, scanContext); } } } @@ -272,11 +293,25 @@ public GCFrameData(StackDataFrameHandle frame) public bool ShouldParentToFuncletSkipReportingGCReferences { get; set; } public bool ShouldCrawlFrameReportGCReferences { get; set; } // required public bool ShouldParentFrameUseUnwindTargetPCforGCReporting { get; set; } + public uint ClauseForCatchHandlerStartPC { get; set; } + public uint ClauseForCatchHandlerEndPC { get; set; } + // Set when the frame was reached via an exception Frame (FRAME_ATTR_EXCEPTION). + // Causes ExecutionAborted to be passed to EnumGcRefs. + public bool IsInterrupted { get; set; } } - private IEnumerable Filter(IEnumerable handles) + /// + /// Port of native StackFrameIterator::Filter (GC_FUNCLET_REFERENCE_REPORTING mode). + /// Unlike the previous implementation that passively consumed pre-generated frames, + /// this version drives Next() directly — matching native Filter() which calls NextRaw() + /// internally to skip frames. This prevents funclet-to-parent transitions from + /// re-visiting already-walked frames. + /// +#pragma warning disable IDE0059 // Unnecessary assignment — false positives from goto case + do/while pattern + private IEnumerable Filter(StackWalkData walkData) { - // StackFrameIterator::Filter assuming GC_FUNCLET_REFERENCE_REPORTING is defined + // Process the initial frame, then loop calling Next() for subsequent frames. + // This matches native: Init() produces the first frame, then Filter()+NextRaw() loop. // global tracking variables bool processNonFilterFunclet = false; @@ -284,11 +319,19 @@ private IEnumerable Filter(IEnumerable handle bool didFuncletReportGCReferences = true; TargetPointer parentStackFrame = TargetPointer.Null; TargetPointer funcletParentStackFrame = TargetPointer.Null; - TargetPointer intermediaryFuncletParentStackFrame; + TargetPointer intermediaryFuncletParentStackFrame = TargetPointer.Null; - foreach (StackDataFrameHandle handle in handles) + // Process the initial frame, then advance with Next() + bool isValid = walkData.State is not (StackWalkState.SW_ERROR or StackWalkState.SW_COMPLETE); + while (isValid) { - GCFrameData gcFrame = new(handle); + StackDataFrameHandle handle = walkData.ToDataFrame(); + walkData.AdvanceIsFirst(); + + GCFrameData gcFrame = new(handle) + { + IsInterrupted = walkData.IsInterrupted, + }; // per-frame tracking variables bool stop = false; @@ -506,6 +549,9 @@ private IEnumerable Filter(IEnumerable handle didFuncletReportGCReferences = true; gcFrame.ShouldParentFrameUseUnwindTargetPCforGCReporting = true; + + gcFrame.ClauseForCatchHandlerStartPC = exInfo.ClauseForCatchHandlerStartPC; + gcFrame.ClauseForCatchHandlerEndPC = exInfo.ClauseForCatchHandlerEndPC; } else if (!IsFunclet(handle)) { @@ -580,8 +626,14 @@ private IEnumerable Filter(IEnumerable handle if (stop) yield return gcFrame; + + // Advance the iterator - matching native Filter() calling NextRaw() + // When a frame was skipped (stop=false), this advances past it. + // When a frame was yielded (stop=true), this advances to the next frame. + isValid = Next(walkData); } } +#pragma warning restore IDE0059 private bool IsUnwoundToTargetParentFrame(StackDataFrameHandle handle, TargetPointer targetParentFrame) { @@ -598,6 +650,18 @@ private bool Next(StackWalkData handle) switch (handle.State) { case StackWalkState.SW_FRAMELESS: + // Native assertion (stackwalk.cpp): current SP must be below the next Frame. + // FaultingExceptionFrame is a special case where it gets pushed after the frame is running. + Debug.Assert( + !handle.FrameIter.IsValid() || + handle.Context.StackPointer.Value < handle.FrameIter.CurrentFrameAddress.Value || + handle.FrameIter.GetCurrentFrameType() == FrameIterator.FrameType.FaultingExceptionFrame, + $"SP (0x{handle.Context.StackPointer:X}) should be below next Frame (0x{handle.FrameIter.CurrentFrameAddress:X})"); + + // Reset interrupted state after processing a managed frame. + // Native stackwalk.cpp:2203-2205: isInterrupted = false; hasFaulted = false; + handle.IsInterrupted = false; + try { handle.Context.Unwind(_target); @@ -609,13 +673,33 @@ private bool Next(StackWalkData handle) } break; case StackWalkState.SW_SKIPPED_FRAME: + // Advance past the skipped frame, then let UpdateState detect + // whether there are more skipped frames or we've reached the managed method. handle.FrameIter.Next(); break; case StackWalkState.SW_FRAME: - handle.FrameIter.UpdateContextFromFrame(handle.Context); - if (!handle.FrameIter.IsInlineCallFrameWithActiveCall()) + // Native SFITER_FRAME_FUNCTION gates ProcessIp + UpdateRegDisplay on + // GetReturnAddress() != 0, and gates GotoNextFrame on !pInlinedFrame. + // pInlinedFrame is set only for active InlinedCallFrames. { - handle.FrameIter.Next(); + var frameType = handle.FrameIter.GetCurrentFrameType(); + + TargetPointer returnAddress = handle.FrameIter.GetReturnAddress(); + bool isActiveICF = frameType == FrameIterator.FrameType.InlinedCallFrame + && returnAddress != TargetPointer.Null; + + // Record the frame type so UpdateState can detect exception frames + // and set IsInterrupted when transitioning to the managed frame. + handle.LastProcessedFrameType = frameType; + + if (returnAddress != TargetPointer.Null) + { + handle.FrameIter.UpdateContextFromFrame(handle.Context); + } + if (!isActiveICF) + { + handle.FrameIter.Next(); + } } break; case StackWalkState.SW_ERROR: @@ -641,6 +725,18 @@ private void UpdateState(StackWalkData handle) if (isManaged) { handle.State = StackWalkState.SW_FRAMELESS; + + // Detect exception frames (FRAME_ATTR_EXCEPTION) when transitioning to managed. + // Both FaultingExceptionFrame (hardware) and SoftwareExceptionFrame (managed throw) + // have FRAME_ATTR_EXCEPTION set. The resulting managed frame gets ExecutionAborted, + // causing GcInfoDecoder to skip live slot reporting at non-interruptible offsets. + if (handle.LastProcessedFrameType is FrameIterator.FrameType.FaultingExceptionFrame + or FrameIterator.FrameType.SoftwareExceptionFrame) + { + handle.IsInterrupted = true; + } + handle.LastProcessedFrameType = null; + if (CheckForSkippedFrames(handle)) { handle.State = StackWalkState.SW_SKIPPED_FRAME; @@ -719,15 +815,17 @@ TargetPointer IStackWalk.GetMethodDescPtr(IStackDataFrameHandle stackDataFrameHa // 4) the return address method has a MDContext arg bool reportInteropMD = false; - if (FrameIterator.IsInlinedCallFrame(_target, framePtr) && + Data.Frame frameData = _target.ProcessedData.GetOrAdd(framePtr); + FrameIterator.FrameType frameType = FrameIterator.GetFrameType(_target, frameData.Identifier); + + if (frameType == FrameIterator.FrameType.InlinedCallFrame && handle.State == StackWalkState.SW_SKIPPED_FRAME) { IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; - // FrameIterator.GetReturnAddress is currently only implemented for InlinedCallFrame - // This is fine as this check is only needed for that frame type - TargetPointer returnAddress = FrameIterator.GetReturnAddress(_target, framePtr); - if (_eman.GetCodeBlockHandle(returnAddress.Value) is CodeBlockHandle cbh) + Data.InlinedCallFrame icf = _target.ProcessedData.GetOrAdd(framePtr); + TargetPointer returnAddress = icf.CallerReturnAddress; + if (returnAddress != TargetPointer.Null && _eman.GetCodeBlockHandle(returnAddress.Value) is CodeBlockHandle cbh) { MethodDescHandle returnMethodDesc = rts.GetMethodDescHandle(_eman.GetMethodDesc(cbh)); reportInteropMD = rts.HasMDContextArg(returnMethodDesc); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs index c8d30ded52e678..d582523b5159ec 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs @@ -23,6 +23,8 @@ public ExceptionInfo(Target target, TargetPointer address) CSFEHClause = target.ReadPointerField(address, type, nameof(CSFEHClause)); CSFEnclosingClause = target.ReadPointerField(address, type, nameof(CSFEnclosingClause)); CallerOfActualHandlerFrame = target.ReadPointerField(address, type, nameof(CallerOfActualHandlerFrame)); + ClauseForCatchHandlerStartPC = target.ReadField(address, type, nameof(ClauseForCatchHandlerStartPC)); + ClauseForCatchHandlerEndPC = target.ReadField(address, type, nameof(ClauseForCatchHandlerEndPC)); } public TargetPointer PreviousNestedInfo { get; } @@ -35,4 +37,6 @@ public ExceptionInfo(Target target, TargetPointer address) public TargetPointer CSFEHClause { get; } public TargetPointer CSFEnclosingClause { get; } public TargetPointer CallerOfActualHandlerFrame { get; } + public uint ClauseForCatchHandlerStartPC { get; } + public uint ClauseForCatchHandlerEndPC { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DynamicHelperFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DynamicHelperFrame.cs new file mode 100644 index 00000000000000..625c616d42616e --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DynamicHelperFrame.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +internal class DynamicHelperFrame : IData +{ + static DynamicHelperFrame IData.Create(Target target, TargetPointer address) + => new DynamicHelperFrame(target, address); + + public DynamicHelperFrame(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.DynamicHelperFrame); + DynamicHelperFrameFlags = target.ReadField(address, type, nameof(DynamicHelperFrameFlags)); + } + + public int DynamicHelperFrameFlags { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs new file mode 100644 index 00000000000000..108fe0d0a5b70d --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +internal class ExternalMethodFrame : IData +{ + static ExternalMethodFrame IData.Create(Target target, TargetPointer address) + => new ExternalMethodFrame(target, address); + + public ExternalMethodFrame(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.ExternalMethodFrame); + Indirection = target.ReadPointerField(address, type, nameof(Indirection)); + } + + public TargetPointer Indirection { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs index c49f6919353255..a063f9f3a0dc52 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs @@ -14,6 +14,7 @@ public StubDispatchFrame(Target target, TargetPointer address) MethodDescPtr = target.ReadPointerField(address, type, nameof(MethodDescPtr)); RepresentativeMTPtr = target.ReadPointerField(address, type, nameof(RepresentativeMTPtr)); RepresentativeSlot = target.ReadField(address, type, nameof(RepresentativeSlot)); + Indirection = target.ReadPointerField(address, type, nameof(Indirection)); Address = address; } @@ -21,4 +22,5 @@ public StubDispatchFrame(Target target, TargetPointer address) public TargetPointer MethodDescPtr { get; } public TargetPointer RepresentativeMTPtr { get; } public uint RepresentativeSlot { get; } + public TargetPointer Indirection { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs index b2c0c71cb47ef9..470aacc61446bc 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/TransitionBlock.cs @@ -14,17 +14,21 @@ public TransitionBlock(Target target, TargetPointer address) ReturnAddress = target.ReadPointerField(address, type, nameof(ReturnAddress)); CalleeSavedRegisters = address + (ulong)type.Fields[nameof(CalleeSavedRegisters)].Offset; - if (type.Fields.ContainsKey(nameof(ArgumentRegisters))) - { - ArgumentRegisters = address + (ulong)type.Fields[nameof(ArgumentRegisters)].Offset; - } + // These are computed positions within the TransitionBlock. + ArgumentRegisters = address + (ulong)type.Fields[nameof(ArgumentRegisters)].Offset; + FirstGCRefMapSlot = address + (ulong)type.Fields[nameof(FirstGCRefMapSlot)].Offset; } public TargetPointer ReturnAddress { get; } public TargetPointer CalleeSavedRegisters { get; } /// - /// Only available on ARM targets. + /// Address of the argument registers area within this TransitionBlock. /// - public TargetPointer? ArgumentRegisters { get; } + public TargetPointer ArgumentRegisters { get; } + + /// + /// Address of the first slot covered by the GCRefMap within this TransitionBlock. + /// + public TargetPointer FirstGCRefMapSlot { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs index 3241fa45b965a0..6557ee7aa99a1c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs @@ -33,6 +33,11 @@ public ReadyToRunInfo(Target target, TargetPointer address) DebugInfoSection = target.ReadPointerField(address, type, nameof(DebugInfoSection)); ExceptionInfoSection = target.ReadPointerField(address, type, nameof(ExceptionInfoSection)); + NumImportSections = target.Read(address + (ulong)type.Fields[nameof(NumImportSections)].Offset); + ImportSections = NumImportSections > 0 + ? target.ReadPointer(address + (ulong)type.Fields[nameof(ImportSections)].Offset) + : TargetPointer.Null; + // Map is from the composite info pointer (set to itself for non-multi-assembly composite images) EntryPointToMethodDescMap = CompositeInfo + (ulong)type.Fields[nameof(EntryPointToMethodDescMap)].Offset; LoadedImageBase = target.ReadPointerField(address, type, nameof(LoadedImageBase)); @@ -55,4 +60,6 @@ public ReadyToRunInfo(Target target, TargetPointer address) public TargetPointer EntryPointToMethodDescMap { get; } public TargetPointer LoadedImageBase { get; } public TargetPointer Composite { get; } + public uint NumImportSections { get; } + public TargetPointer ImportSections { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodDescFlags_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodDescFlags_1.cs index 901d9331c8938f..7fabac70993319 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodDescFlags_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodDescFlags_1.cs @@ -15,7 +15,8 @@ internal enum MethodDescFlags : ushort HasNonVtableSlot = 0x0008, HasMethodImpl = 0x0010, HasNativeCodeSlot = 0x0020, - HasAsyncMethodData = 0x040, + HasAsyncMethodData = 0x0040, + Static = 0x0080, #endregion Optional slots } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs index d67adff3ab3489..8a909f25f5ebc7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -4034,13 +4034,69 @@ int ISOSEnum.GetCount(uint* pCount) int ISOSDacInterface.GetStackReferences(int osThreadID, DacComNullableByRef ppEnum) { - // Stack reference enumeration is not yet complete in the cDAC — capital-F Frame - // GC root scanning (ScanFrameRoots) is still pending. Fall through to the legacy - // DAC so that consumers (dump tests, SOS) continue to work while the implementation - // is in progress. - return _legacyImpl is not null - ? _legacyImpl.GetStackReferences(osThreadID, ppEnum) - : HResults.E_NOTIMPL; + int hr = HResults.S_OK; + try + { + IThread threadContract = _target.Contracts.Thread; + IStackWalk stackWalkContract = _target.Contracts.StackWalk; + ThreadData? matchingThread = null; + + ThreadStoreData threadStore = threadContract.GetThreadStoreData(); + TargetPointer threadAddr = threadStore.FirstThread; + while (threadAddr != TargetPointer.Null) + { + ThreadData td = threadContract.GetThreadData(threadAddr); + if (td.OSId.Value == (ulong)osThreadID) + { + matchingThread = td; + break; + } + threadAddr = td.NextThread; + } + + if (matchingThread is null) + { + throw new ArgumentException($"No thread with OS ID {osThreadID} was found."); + } + + IReadOnlyList refs = stackWalkContract.WalkStackReferences(matchingThread.Value); + + SOSStackRefData[] sosRefs = new SOSStackRefData[refs.Count]; + for (int i = 0; i < refs.Count; i++) + { + sosRefs[i] = new SOSStackRefData + { + HasRegisterInformation = refs[i].HasRegisterInformation ? 1 : 0, + Register = refs[i].Register, + Offset = refs[i].Offset, + Address = refs[i].Address.Value, + Object = refs[i].Object.Value, + Flags = refs[i].Flags, + Source = refs[i].Source.Value, + SourceType = refs[i].IsStackSourceFrame + ? SOSStackSourceType.SOS_StackSourceFrame + : SOSStackSourceType.SOS_StackSourceIP, + StackPointer = refs[i].StackPointer.Value, + }; + } + + ppEnum.Interface = new SOSStackRefEnum(sosRefs); + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacyImpl is not null) + { + // Validate that the legacy DAC produces the same HResult. + // We pass isNullRef: false to request actual enumeration, but we don't + // compare individual refs — that's done by cdacstress.cpp at runtime. + int hrLocal = _legacyImpl.GetStackReferences(osThreadID, new DacComNullableByRef(isNullRef: false)); + Debug.ValidateHResult(hr, hrLocal); + } +#endif + return hr; } int ISOSDacInterface.GetStressLogAddress(ClrDataAddress* stressLog) diff --git a/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj b/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj index 5b33a365154275..a3951ba48e1a21 100644 --- a/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj +++ b/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs index c72126d962f939..d054e97d09de27 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs @@ -331,6 +331,9 @@ internal sealed class MockReadyToRunInfo : TypedView private const string LoadedImageBaseFieldName = "LoadedImageBase"; private const string CompositeFieldName = "Composite"; + private const string ImportSectionsFieldName = "ImportSections"; + private const string NumImportSectionsFieldName = "NumImportSections"; + public static Layout CreateLayout(MockTarget.Architecture architecture, int hashMapStride) => new SequentialLayoutBuilder("ReadyToRunInfo", architecture) .AddPointerField(ReadyToRunHeaderFieldName) @@ -342,6 +345,8 @@ public static Layout CreateLayout(MockTarget.Architecture ar .AddPointerField(DelayLoadMethodCallThunksFieldName) .AddPointerField(DebugInfoSectionFieldName) .AddPointerField(ExceptionInfoSectionFieldName) + .AddPointerField(ImportSectionsFieldName) + .AddUInt32Field(NumImportSectionsFieldName) .AddField(EntryPointToMethodDescMapFieldName, hashMapStride) .AddPointerField(LoadedImageBaseFieldName) .AddPointerField(CompositeFieldName) diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs index cf6dd55ee67f3b..bf7603a9966c2c 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs @@ -17,6 +17,8 @@ internal sealed class MockExceptionInfo : TypedView private const string CSFEHClauseFieldName = "CSFEHClause"; private const string CSFEnclosingClauseFieldName = "CSFEnclosingClause"; private const string CallerOfActualHandlerFrameFieldName = "CallerOfActualHandlerFrame"; + private const string ClauseForCatchHandlerStartPCFieldName = "ClauseForCatchHandlerStartPC"; + private const string ClauseForCatchHandlerEndPCFieldName = "ClauseForCatchHandlerEndPC"; public static Layout CreateLayout(MockTarget.Architecture architecture) => new SequentialLayoutBuilder("ExceptionInfo", architecture) @@ -30,6 +32,8 @@ public static Layout CreateLayout(MockTarget.Architecture arc .AddPointerField(CSFEHClauseFieldName) .AddPointerField(CSFEnclosingClauseFieldName) .AddPointerField(CallerOfActualHandlerFrameFieldName) + .AddUInt32Field(ClauseForCatchHandlerStartPCFieldName) + .AddUInt32Field(ClauseForCatchHandlerEndPCFieldName) .Build(); public ulong ThrownObjectHandle From e883467e634e8d64da15d707c9ce30ca8cde6618 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Fri, 1 May 2026 01:02:07 -0400 Subject: [PATCH 055/115] [NativeAOT] Add cDAC data descriptor infrastructure (#126972) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > [!NOTE] > This PR was created with assistance from GitHub Copilot. ## Summary Adds the cDAC data descriptor infrastructure for NativeAOT, enabling diagnostic tools (cDAC reader, SOS) to inspect NativeAOT runtime state through the same contract-based mechanism used by CoreCLR. ## Changes ### Native data descriptor (`datadescriptor.inc`) - **Thread/ThreadStore**: Thread state, OS ID, exception tracker, stack bounds, alloc context, transition frame, thread link - **EEAllocContext/GCAllocContext**: Allocation pointer, limit, bytes allocated - **MethodTable (EEType)**: Flags, base size, related type, vtable slots, interfaces, hash code — with flag constants exposed via `cdac_data<>` friend pattern - **ExInfo**: Exception linked list traversal - **StressLog/ThreadStressLog**: Stress log infrastructure (guarded by `STRESS_LOG`) - **Globals**: ThreadStore static pointer, free object MethodTable, GC bounds, thread state flags, object unmask, stress log - **Contracts**: Thread (n1), Exception (c1), RuntimeTypeSystem (n1), StressLog (c2) - **Sub-descriptors**: GC (workstation + server) and managed type descriptors ### ILC managed type descriptor (`ManagedDataDescriptorNode`) - Computes managed type field offsets at compile time in ILC - Emits a `ContractDescriptor` (`DotNetManagedContractDescriptor`) with JSON-encoded type layouts using `Utf8JsonWriter` - Types and fields discovered via `[DataContract]` attribute on types in `MetadataManager.GetTypesWithEETypes()` - Type name mangling: `System.Threading.Thread` -> `System_Threading_Thread` - Referenced by the native descriptor as a sub-descriptor via `CDAC_GLOBAL_SUB_DESCRIPTOR` - Currently registers `System.Threading.Thread` fields (`ManagedThreadId`, `Name`) ### GC sub-descriptor - Enabled GC sub-descriptor for NativeAOT by setting `GC_INTERFACE_*_VERSION` before `GC_Initialize` - Added `GC_DESCRIPTOR` compile definition (guarded on non-WASM) - Linked both WKS and SVR GC descriptor objects into `Runtime.ServerGC` (ServerGC compiles both paths) - Added `#ifdef HEAP_ANALYZE` guards in shared GC datadescriptor files (NativeAOT disables `HEAP_ANALYZE`) ### Attribute-based type discovery - `[DataContract]` attribute in `System.Diagnostics` namespace (internal, targets Class/Struct/Field) - Applied to `System.Threading.Thread` fields in `Thread.NativeAot.cs` - ILC scans for annotated types in `GetTypesWithEETypes()` ensuring only types with MethodTables are included ### Build integration - CMake integration using shared `clrdatadescriptors.cmake` infrastructure - `nativeaot_runtime_includes` interface library captures all Runtime include paths for cross-target compilation - Separate GC descriptor targets for workstation and server GC - `cdac-build-tool` enabled for NativeAOT via `ClrNativeAotSubset` in `runtime.proj` - Symbol export via `--export-dynamic-symbol` in `Microsoft.NETCore.Native.targets` (WASM excluded) - Local copy of `cdacdata.h` template in `Runtime/inc/` (matching GC pattern for self-contained builds) ### Key design decisions - **Contract versions**: `n1` for NativeAOT-specific contracts, `c1`/`c2` for contracts shared with CoreCLR (same version) - **ThreadStore**: Uses `SPTR_DECL/SPTR_IMPL` for `s_pThreadStore` static member, matching CoreCLR pattern - **Singleton node**: `ManagedDataDescriptorNode` does not override `CompareToImpl` — follows the ILC singleton pattern (base class throws on duplicates) - **SList**: Unified `slist.h` shared between CoreCLR VM and NativeAOT Runtime ## Validation - Build: `build.cmd clr.aot+libs -rc release` — 0 errors, 0 warnings - Symbol verified in `Runtime.WorkstationGC.lib` via dumpbin - cDAC reader tests: 1586/1586 passed - tools.cdac tests: All passed - Dump inspection: All 3 sub-descriptors verified (main: 4 contracts/11 types/20 globals, managed: System_Threading_Thread with fields, GC: 1 contract/10 types/41 globals) --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../gc/datadescriptor/datadescriptor.h | 2 + .../gc/datadescriptor/datadescriptor.inc | 4 + .../Microsoft.NETCore.Native.targets | 2 + src/coreclr/nativeaot/Runtime/CMakeLists.txt | 13 ++ src/coreclr/nativeaot/Runtime/DebugHeader.cpp | 10 +- .../nativeaot/Runtime/Full/CMakeLists.txt | 4 +- .../nativeaot/Runtime/RuntimeInstance.cpp | 11 +- .../nativeaot/Runtime/RuntimeInstance.h | 1 - .../Runtime/datadescriptor/CMakeLists.txt | 49 +++++ .../Runtime/datadescriptor/datadescriptor.h | 30 +++ .../Runtime/datadescriptor/datadescriptor.inc | 179 ++++++++++++++++++ .../nativeaot/Runtime/gcheaputilities.cpp | 3 + .../nativeaot/Runtime/inc/MethodTable.h | 14 ++ src/coreclr/nativeaot/Runtime/inc/cdacdata.h | 4 + src/coreclr/nativeaot/Runtime/inc/stressLog.h | 16 ++ src/coreclr/nativeaot/Runtime/threadstore.cpp | 2 + src/coreclr/nativeaot/Runtime/threadstore.h | 9 + .../src/System.Private.CoreLib.csproj | 1 + .../Diagnostics/DataContractAttribute.cs | 16 ++ .../src/System/Threading/Thread.NativeAot.cs | 3 + src/coreclr/runtime.proj | 2 +- .../ManagedDataDescriptorNode.cs | 172 +++++++++++++++++ .../Compiler/ManagedDataDescriptorProvider.cs | 21 ++ .../ILCompiler.Compiler.csproj | 2 + src/coreclr/tools/aot/ILCompiler/Program.cs | 3 + 25 files changed, 557 insertions(+), 16 deletions(-) create mode 100644 src/coreclr/nativeaot/Runtime/datadescriptor/CMakeLists.txt create mode 100644 src/coreclr/nativeaot/Runtime/datadescriptor/datadescriptor.h create mode 100644 src/coreclr/nativeaot/Runtime/datadescriptor/datadescriptor.inc create mode 100644 src/coreclr/nativeaot/Runtime/inc/cdacdata.h create mode 100644 src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/DataContractAttribute.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ManagedDataDescriptorNode.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ManagedDataDescriptorProvider.cs diff --git a/src/coreclr/gc/datadescriptor/datadescriptor.h b/src/coreclr/gc/datadescriptor/datadescriptor.h index d9589fedb0e056..d60a73e8e24a92 100644 --- a/src/coreclr/gc/datadescriptor/datadescriptor.h +++ b/src/coreclr/gc/datadescriptor/datadescriptor.h @@ -67,9 +67,11 @@ struct cdac_data #endif // !USE_REGIONS /* For use in GCHeapAnalyzeData APIs */ +#ifdef HEAP_ANALYZE GC_HEAP_FIELD(InternalRootArray, internal_root_array) GC_HEAP_FIELD(InternalRootArrayIndex, internal_root_array_index) GC_HEAP_FIELD(HeapAnalyzeSuccess, heap_analyze_success) +#endif // HEAP_ANALYZE /* For use in GCInterestingInfo APIs */ GC_HEAP_FIELD(InterestingData, interesting_data_per_heap) diff --git a/src/coreclr/gc/datadescriptor/datadescriptor.inc b/src/coreclr/gc/datadescriptor/datadescriptor.inc index 49243687f13f4b..2937aa7627eb8e 100644 --- a/src/coreclr/gc/datadescriptor/datadescriptor.inc +++ b/src/coreclr/gc/datadescriptor/datadescriptor.inc @@ -27,9 +27,11 @@ CDAC_TYPE_FIELD(GCHeap, T_POINTER, SavedSweepEphemeralSeg, cdac_data::SavedSweepEphemeralStart) #endif // !USE_REGIONS CDAC_TYPE_FIELD(GCHeap, TYPE(OomHistory), OomData, cdac_data::OomData) +#ifdef HEAP_ANALYZE CDAC_TYPE_FIELD(GCHeap, T_POINTER, InternalRootArray, cdac_data::InternalRootArray) CDAC_TYPE_FIELD(GCHeap, T_NUINT, InternalRootArrayIndex, cdac_data::InternalRootArrayIndex) CDAC_TYPE_FIELD(GCHeap, T_INT32, HeapAnalyzeSuccess, cdac_data::HeapAnalyzeSuccess) +#endif // HEAP_ANALYZE CDAC_TYPE_FIELD(GCHeap, T_POINTER, InterestingData, cdac_data::InterestingData) CDAC_TYPE_FIELD(GCHeap, T_POINTER, CompactReasons, cdac_data::CompactReasons) CDAC_TYPE_FIELD(GCHeap, T_POINTER, ExpandMechanisms, cdac_data::ExpandMechanisms) @@ -166,9 +168,11 @@ CDAC_GLOBAL_POINTER(GCHeapSavedSweepEphemeralSeg, cdac_data::SavedSweepEphemeralStart) #endif // !USE_REGIONS CDAC_GLOBAL_POINTER(GCHeapOomData, cdac_data::OomData) +#ifdef HEAP_ANALYZE CDAC_GLOBAL_POINTER(GCHeapInternalRootArray, cdac_data::InternalRootArray) CDAC_GLOBAL_POINTER(GCHeapInternalRootArrayIndex, cdac_data::InternalRootArrayIndex) CDAC_GLOBAL_POINTER(GCHeapHeapAnalyzeSuccess, cdac_data::HeapAnalyzeSuccess) +#endif // HEAP_ANALYZE CDAC_GLOBAL_POINTER(GCHeapInterestingData, cdac_data::InterestingData) CDAC_GLOBAL_POINTER(GCHeapCompactReasons, cdac_data::CompactReasons) CDAC_GLOBAL_POINTER(GCHeapExpandMechanisms, cdac_data::ExpandMechanisms) diff --git a/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets b/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets index 2c1e91afc8ba16..950cc3d538a236 100644 --- a/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets +++ b/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets @@ -236,6 +236,8 @@ The .NET Foundation licenses this file to you under the MIT license. + + diff --git a/src/coreclr/nativeaot/Runtime/CMakeLists.txt b/src/coreclr/nativeaot/Runtime/CMakeLists.txt index 9f3a80c702358e..058e4e20adde42 100644 --- a/src/coreclr/nativeaot/Runtime/CMakeLists.txt +++ b/src/coreclr/nativeaot/Runtime/CMakeLists.txt @@ -290,6 +290,9 @@ if(CLR_CMAKE_TARGET_ARCH_I386 OR CLR_CMAKE_TARGET_ARCH_ARM) add_definitions(-DINTERFACE_DISPATCH_CACHE_HAS_CELL_BACKPOINTER) endif() add_definitions(-D_LIB) +if(NOT CLR_CMAKE_TARGET_ARCH_WASM) + add_definitions(-DGC_DESCRIPTOR) +endif() # there is a problem with undefined symbols when this is set # add_definitions(-DSTRESS_HEAP) @@ -346,3 +349,13 @@ endif() if(FEATURE_PERFTRACING) add_subdirectory(eventpipe) endif() + +if(NOT CLR_CMAKE_TARGET_ARCH_WASM) + # Create an interface library that captures the Runtime's include directories + # for use by the datadescriptor intermediary compilation. + add_library(nativeaot_runtime_includes INTERFACE) + get_property(_runtime_includes DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY INCLUDE_DIRECTORIES) + target_include_directories(nativeaot_runtime_includes INTERFACE ${_runtime_includes}) + + add_subdirectory(datadescriptor) +endif() diff --git a/src/coreclr/nativeaot/Runtime/DebugHeader.cpp b/src/coreclr/nativeaot/Runtime/DebugHeader.cpp index 9c2a06892c629f..0f86c1e2383792 100644 --- a/src/coreclr/nativeaot/Runtime/DebugHeader.cpp +++ b/src/coreclr/nativeaot/Runtime/DebugHeader.cpp @@ -80,8 +80,9 @@ struct DotNetRuntimeDebugHeader // v1-v4 were never doc'ed but history is source control if you need it // v5 - Thread now has an m_eeAllocContext field and the previous m_rgbAllocContextBuffer // field is nested inside of it. + // v6 - Removed RuntimeInstance.m_pThreadStore field, added g_pThreadStore global. // - const uint16_t MajorVersion = 5; + const uint16_t MajorVersion = 6; // This counter can be incremented to indicate back-compatible changes // This field must be encoded little endian, regardless of the typical endianness of @@ -255,13 +256,10 @@ extern "C" void PopulateDebugHeaders() MAKE_SIZE_ENTRY(StressMsg); MAKE_DEBUG_FIELD_ENTRY(StressMsg, args); - MAKE_SIZE_ENTRY(RuntimeInstance); - MAKE_DEBUG_FIELD_ENTRY(RuntimeInstance, m_pThreadStore); - MAKE_GLOBAL_ENTRY(g_CrashInfoBuffer); - RuntimeInstance *g_pTheRuntimeInstance = GetRuntimeInstance(); - MAKE_GLOBAL_ENTRY(g_pTheRuntimeInstance); + ThreadStore *g_pThreadStore = ThreadStore::s_pThreadStore; + MAKE_GLOBAL_ENTRY(g_pThreadStore); MAKE_GLOBAL_ENTRY(g_gcDacGlobals); diff --git a/src/coreclr/nativeaot/Runtime/Full/CMakeLists.txt b/src/coreclr/nativeaot/Runtime/Full/CMakeLists.txt index 74cdeca700a1ae..1cd73bfd07823d 100644 --- a/src/coreclr/nativeaot/Runtime/Full/CMakeLists.txt +++ b/src/coreclr/nativeaot/Runtime/Full/CMakeLists.txt @@ -24,11 +24,11 @@ endif (CLR_CMAKE_TARGET_WIN32) add_library(Runtime.WorkstationGC STATIC ${COMMON_RUNTIME_SOURCES} ${FULL_RUNTIME_SOURCES} ${RUNTIME_ARCH_ASM_OBJECTS}) add_dependencies(Runtime.WorkstationGC aot_eventing_headers) -target_link_libraries(Runtime.WorkstationGC PRIVATE aotminipal) +target_link_libraries(Runtime.WorkstationGC PRIVATE aotminipal nativeaot_cdac_contract_descriptor nativeaot_gc_wks_descriptor) add_library(Runtime.ServerGC STATIC ${COMMON_RUNTIME_SOURCES} ${FULL_RUNTIME_SOURCES} ${SERVER_GC_SOURCES} ${RUNTIME_ARCH_ASM_OBJECTS}) add_dependencies(Runtime.ServerGC aot_eventing_headers) -target_link_libraries(Runtime.ServerGC PRIVATE aotminipal) +target_link_libraries(Runtime.ServerGC PRIVATE aotminipal nativeaot_cdac_contract_descriptor nativeaot_gc_wks_descriptor nativeaot_gc_svr_descriptor) add_library(standalonegc-disabled STATIC ${STANDALONEGC_DISABLED_SOURCES}) add_dependencies(standalonegc-disabled aot_eventing_headers) diff --git a/src/coreclr/nativeaot/Runtime/RuntimeInstance.cpp b/src/coreclr/nativeaot/Runtime/RuntimeInstance.cpp index 74a930a0e9bdc8..141e53aa4af2e3 100644 --- a/src/coreclr/nativeaot/Runtime/RuntimeInstance.cpp +++ b/src/coreclr/nativeaot/Runtime/RuntimeInstance.cpp @@ -39,7 +39,7 @@ uint8_t g_CrashInfoBuffer[MAX_CRASHINFOBUFFER_SIZE] = { 0 }; ThreadStore * RuntimeInstance::GetThreadStore() { - return m_pThreadStore; + return ThreadStore::s_pThreadStore; } FCIMPL1(uint8_t *, RhGetCrashInfoBuffer, int32_t* pcbMaxSize) @@ -179,7 +179,6 @@ RuntimeInstance::OsModuleList* RuntimeInstance::GetOsModuleList() #ifndef DACCESS_COMPILE RuntimeInstance::RuntimeInstance() : - m_pThreadStore(NULL), m_CodeManager(NULL), m_conservativeStackReportingEnabled(false), m_pUnboxingStubsRegion(NULL) @@ -188,10 +187,10 @@ RuntimeInstance::RuntimeInstance() : RuntimeInstance::~RuntimeInstance() { - if (NULL != m_pThreadStore) + if (NULL != ThreadStore::s_pThreadStore) { - delete m_pThreadStore; - m_pThreadStore = NULL; + delete ThreadStore::s_pThreadStore; + ThreadStore::s_pThreadStore = NULL; } } @@ -314,11 +313,11 @@ bool RuntimeInstance::Initialize(HANDLE hPalInstance) pThreadStore.SuppressRelease(); pRuntimeInstance.SuppressRelease(); - pRuntimeInstance->m_pThreadStore = pThreadStore; pRuntimeInstance->m_hPalInstance = hPalInstance; ASSERT_MSG(g_pTheRuntimeInstance == NULL, "multi-instances are not supported"); g_pTheRuntimeInstance = pRuntimeInstance; + ThreadStore::s_pThreadStore = pThreadStore; return true; } diff --git a/src/coreclr/nativeaot/Runtime/RuntimeInstance.h b/src/coreclr/nativeaot/Runtime/RuntimeInstance.h index 349bf1dbc7b578..e50ddac18dfd40 100644 --- a/src/coreclr/nativeaot/Runtime/RuntimeInstance.h +++ b/src/coreclr/nativeaot/Runtime/RuntimeInstance.h @@ -21,7 +21,6 @@ class RuntimeInstance friend class Thread; friend void PopulateDebugHeaders(); - PTR_ThreadStore m_pThreadStore; HANDLE m_hPalInstance; // this is the HANDLE passed into DllMain public: diff --git a/src/coreclr/nativeaot/Runtime/datadescriptor/CMakeLists.txt b/src/coreclr/nativeaot/Runtime/datadescriptor/CMakeLists.txt new file mode 100644 index 00000000000000..344dc601110f3d --- /dev/null +++ b/src/coreclr/nativeaot/Runtime/datadescriptor/CMakeLists.txt @@ -0,0 +1,49 @@ +set(CMAKE_INCLUDE_CURRENT_DIR OFF) + +# cDAC contract descriptor for NativeAOT +# +# This uses the shared datadescriptor infrastructure from +# src/coreclr/debug/datadescriptor-shared/ to generate a +# DotNetRuntimeContractDescriptor symbol in the NativeAOT runtime. +# +# The nativeaot_runtime_includes interface library (created in the parent +# Runtime/CMakeLists.txt) provides the same include directories used by +# the regular NativeAOT Runtime build. + +include(${CLR_DIR}/clrdatadescriptors.cmake) + +add_library(nativeaot_descriptor_interface INTERFACE) +target_include_directories(nativeaot_descriptor_interface INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(nativeaot_descriptor_interface INTERFACE nativeaot_runtime_includes) +add_dependencies(nativeaot_descriptor_interface aot_eventing_headers) +generate_data_descriptors( + LIBRARY_NAME nativeaot_cdac_contract_descriptor + CONTRACT_NAME "DotNetRuntimeContractDescriptor" + INTERFACE_TARGET nativeaot_descriptor_interface + EXPORT_VISIBLE) + +# GC contract descriptors (workstation + server). +# The GC has its own data descriptor exposed as a sub-descriptor via gc_descriptor in GcDacVars. +set(GC_DESCRIPTOR_DIR "${CLR_DIR}/gc/datadescriptor") + +add_library(nativeaot_gc_wks_descriptor_interface INTERFACE) +target_include_directories(nativeaot_gc_wks_descriptor_interface INTERFACE + ${GC_DESCRIPTOR_DIR}) +target_link_libraries(nativeaot_gc_wks_descriptor_interface INTERFACE nativeaot_runtime_includes) +add_dependencies(nativeaot_gc_wks_descriptor_interface aot_eventing_headers) +generate_data_descriptors( + LIBRARY_NAME nativeaot_gc_wks_descriptor + CONTRACT_NAME "GCContractDescriptorWKS" + INTERFACE_TARGET nativeaot_gc_wks_descriptor_interface) + +add_library(nativeaot_gc_svr_descriptor_interface INTERFACE) +target_include_directories(nativeaot_gc_svr_descriptor_interface INTERFACE + ${GC_DESCRIPTOR_DIR}) +target_link_libraries(nativeaot_gc_svr_descriptor_interface INTERFACE nativeaot_runtime_includes) +add_dependencies(nativeaot_gc_svr_descriptor_interface aot_eventing_headers) +target_compile_definitions(nativeaot_gc_svr_descriptor_interface INTERFACE -DSERVER_GC) +generate_data_descriptors( + LIBRARY_NAME nativeaot_gc_svr_descriptor + CONTRACT_NAME "GCContractDescriptorSVR" + INTERFACE_TARGET nativeaot_gc_svr_descriptor_interface) diff --git a/src/coreclr/nativeaot/Runtime/datadescriptor/datadescriptor.h b/src/coreclr/nativeaot/Runtime/datadescriptor/datadescriptor.h new file mode 100644 index 00000000000000..4b5b4e1824f413 --- /dev/null +++ b/src/coreclr/nativeaot/Runtime/datadescriptor/datadescriptor.h @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "common.h" +#include "gcenv.h" +#include "gcheaputilities.h" +#include "gcinterface.dac.h" +#include "rhassert.h" +#include "TargetPtrs.h" +#include "PalLimitedContext.h" +#include "Pal.h" +#include "holder.h" +#include "RuntimeInstance.h" +#include "regdisplay.h" +#include "StackFrameIterator.h" +#include "thread.h" +#include "threadstore.h" + +#include +#include + +GPTR_DECL(MethodTable, g_pFreeObjectEEType); +GPTR_DECL(StressLog, g_pStressLog); + +// ILC emits a ContractDescriptor named "DotNetManagedContractDescriptor" with +// managed type layouts. We take its address so datadescriptor.inc can reference +// it as a sub-descriptor via CDAC_GLOBAL_SUB_DESCRIPTOR. +struct ContractDescriptor; +extern "C" ContractDescriptor DotNetManagedContractDescriptor; +static const void* g_pManagedContractDescriptor = &DotNetManagedContractDescriptor; diff --git a/src/coreclr/nativeaot/Runtime/datadescriptor/datadescriptor.inc b/src/coreclr/nativeaot/Runtime/datadescriptor/datadescriptor.inc new file mode 100644 index 00000000000000..a3aaf5376bfddf --- /dev/null +++ b/src/coreclr/nativeaot/Runtime/datadescriptor/datadescriptor.inc @@ -0,0 +1,179 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// +// No include guards. This file is included multiple times. +// +// NativeAOT data descriptor declarations for the cDAC contract system. +// This file defines the types, fields, and globals that diagnostic tools +// need to inspect NativeAOT runtime state. +// +// When modifying this file (adding/removing types, fields, or globals), you must also: +// 1. Update the corresponding contract doc in docs/design/datacontracts/.md +// 2. Update the managed data class in src/native/managed/cdac/.../Data/.cs +// 3. Update the contract implementation in src/native/managed/cdac/.../Contracts/.cs +// 4. Update the mock descriptors and tests in src/native/managed/cdac/tests/. + +CDAC_BASELINE("empty") +CDAC_TYPES_BEGIN() + +// ======================== +// Thread and ThreadStore +// ======================== + +CDAC_TYPE_BEGIN(Thread) +CDAC_TYPE_INDETERMINATE(Thread) +CDAC_TYPE_FIELD(Thread, T_UINT64, OSId, offsetof(RuntimeThreadLocals, m_threadId)) +CDAC_TYPE_FIELD(Thread, T_UINT32, State, offsetof(RuntimeThreadLocals, m_ThreadStateFlags)) +CDAC_TYPE_FIELD(Thread, T_POINTER, LinkNext, offsetof(RuntimeThreadLocals, m_pNext)) +CDAC_TYPE_FIELD(Thread, T_POINTER, ExceptionTracker, offsetof(RuntimeThreadLocals, m_pExInfoStackHead)) +CDAC_TYPE_FIELD(Thread, T_POINTER, CachedStackBase, offsetof(RuntimeThreadLocals, m_pStackHigh)) +CDAC_TYPE_FIELD(Thread, T_POINTER, CachedStackLimit, offsetof(RuntimeThreadLocals, m_pStackLow)) +// Thread inherits from RuntimeThreadLocals at offset 0, so EEAllocContext is directly on Thread +CDAC_TYPE_FIELD(Thread, TYPE(EEAllocContext), AllocContext, offsetof(RuntimeThreadLocals, m_eeAllocContext)) +CDAC_TYPE_FIELD(Thread, T_POINTER, TransitionFrame, offsetof(RuntimeThreadLocals, m_pTransitionFrame)) +CDAC_TYPE_END(Thread) + +CDAC_TYPE_BEGIN(ThreadStore) +CDAC_TYPE_INDETERMINATE(ThreadStore) +CDAC_TYPE_FIELD(ThreadStore, T_POINTER, FirstThreadLink, cdac_data::FirstThreadLink) +CDAC_TYPE_END(ThreadStore) + +// ======================== +// Allocation Context +// ======================== + +CDAC_TYPE_BEGIN(EEAllocContext) +CDAC_TYPE_INDETERMINATE(EEAllocContext) +CDAC_TYPE_FIELD(EEAllocContext, TYPE(GCAllocContext), GCAllocationContext, offsetof(ee_alloc_context, m_rgbAllocContextBuffer)) +CDAC_TYPE_END(EEAllocContext) + +CDAC_TYPE_BEGIN(GCAllocContext) +CDAC_TYPE_INDETERMINATE(GCAllocContext) +CDAC_TYPE_FIELD(GCAllocContext, T_POINTER, Pointer, offsetof(gc_alloc_context, alloc_ptr)) +CDAC_TYPE_FIELD(GCAllocContext, T_POINTER, Limit, offsetof(gc_alloc_context, alloc_limit)) +CDAC_TYPE_FIELD(GCAllocContext, T_INT64, AllocBytes, offsetof(gc_alloc_context, alloc_bytes)) +CDAC_TYPE_FIELD(GCAllocContext, T_INT64, AllocBytesLoh, offsetof(gc_alloc_context, alloc_bytes_uoh)) +CDAC_TYPE_END(GCAllocContext) + +// ======================== +// MethodTable (EEType) +// ======================== + +CDAC_TYPE_BEGIN(MethodTable) +CDAC_TYPE_INDETERMINATE(MethodTable) +CDAC_TYPE_FIELD(MethodTable, T_UINT32, Flags, cdac_data::Flags) +CDAC_TYPE_FIELD(MethodTable, T_UINT32, BaseSize, cdac_data::BaseSize) +CDAC_TYPE_FIELD(MethodTable, T_POINTER, RelatedType, cdac_data::RelatedType) +CDAC_TYPE_FIELD(MethodTable, T_UINT16, NumVtableSlots, cdac_data::NumVtableSlots) +CDAC_TYPE_FIELD(MethodTable, T_UINT16, NumInterfaces, cdac_data::NumInterfaces) +CDAC_TYPE_FIELD(MethodTable, T_UINT32, HashCode, cdac_data::HashCode) +CDAC_TYPE_FIELD(MethodTable, T_POINTER, VTable, cdac_data::VTable) +CDAC_TYPE_END(MethodTable) + +// ======================== +// Exception Info +// ======================== + +CDAC_TYPE_BEGIN(ExInfo) +CDAC_TYPE_INDETERMINATE(ExInfo) +CDAC_TYPE_FIELD(ExInfo, T_POINTER, PreviousNestedInfo, offsetof(ExInfo, m_pPrevExInfo)) +CDAC_TYPE_FIELD(ExInfo, T_POINTER, ThrownObject, offsetof(ExInfo, m_exception)) +CDAC_TYPE_END(ExInfo) + +// ======================== +// StressLog +// ======================== + +CDAC_TYPE_BEGIN(StressLog) +CDAC_TYPE_SIZE(sizeof(StressLog)) +CDAC_TYPE_FIELD(StressLog, T_UINT32, LoggedFacilities, offsetof(StressLog, facilitiesToLog)) +CDAC_TYPE_FIELD(StressLog, T_UINT32, Level, offsetof(StressLog, levelToLog)) +CDAC_TYPE_FIELD(StressLog, T_UINT32, MaxSizePerThread, offsetof(StressLog, MaxSizePerThread)) +CDAC_TYPE_FIELD(StressLog, T_UINT32, MaxSizeTotal, offsetof(StressLog, MaxSizeTotal)) +CDAC_TYPE_FIELD(StressLog, T_INT32, TotalChunks, offsetof(StressLog, totalChunk)) +CDAC_TYPE_FIELD(StressLog, T_POINTER, Logs, offsetof(StressLog, logs)) +CDAC_TYPE_FIELD(StressLog, T_UINT64, TickFrequency, offsetof(StressLog, tickFrequency)) +CDAC_TYPE_FIELD(StressLog, T_UINT64, StartTimestamp, offsetof(StressLog, startTimeStamp)) +CDAC_TYPE_END(StressLog) + +CDAC_TYPE_BEGIN(ThreadStressLog) +CDAC_TYPE_INDETERMINATE(ThreadStressLog) +CDAC_TYPE_FIELD(ThreadStressLog, T_POINTER, Next, cdac_data::Next) +CDAC_TYPE_FIELD(ThreadStressLog, T_UINT64, ThreadId, cdac_data::ThreadId) +CDAC_TYPE_FIELD(ThreadStressLog, T_UINT8, WriteHasWrapped, cdac_data::WriteHasWrapped) +CDAC_TYPE_FIELD(ThreadStressLog, T_POINTER, CurrentPtr, cdac_data::CurrentPtr) +CDAC_TYPE_FIELD(ThreadStressLog, T_POINTER, ChunkListHead, cdac_data::ChunkListHead) +CDAC_TYPE_FIELD(ThreadStressLog, T_POINTER, ChunkListTail, cdac_data::ChunkListTail) +CDAC_TYPE_FIELD(ThreadStressLog, T_POINTER, CurrentWriteChunk, cdac_data::CurrentWriteChunk) +CDAC_TYPE_END(ThreadStressLog) + +CDAC_TYPE_BEGIN(StressLogChunk) +CDAC_TYPE_SIZE(sizeof(StressLogChunk)) +CDAC_TYPE_FIELD(StressLogChunk, T_POINTER, Prev, offsetof(StressLogChunk, prev)) +CDAC_TYPE_FIELD(StressLogChunk, T_POINTER, Next, offsetof(StressLogChunk, next)) +CDAC_TYPE_FIELD(StressLogChunk, T_ARRAY(T_UINT8), Buf, offsetof(StressLogChunk, buf)) +CDAC_TYPE_FIELD(StressLogChunk, T_UINT32, Sig1, offsetof(StressLogChunk, dwSig1)) +CDAC_TYPE_FIELD(StressLogChunk, T_UINT32, Sig2, offsetof(StressLogChunk, dwSig2)) +CDAC_TYPE_END(StressLogChunk) + +CDAC_TYPE_BEGIN(StressMsgHeader) +CDAC_TYPE_SIZE(sizeof(StressMsg)) +CDAC_TYPE_END(StressMsgHeader) + +CDAC_TYPE_BEGIN(StressMsg) +CDAC_TYPE_INDETERMINATE(StressMsg) +CDAC_TYPE_FIELD(StressMsg, TYPE(StressMsgHeader), Header, 0) +CDAC_TYPE_FIELD(StressMsg, T_POINTER, Args, offsetof(StressMsg, args)) +CDAC_TYPE_END(StressMsg) + +CDAC_TYPES_END() + +// ======================== +// Globals +// ======================== + +CDAC_GLOBALS_BEGIN() + +CDAC_GLOBAL_POINTER(ThreadStore, &ThreadStore::s_pThreadStore) + +CDAC_GLOBAL_POINTER(FreeObjectMethodTable, &g_pFreeObjectEEType) + +CDAC_GLOBAL_POINTER(GCLowestAddress, &g_lowest_address) +CDAC_GLOBAL_POINTER(GCHighestAddress, &g_highest_address) + +// Thread state flag constants +CDAC_GLOBAL(ThreadStateFlagAttached, T_UINT32, 0x00000001) +CDAC_GLOBAL(ThreadStateFlagDetached, T_UINT32, 0x00000002) + +// Object contract globals +#ifdef TARGET_64BIT +CDAC_GLOBAL(ObjectToMethodTableUnmask, T_UINT8, 7) +#else +CDAC_GLOBAL(ObjectToMethodTableUnmask, T_UINT8, 3) +#endif + +// StressLog globals (no module table in NativeAOT) +CDAC_GLOBAL(StressLogEnabled, T_UINT8, 1) +CDAC_GLOBAL_POINTER(StressLog, &g_pStressLog) +CDAC_GLOBAL(StressLogHasModuleTable, T_UINT8, 0) +CDAC_GLOBAL(StressLogChunkSize, T_UINT32, STRESSLOG_CHUNK_SIZE) +CDAC_GLOBAL(StressLogValidChunkSig, T_UINT32, 0xCFCFCFCF) +CDAC_GLOBAL(StressLogMaxMessageSize, T_UINT64, (uint64_t)StressMsg::maxMsgSize) + +// Contracts: declare which contracts this runtime supports +CDAC_GLOBAL_CONTRACT(Thread, n1) +CDAC_GLOBAL_CONTRACT(Exception, c1) +CDAC_GLOBAL_CONTRACT(RuntimeTypeSystem, n1) +CDAC_GLOBAL_CONTRACT(StressLog, c2) + +// Managed type sub-descriptor: ILC emits a ContractDescriptor with managed type layouts +// that the cDAC reader merges as a sub-descriptor. This provides field offsets for managed +// types (e.g., ConditionalWeakTable internals, IdDispenser) that are not exposed through +// native C++ data descriptors. +CDAC_GLOBAL_SUB_DESCRIPTOR(ManagedTypes, &g_pManagedContractDescriptor) + +// GC sub-descriptor: the GC populates gc_descriptor during GC_Initialize. +// It is important for subdescriptor pointers to be the last pointers. +CDAC_GLOBAL_SUB_DESCRIPTOR(GC, &(g_gc_dac_vars.gc_descriptor)) + +CDAC_GLOBALS_END() diff --git a/src/coreclr/nativeaot/Runtime/gcheaputilities.cpp b/src/coreclr/nativeaot/Runtime/gcheaputilities.cpp index 2678b12c1aea0b..edafc599814e51 100644 --- a/src/coreclr/nativeaot/Runtime/gcheaputilities.cpp +++ b/src/coreclr/nativeaot/Runtime/gcheaputilities.cpp @@ -7,6 +7,7 @@ #include "gchandleutilities.h" #include "gceventstatus.h" +#include "gcinterface.h" // This is the global GC heap, maintained by the VM. GPTR_IMPL(IGCHeap, g_pGCHeap); @@ -71,6 +72,8 @@ HRESULT GCHeapUtilities::InitializeDefaultGC() IGCHeap* heap; IGCHandleManager* manager; + g_gc_dac_vars.major_version_number = GC_INTERFACE_MAJOR_VERSION; + g_gc_dac_vars.minor_version_number = GC_INTERFACE_MINOR_VERSION; HRESULT initResult = GC_Initialize(nullptr, &heap, &manager, &g_gc_dac_vars); if (initResult == S_OK) { diff --git a/src/coreclr/nativeaot/Runtime/inc/MethodTable.h b/src/coreclr/nativeaot/Runtime/inc/MethodTable.h index 89ff445f6174d9..8ff6c556404cf2 100644 --- a/src/coreclr/nativeaot/Runtime/inc/MethodTable.h +++ b/src/coreclr/nativeaot/Runtime/inc/MethodTable.h @@ -12,6 +12,8 @@ class MethodTable; class TypeManager; struct TypeManagerHandle; +#include "cdacdata.h" + //------------------------------------------------------------------------------------------------- // The subset of TypeFlags that NativeAOT knows about at runtime // This should match the TypeFlags enum in the managed type system. @@ -86,6 +88,7 @@ class MethodTable { friend class AsmOffsets; friend void PopulateDebugHeaders(); + friend struct ::cdac_data; private: struct RelatedTypeUnion @@ -346,4 +349,15 @@ class MethodTable UInt32_BOOL SanityCheck() { return Validate(); } }; +template<> struct cdac_data +{ + static constexpr size_t Flags = offsetof(MethodTable, m_uFlags); + static constexpr size_t BaseSize = offsetof(MethodTable, m_uBaseSize); + static constexpr size_t RelatedType = offsetof(MethodTable, m_RelatedType); + static constexpr size_t NumVtableSlots = offsetof(MethodTable, m_usNumVtableSlots); + static constexpr size_t NumInterfaces = offsetof(MethodTable, m_usNumInterfaces); + static constexpr size_t HashCode = offsetof(MethodTable, m_uHashCode); + static constexpr size_t VTable = offsetof(MethodTable, m_VTable); +}; + #pragma warning(pop) diff --git a/src/coreclr/nativeaot/Runtime/inc/cdacdata.h b/src/coreclr/nativeaot/Runtime/inc/cdacdata.h new file mode 100644 index 00000000000000..ffbf4431b381c7 --- /dev/null +++ b/src/coreclr/nativeaot/Runtime/inc/cdacdata.h @@ -0,0 +1,4 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "../../../inc/cdacdata.h" diff --git a/src/coreclr/nativeaot/Runtime/inc/stressLog.h b/src/coreclr/nativeaot/Runtime/inc/stressLog.h index 17bf5fbfee1b20..888edc33961057 100644 --- a/src/coreclr/nativeaot/Runtime/inc/stressLog.h +++ b/src/coreclr/nativeaot/Runtime/inc/stressLog.h @@ -47,6 +47,8 @@ #if defined(STRESS_LOG) +#include "cdacdata.h" + // // Logging levels and facilities // @@ -532,6 +534,7 @@ class ThreadStressLog { PTR_Thread pThread; // thread associated with these stress logs StressMsg * origCurPtr; // this holds the original curPtr before we start the dump + template friend struct ::cdac_data; friend void PopulateDebugHeaders(); friend class StressLog; @@ -793,4 +796,17 @@ inline StressMsg* ThreadStressLog::AdvWritePastBoundary(int cArgs) { #endif // !STRESS_LOG || DACCESS_COMPILE #endif // !__GCENV_BASE_INCLUDED__ +#if defined(STRESS_LOG) +template<> struct cdac_data +{ + static constexpr size_t Next = offsetof(ThreadStressLog, next); + static constexpr size_t ThreadId = offsetof(ThreadStressLog, threadId); + static constexpr size_t WriteHasWrapped = offsetof(ThreadStressLog, writeHasWrapped); + static constexpr size_t CurrentPtr = offsetof(ThreadStressLog, curPtr); + static constexpr size_t ChunkListHead = offsetof(ThreadStressLog, chunkListHead); + static constexpr size_t ChunkListTail = offsetof(ThreadStressLog, chunkListTail); + static constexpr size_t CurrentWriteChunk = offsetof(ThreadStressLog, curWriteChunk); +}; +#endif // STRESS_LOG + #endif // StressLog_h diff --git a/src/coreclr/nativeaot/Runtime/threadstore.cpp b/src/coreclr/nativeaot/Runtime/threadstore.cpp index 0439f0c76058ab..c87207a562ba62 100644 --- a/src/coreclr/nativeaot/Runtime/threadstore.cpp +++ b/src/coreclr/nativeaot/Runtime/threadstore.cpp @@ -30,6 +30,8 @@ volatile uint32_t RhpTrapThreads = (uint32_t)TrapThreadsFlags::None; GVAL_IMPL_INIT(PTR_Thread, RhpSuspendingThread, 0); +SPTR_IMPL(ThreadStore, ThreadStore, s_pThreadStore); + ThreadStore * GetThreadStore() { return GetRuntimeInstance()->GetThreadStore(); diff --git a/src/coreclr/nativeaot/Runtime/threadstore.h b/src/coreclr/nativeaot/Runtime/threadstore.h index d2347f9a631ffa..5f855dc5b18abd 100644 --- a/src/coreclr/nativeaot/Runtime/threadstore.h +++ b/src/coreclr/nativeaot/Runtime/threadstore.h @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. #include "Crst.h" +#include "cdacdata.h" class Thread; class CLREventStatic; @@ -19,7 +20,9 @@ extern "C" void PopulateDebugHeaders(); class ThreadStore { + friend class RuntimeInstance; friend void PopulateDebugHeaders(); + friend struct ::cdac_data; SList m_ThreadList; PTR_RuntimeInstance m_pRuntimeInstance; @@ -29,6 +32,7 @@ class ThreadStore ThreadStore(); public: + SPTR_DECL(ThreadStore, s_pThreadStore); void LockThreadStore(); void UnlockThreadStore(); @@ -68,6 +72,11 @@ class ThreadStore }; typedef DPTR(ThreadStore) PTR_ThreadStore; +template<> struct cdac_data +{ + static constexpr size_t FirstThreadLink = offsetof(ThreadStore, m_ThreadList); +}; + ThreadStore * GetThreadStore(); #define FOREACH_THREAD(p_thread_name) \ diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj index d120f10da497ac..0f808ede316353 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj @@ -195,6 +195,7 @@ + diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/DataContractAttribute.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/DataContractAttribute.cs new file mode 100644 index 00000000000000..062a9d311c5972 --- /dev/null +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/DataContractAttribute.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics +{ + /// + /// When applied to a type, indicates that ILC should include its field layout in the + /// managed cDAC data descriptor so diagnostic tools can inspect instances without + /// runtime metadata or symbols. When applied to a field of such a type, indicates ILC should + /// include that field's offset in the descriptor. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field, Inherited = false)] + internal sealed class DataContractAttribute : Attribute + { + } +} diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs index 3f7aa4ffc429df..5bd5dcdc717cc9 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs @@ -13,6 +13,7 @@ namespace System.Threading { + [DataContract] public sealed partial class Thread { // Extra bits used in _threadState @@ -29,7 +30,9 @@ public sealed partial class Thread private volatile int _threadState = (int)ThreadState.Unstarted; private ThreadPriority _priority; + [DataContract] private ManagedThreadId _managedThreadId; + [DataContract] private string? _name; private StartHelper? _startHelper; private Exception? _startException; diff --git a/src/coreclr/runtime.proj b/src/coreclr/runtime.proj index 978d9a55a25e1b..2c968913b26f4c 100644 --- a/src/coreclr/runtime.proj +++ b/src/coreclr/runtime.proj @@ -3,7 +3,7 @@ <_BuildNativeTargetOS>$(TargetOS) <_BuildNativeTargetOS Condition="'$(TargetsLinuxBionic)' == 'true'">linux-bionic - true + true GetPgoDataPackagePath AcquireEmscriptenSdk;$(BuildRuntimeDependsOnTargets);GenerateEmccExports diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ManagedDataDescriptorNode.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ManagedDataDescriptorNode.cs new file mode 100644 index 00000000000000..696810e9629cc7 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ManagedDataDescriptorNode.cs @@ -0,0 +1,172 @@ +// 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.Diagnostics; +using System.IO; +using System.Text.Json; + +using Internal.Text; +using Internal.TypeSystem; +using Internal.TypeSystem.Ecma; + +namespace ILCompiler.DependencyAnalysis +{ + /// + /// Emits a ContractDescriptor for managed type layouts that the cDAC reader + /// can consume as a sub-descriptor. ILC knows managed type layouts at compile time, + /// so it can emit field offsets that would otherwise require runtime metadata resolution. + /// + /// Types are discovered by scanning MetadataManager.GetTypesWithEETypes() for types + /// annotated with [DataContract], ensuring only types that actually have a MethodTable + /// in the binary are included. + /// + public class ManagedDataDescriptorNode : ObjectNode, ISymbolDefinitionNode + { + private const string DataContractAttributeNamespace = "System.Diagnostics"; + private const string DataContractAttributeName = "DataContractAttribute"; + + public const string SymbolName = "DotNetManagedContractDescriptor"; + + public override ObjectNodeSection GetSection(NodeFactory factory) => + factory.Target.IsWindows ? ObjectNodeSection.ReadOnlyDataSection : ObjectNodeSection.DataSection; + + public override bool StaticDependenciesAreComputed => true; + public override bool IsShareable => false; + + public void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder sb) + { + sb.Append(nameMangler.NodeMangler.ExternVariable(new Utf8String(SymbolName))); + } + + public int Offset => 0; + + protected override string GetName(NodeFactory factory) => this.GetMangledName(factory.NameMangler); + + public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) + { + if (relocsOnly) + return new ObjectData(Array.Empty(), Array.Empty(), 1, new ISymbolDefinitionNode[] { this }); + + byte[] jsonBytes = BuildJsonDescriptor(factory); + + // Header layout: magic(8) + flags(4) + desc_size(4) + desc_ptr(ptr) + pointer_data_count(4) + pad(4) + pointer_data(ptr) + int headerSize = 8 + 4 + 4 + factory.Target.PointerSize + 4 + 4 + factory.Target.PointerSize; + + ObjectDataBuilder builder = new ObjectDataBuilder(factory, relocsOnly); + builder.RequireInitialPointerAlignment(); + builder.AddSymbol(this); + + // uint64_t magic + builder.EmitLong(0x0043414443434e44L); // "DNCCDAC\0" + + // uint32_t flags (bit 0 must be set; bit 1 indicates 32-bit pointers) + uint flags = (uint)(0x01 | (factory.Target.PointerSize == 4 ? 0x02 : 0x00)); + builder.EmitUInt(flags); + + // uint32_t descriptor_size + builder.EmitUInt((uint)jsonBytes.Length); + + // char* descriptor — points to inline JSON after the header + builder.EmitPointerReloc(this, headerSize); + + // uint32_t pointer_data_count = 0 + builder.EmitUInt(0); + + // uint32_t pad0 + builder.EmitUInt(0); + + // void** pointer_data = null + builder.EmitZeroPointer(); + + // Emit JSON bytes inline, null-terminated + Debug.Assert(builder.CountBytes == headerSize); + builder.EmitBytes(jsonBytes); + builder.EmitByte(0); + + return builder.ToObjectData(); + } + + /// + /// Build the JSON descriptor using the compact format expected by the cDAC reader's + /// ContractDescriptorParser. Types are objects with an optional "!" size sigil and + /// field-name properties mapped to their offsets. + /// + private static byte[] BuildJsonDescriptor(NodeFactory factory) + { + using var stream = new MemoryStream(); + using (var writer = new Utf8JsonWriter(stream)) + { + writer.WriteStartObject(); + writer.WriteNumber("version", 0); + writer.WriteString("baseline", "empty"); + + writer.WriteStartObject("types"); + foreach (TypeDesc type in factory.MetadataManager.GetTypesWithEETypes()) + { + if (type is not EcmaType ecmaType) + continue; + + if (!ecmaType.HasCustomAttribute(DataContractAttributeNamespace, DataContractAttributeName)) + continue; + + WriteType(writer, ecmaType); + } + writer.WriteEndObject(); + + writer.WriteStartObject("globals"); + writer.WriteEndObject(); + + writer.WriteStartObject("contracts"); + writer.WriteEndObject(); + + writer.WriteEndObject(); + } + + return stream.ToArray(); + } + + private static void WriteType(Utf8JsonWriter writer, EcmaType type) + { + writer.WriteStartObject(GetFullTypeName(type)); + + if (type.IsValueType) + { + writer.WriteNumber("!", type.InstanceFieldSize.AsInt); + } + + foreach (FieldDesc field in type.GetFields()) + { + if (field.IsStatic || field is not EcmaField ecmaField) + continue; + + if (!ecmaField.HasCustomAttribute(DataContractAttributeNamespace, DataContractAttributeName)) + continue; + + writer.WriteNumber(field.GetName(), field.Offset.AsInt); + } + + writer.WriteEndObject(); + } + + /// + /// Returns a fully-qualified type name for cDAC descriptors + /// (e.g., "System.Threading.Thread" or "System.Foo.Outer+Inner"). + /// + private static string GetFullTypeName(MetadataType type) + { + if (type.ContainingType is not null) + return $"{GetFullTypeName(type.ContainingType)}+{type.GetName()}"; + + string ns = type.GetNamespace(); + string name = type.GetName(); + + if (string.IsNullOrEmpty(ns)) + return name; + + return $"{ns}.{name}"; + } + + public override int ClassCode => 0x4d444e01; + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ManagedDataDescriptorProvider.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ManagedDataDescriptorProvider.cs new file mode 100644 index 00000000000000..734b9d1ad7faf8 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ManagedDataDescriptorProvider.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using ILCompiler.DependencyAnalysis; + +namespace ILCompiler +{ + /// + /// Compilation root provider that adds the managed cDAC data descriptor node. + /// The node discovers [DataContract]-annotated types from MetadataManager.GetTypesWithEETypes() + /// during object data emission, ensuring only types with MethodTables are included. + /// + public class ManagedDataDescriptorProvider : ICompilationRootProvider + { + void ICompilationRootProvider.AddCompilationRoots(IRootingServiceProvider rootProvider) + { + var descriptorNode = new ManagedDataDescriptorNode(); + rootProvider.AddCompilationRoot(descriptorNode, "Managed type descriptors for cDAC"); + } + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj index a03e3003f7417c..05a5439d22e056 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj @@ -598,6 +598,7 @@ + @@ -691,6 +692,7 @@ + diff --git a/src/coreclr/tools/aot/ILCompiler/Program.cs b/src/coreclr/tools/aot/ILCompiler/Program.cs index 45e0a2ff444800..2ba003f6a177c5 100644 --- a/src/coreclr/tools/aot/ILCompiler/Program.cs +++ b/src/coreclr/tools/aot/ILCompiler/Program.cs @@ -257,6 +257,9 @@ public int Run() } } + if (Get(_command.EnableDebugInfo)) + compilationRoots.Add(new ManagedDataDescriptorProvider()); + string win32resourcesModule = Get(_command.Win32ResourceModuleName); if (typeSystemContext.Target.IsWindows && !string.IsNullOrEmpty(win32resourcesModule)) { From e1703cfe2d3cdc275cc98a0fcdd27bdb9529c825 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Fri, 1 May 2026 01:06:12 -0400 Subject: [PATCH 056/115] Fix GetUserString DEBUG validation to use length-bounded string construction (#127620) ## Summary Fixes a DEBUG assertion failure in the cDAC `GetUserString` validation introduced in #127471. This failure wasn't in the method implementation but the validation logic. With this fix the runtime-diagnostics SOS test runs are clean. ## Problem The `GetUserString` implementation correctly matches native behavior by **not** null-terminating its output buffer when the string fits without truncation. However, the DEBUG validation block used `new string(char*)` which reads until a null terminator, potentially reading garbage bytes past the actual string data. This caused assertion failures in SOS integration tests: ``` UserString content mismatch: cDAC='ThrowExceptionA', DAC='ThrowException' ``` The extra `A` character came from whatever followed the string in the caller's buffer. ## Fix Use length-bounded string construction `new string(char*, 0, length)` with the known character count instead of relying on null termination. This is safe regardless of the buffer's contents past the written characters. Co-authored-by: Max Charlamb Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../MetaDataImportImpl.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/MetaDataImportImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/MetaDataImportImpl.cs index 2ee6c19c075907..214bdc56995981 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/MetaDataImportImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/MetaDataImportImpl.cs @@ -1456,8 +1456,11 @@ int IMetaDataImport.GetUserString(uint stk, char* szString, uint cchString, uint Debug.Assert(*pchString == pchLocal, $"String length mismatch: cDAC={*pchString}, DAC={pchLocal}"); if (szString is not null && cchString > 0) { - string cdacStr = new string(szString); - string dacStr = new string(szLocal); + // GetUserString does not null-terminate its output buffer (matching native behavior), + // so we must use length-bounded string construction instead of new string(char*). + int compareLen = Math.Min((int)pchLocal, (int)cchString); + string cdacStr = new string(szString, 0, compareLen); + string dacStr = new string(szLocal, 0, compareLen); Debug.Assert(cdacStr == dacStr, $"UserString content mismatch: cDAC='{cdacStr}', DAC='{dacStr}'"); } } From 9a61dfec9c989781b796a7885789bb80f5152215 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Thu, 30 Apr 2026 22:46:46 -0700 Subject: [PATCH 057/115] Remove an unnecessary switch from GenTree:TryGetUse (#127632) --- src/coreclr/jit/gentree.cpp | 306 ++++++++++++++---------------------- src/coreclr/jit/gentree.h | 8 +- src/coreclr/jit/gtlist.h | 6 +- 3 files changed, 129 insertions(+), 191 deletions(-) diff --git a/src/coreclr/jit/gentree.cpp b/src/coreclr/jit/gentree.cpp index dd85dcf67691bc..1b846460e89c8b 100644 --- a/src/coreclr/jit/gentree.cpp +++ b/src/coreclr/jit/gentree.cpp @@ -8069,239 +8069,175 @@ bool GenTree::TryGetUse(GenTree* operand, GenTree*** pUse) assert(operand != nullptr); assert(pUse != nullptr); - switch (OperGet()) + if (OperIsLeaf()) { - // Leaf nodes - case GT_LCL_VAR: - case GT_LCL_FLD: - case GT_LCL_ADDR: - case GT_CATCH_ARG: - case GT_ASYNC_CONTINUATION: - case GT_ASYNC_RESUME_INFO: - case GT_LABEL: - case GT_FTN_ADDR: - case GT_FTN_ENTRY: - case GT_RET_EXPR: - case GT_CNS_INT: - case GT_CNS_LNG: - case GT_CNS_DBL: - case GT_CNS_STR: -#if defined(FEATURE_SIMD) - case GT_CNS_VEC: -#endif // FEATURE_SIMD -#if defined(FEATURE_MASKED_HW_INTRINSICS) - case GT_CNS_MSK: -#endif // FEATURE_MASKED_HW_INTRINSICS - case GT_MEMORYBARRIER: - case GT_JMP: - case GT_JCC: - case GT_SETCC: - case GT_NO_OP: - case GT_START_NONGC: - case GT_START_PREEMPTGC: - case GT_PROF_HOOK: - case GT_PHI_ARG: - case GT_JMPTABLE: - case GT_PHYSREG: - case GT_IL_OFFSET: - case GT_RECORD_ASYNC_RESUME: - case GT_NOP: - case GT_SWIFT_ERROR: - case GT_GCPOLL: - case GT_WASM_THROW_REF: - case GT_WASM_JEXCEPT: - return false; + return false; + } - // Standard unary operators - case GT_STORE_LCL_VAR: - case GT_STORE_LCL_FLD: - case GT_NOT: - case GT_NEG: - case GT_COPY: - case GT_RELOAD: - case GT_ARR_LENGTH: - case GT_MDARR_LENGTH: - case GT_MDARR_LOWER_BOUND: - case GT_CAST: - case GT_BITCAST: - case GT_CKFINITE: - case GT_LCLHEAP: - case GT_IND: - case GT_BLK: - case GT_BOX: - case GT_ALLOCOBJ: - case GT_RUNTIMELOOKUP: - case GT_ARR_ADDR: - case GT_INIT_VAL: - case GT_JTRUE: - case GT_SWITCH: - case GT_NULLCHECK: - case GT_PUTARG_REG: - case GT_PUTARG_STK: - case GT_RETURNTRAP: - case GT_RETURN: - case GT_RETFILT: - case GT_RETURN_SUSPEND: - case GT_PATCHPOINT_FORCED: - case GT_NONLOCAL_JMP: - case GT_BSWAP: - case GT_BSWAP16: - case GT_KEEPALIVE: - case GT_INC_SATURATE: - if (operand == this->AsUnOp()->gtOp1) + if (OperIsSpecial()) + { + switch (OperGet()) + { +#if defined(FEATURE_HW_INTRINSICS) + case GT_HWINTRINSIC: + for (GenTree** opUse : AsMultiOp()->UseEdges()) + { + if (*opUse == operand) + { + *pUse = opUse; + return true; + } + } + return false; +#endif // FEATURE_HW_INTRINSICS + + // Special nodes + case GT_PHI: { - *pUse = &this->AsUnOp()->gtOp1; - return true; + for (GenTreePhi::Use& phiUse : AsPhi()->Uses()) + { + if (phiUse.GetNode() == operand) + { + *pUse = &phiUse.NodeRef(); + return true; + } + } + return false; } - return false; -#if defined(FEATURE_HW_INTRINSICS) - case GT_HWINTRINSIC: - for (GenTree** opUse : this->AsMultiOp()->UseEdges()) + case GT_FIELD_LIST: { - if (*opUse == operand) + for (GenTreeFieldList::Use& fieldUse : AsFieldList()->Uses()) { - *pUse = opUse; - return true; + if (fieldUse.GetNode() == operand) + { + *pUse = &fieldUse.NodeRef(); + return true; + } } + return false; } - return false; -#endif // FEATURE_HW_INTRINSICS - // Special nodes - case GT_PHI: - for (GenTreePhi::Use& phiUse : AsPhi()->Uses()) + case GT_CMPXCHG: { - if (phiUse.GetNode() == operand) + GenTreeCmpXchg* const cmpXchg = AsCmpXchg(); + + if (operand == cmpXchg->Addr()) { - *pUse = &phiUse.NodeRef(); + *pUse = &cmpXchg->Addr(); return true; } - } - return false; - case GT_FIELD_LIST: - for (GenTreeFieldList::Use& fieldUse : AsFieldList()->Uses()) - { - if (fieldUse.GetNode() == operand) + if (operand == cmpXchg->Data()) { - *pUse = &fieldUse.NodeRef(); + *pUse = &cmpXchg->Data(); return true; } - } - return false; - case GT_CMPXCHG: - { - GenTreeCmpXchg* const cmpXchg = this->AsCmpXchg(); - if (operand == cmpXchg->Addr()) - { - *pUse = &cmpXchg->Addr(); - return true; - } - if (operand == cmpXchg->Data()) - { - *pUse = &cmpXchg->Data(); - return true; - } - if (operand == cmpXchg->Comparand()) - { - *pUse = &cmpXchg->Comparand(); - return true; + if (operand == cmpXchg->Comparand()) + { + *pUse = &cmpXchg->Comparand(); + return true; + } + return false; } - return false; - } - case GT_ARR_ELEM: - { - GenTreeArrElem* const arrElem = this->AsArrElem(); - if (operand == arrElem->gtArrObj) - { - *pUse = &arrElem->gtArrObj; - return true; - } - for (unsigned i = 0; i < arrElem->gtArrRank; i++) + case GT_ARR_ELEM: { - if (operand == arrElem->gtArrInds[i]) + GenTreeArrElem* const arrElem = AsArrElem(); + + if (operand == arrElem->gtArrObj) { - *pUse = &arrElem->gtArrInds[i]; + *pUse = &arrElem->gtArrObj; return true; } - } - return false; - } - case GT_CALL: - { - GenTreeCall* const call = this->AsCall(); - if (operand == call->gtControlExpr) - { - *pUse = &call->gtControlExpr; - return true; + for (unsigned i = 0; i < arrElem->gtArrRank; i++) + { + if (operand == arrElem->gtArrInds[i]) + { + *pUse = &arrElem->gtArrInds[i]; + return true; + } + } + return false; } - for (CallArg& arg : call->gtArgs.Args()) + + case GT_CALL: { - if (arg.GetEarlyNode() == operand) + GenTreeCall* const call = AsCall(); + + if (operand == call->gtControlExpr) { - *pUse = &arg.EarlyNodeRef(); + *pUse = &call->gtControlExpr; return true; } - if (arg.GetLateNode() == operand) + + for (CallArg& arg : call->gtArgs.Args()) { - *pUse = &arg.LateNodeRef(); - return true; + if (arg.GetEarlyNode() == operand) + { + *pUse = &arg.EarlyNodeRef(); + return true; + } + if (arg.GetLateNode() == operand) + { + *pUse = &arg.LateNodeRef(); + return true; + } } + return false; } - return false; - } + #ifdef TARGET_ARM64 - case GT_SELECT_NEG: - case GT_SELECT_INV: - case GT_SELECT_INC: + case GT_SELECT_NEG: + case GT_SELECT_INV: + case GT_SELECT_INC: #endif - case GT_SELECT: - { - GenTreeConditional* const conditional = this->AsConditional(); - if (operand == conditional->gtCond) - { - *pUse = &conditional->gtCond; - return true; - } - if (operand == conditional->gtOp1) + case GT_SELECT: { - *pUse = &conditional->gtOp1; - return true; + GenTreeConditional* const conditional = AsConditional(); + + if (operand == conditional->gtCond) + { + *pUse = &conditional->gtCond; + return true; + } + + if (operand == conditional->gtOp1) + { + *pUse = &conditional->gtOp1; + return true; + } + + if (operand == conditional->gtOp2) + { + *pUse = &conditional->gtOp2; + return true; + } + return false; } - if (operand == conditional->gtOp2) + + default: { - *pUse = &conditional->gtOp2; - return true; + assert(!"unhandled special oper"); + return false; } - return false; } - - // Binary nodes - default: - assert(this->OperIsBinary()); - return TryGetUseBinOp(operand, pUse); } -} -bool GenTree::TryGetUseBinOp(GenTree* operand, GenTree*** pUse) -{ - assert(operand != nullptr); - assert(pUse != nullptr); - assert(this->OperIsBinary()); + assert(OperIsUnary() || OperIsBinary()); + GenTreeOp* const opNode = AsOp(); - GenTreeOp* const binOp = this->AsOp(); - if (operand == binOp->gtOp1) + if (operand == opNode->gtOp1) { - *pUse = &binOp->gtOp1; + *pUse = &opNode->gtOp1; return true; } - if (operand == binOp->gtOp2) + + if (OperIsBinary() && (operand == opNode->gtOp2)) { - *pUse = &binOp->gtOp2; + *pUse = &opNode->gtOp2; return true; } return false; diff --git a/src/coreclr/jit/gentree.h b/src/coreclr/jit/gentree.h index c9c965960083ea..85b8693ce996a3 100644 --- a/src/coreclr/jit/gentree.h +++ b/src/coreclr/jit/gentree.h @@ -1623,6 +1623,11 @@ struct GenTree return ((OperKind(gtOper) & GTK_KINDMASK) == GTK_SPECIAL); } + bool OperIsSpecial() const + { + return OperIsSpecial(gtOper); + } + bool OperIsSimple() const { return OperIsSimple(gtOper); @@ -1987,9 +1992,6 @@ struct GenTree return TryGetUse(operand, &unusedUse); } -private: - bool TryGetUseBinOp(GenTree* operand, GenTree*** pUse); - public: GenTree* gtGetParent(GenTree*** pUse); diff --git a/src/coreclr/jit/gtlist.h b/src/coreclr/jit/gtlist.h index be5cb9fcf9ff03..b293df525ed695 100644 --- a/src/coreclr/jit/gtlist.h +++ b/src/coreclr/jit/gtlist.h @@ -261,17 +261,17 @@ GTNODE(CCMP , GenTreeCCMP ,0,0,GTK_BINOP|GTK_NOVALUE|DBK_NOTH #ifdef TARGET_ARM64 // Maps to arm64 csinc/cinc instruction. Computes result = condition ? op1 : op2 + 1. // If op2 is null, computes result = condition ? op1 + 1 : op1. -GTNODE(SELECT_INC , GenTreeOp ,0,0,GTK_BINOP|DBK_NOTHIR) +GTNODE(SELECT_INC , GenTreeConditional ,0,0,GTK_SPECIAL|DBK_NOTHIR) // Variant of SELECT_INC that reuses flags computed by a previous node with the specified condition. GTNODE(SELECT_INCCC , GenTreeOpCC ,0,0,GTK_BINOP|DBK_NOTHIR) // Maps to arm64 csinv/cinv instruction. Computes result = condition ? op1 : ~op2. // If op2 is null, computes result = condition ? ~op1 : op1. -GTNODE(SELECT_INV , GenTreeOp ,0,0,GTK_BINOP|DBK_NOTHIR) +GTNODE(SELECT_INV , GenTreeConditional ,0,0,GTK_SPECIAL|DBK_NOTHIR) // Variant of SELECT_INV that reuses flags computed by a previous node with the specified condition. GTNODE(SELECT_INVCC , GenTreeOpCC ,0,0,GTK_BINOP|DBK_NOTHIR) // Maps to arm64 csneg/cneg instruction.. Computes result = condition ? op1 : -op2. // If op2 is null, computes result = condition ? -op1 : op1. -GTNODE(SELECT_NEG , GenTreeOp ,0,0,GTK_BINOP|DBK_NOTHIR) +GTNODE(SELECT_NEG , GenTreeConditional ,0,0,GTK_SPECIAL|DBK_NOTHIR) // Variant of SELECT_NEG that reuses flags computed by a previous node with the specified condition. GTNODE(SELECT_NEGCC , GenTreeOpCC ,0,0,GTK_BINOP|DBK_NOTHIR) #endif From c7286d56b0bb36cfa319adfa4aba6c546f404ef2 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:54:51 -0700 Subject: [PATCH 058/115] Delete FEATURE_AUTO_TRACE (#127631) Removes the `FEATURE_AUTO_TRACE` / AutoTrace feature entirely. AutoTrace was a testing infrastructure for automated stress-testing of the Diagnostic Server via EventPipe, controlled via `DOTNET_AutoTrace_N_Tracers` and `DOTNET_AutoTrace_Command` environment variables. It was never enabled by default (the cmake variable defaulted to `0`). --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> Co-authored-by: Jan Kotas --- ...omated-stress-testing-diagnostic-server.md | 23 ---- src/coreclr/clrdefinitions.cmake | 3 - src/coreclr/clrfeatures.cmake | 4 - src/coreclr/inc/clrconfigvalues.h | 5 - .../nativeaot/Runtime/eventpipe/ds-rt-aot.h | 52 ------- src/coreclr/vm/CMakeLists.txt | 6 - src/coreclr/vm/autotrace.cpp | 129 ------------------ src/coreclr/vm/autotrace.h | 15 -- .../vm/eventing/eventpipe/ds-rt-coreclr.h | 72 ---------- src/mono/mono/eventpipe/ds-rt-mono.h | 36 ----- src/native/eventpipe/ds-rt.h | 20 --- src/native/eventpipe/ds-server.c | 7 - 12 files changed, 372 deletions(-) delete mode 100644 docs/workflow/testing/coreclr/automated-stress-testing-diagnostic-server.md delete mode 100644 src/coreclr/vm/autotrace.cpp delete mode 100644 src/coreclr/vm/autotrace.h diff --git a/docs/workflow/testing/coreclr/automated-stress-testing-diagnostic-server.md b/docs/workflow/testing/coreclr/automated-stress-testing-diagnostic-server.md deleted file mode 100644 index 002c0703689ded..00000000000000 --- a/docs/workflow/testing/coreclr/automated-stress-testing-diagnostic-server.md +++ /dev/null @@ -1,23 +0,0 @@ -# AutoTrace: - -> see: `src/vm/autotrace.h|cpp` for the code - -AutoTrace is used to run automated testing of the Diagnostic Server based tracing and specifically -EventPipe. The feature itself is enabled via the feature flag `FEATURE_AUTO_TRACE` in [clrfeatures.cmake](../../../../src/coreclr/clrfeatures.cmake) - -## Mechanism: - -AutoTrace injects a waitable event into the startup path of the runtime and waits on that event until -some number of Diagnostics IPC (see: Diagnostics IPC in the dotnet/diagnostics repo) connections have occurred. -The runtime then creates some number of processes using a supplied path that typically are Diagnostics IPC based tracers. -Once all the tracers have connected to the server, the event will be signaled and execution will continue as normal. - -## Use: - -Two environment variables dictate behavior: -- `DOTNET_AutoTrace_N_Tracers`: The number of tracers to create. Should be a number in `[0,64]` where `0` will bypass the wait for attach. -- `DOTNET_AutoTrace_Command`: The path to the executable to be invoked. Typically this will be a `run.sh|cmd` script. - -> (NB: you should `cd` into the directory you intend to execute `DOTNET_AutoTrace_Command` from as the first line of the script.) - -Once turned on, AutoTrace will run the specified command `DOTNET_AutoTrace_N_Tracers` times. diff --git a/src/coreclr/clrdefinitions.cmake b/src/coreclr/clrdefinitions.cmake index 60b79850f53ffd..543ef485f0d525 100644 --- a/src/coreclr/clrdefinitions.cmake +++ b/src/coreclr/clrdefinitions.cmake @@ -76,9 +76,6 @@ if(FEATURE_DBGIPC) add_definitions(-DFEATURE_DBGIPC_TRANSPORT_VM) endif(FEATURE_DBGIPC) add_definitions(-DFEATURE_DEFAULT_INTERFACES) -if(FEATURE_AUTO_TRACE) - add_compile_definitions(FEATURE_AUTO_TRACE) -endif(FEATURE_AUTO_TRACE) if(FEATURE_EVENT_TRACE) add_compile_definitions(FEATURE_EVENT_TRACE) add_definitions(-DFEATURE_PERFTRACING) diff --git a/src/coreclr/clrfeatures.cmake b/src/coreclr/clrfeatures.cmake index f5c84aaaad2947..c03aace596ffb3 100644 --- a/src/coreclr/clrfeatures.cmake +++ b/src/coreclr/clrfeatures.cmake @@ -84,10 +84,6 @@ if(NOT DEFINED FEATURE_STANDALONE_GC) set(FEATURE_STANDALONE_GC 1) endif(NOT DEFINED FEATURE_STANDALONE_GC) -if(NOT DEFINED FEATURE_AUTO_TRACE) - set(FEATURE_AUTO_TRACE 0) -endif(NOT DEFINED FEATURE_AUTO_TRACE) - if(NOT DEFINED FEATURE_SINGLE_FILE_DIAGNOSTICS) set(FEATURE_SINGLE_FILE_DIAGNOSTICS 1) endif(NOT DEFINED FEATURE_SINGLE_FILE_DIAGNOSTICS) diff --git a/src/coreclr/inc/clrconfigvalues.h b/src/coreclr/inc/clrconfigvalues.h index 7767eb65a9b9c4..c9dd7c485c99c0 100644 --- a/src/coreclr/inc/clrconfigvalues.h +++ b/src/coreclr/inc/clrconfigvalues.h @@ -612,11 +612,6 @@ RETAIL_CONFIG_DWORD_INFO(INTERNAL_EventPipeOutputStreaming, W("EventPipeOutputSt RETAIL_CONFIG_DWORD_INFO(INTERNAL_EventPipeEnableStackwalk, W("EventPipeEnableStackwalk"), 1, "Set to 0 to disable collecting stacks for EventPipe events.") RETAIL_CONFIG_DWORD_INFO(INTERNAL_EventPipeThreadSamplingRate, W("EventPipeThreadSamplingRate"), 0, "Desired sample interval in milliseconds for EventPipe thread time sampling profiler. 0 means use the default.") -#ifdef FEATURE_AUTO_TRACE -RETAIL_CONFIG_DWORD_INFO_EX(INTERNAL_AutoTrace_N_Tracers, W("AutoTrace_N_Tracers"), 0, "", CLRConfig::LookupOptions::ParseIntegerAsBase10) -RETAIL_CONFIG_STRING_INFO(INTERNAL_AutoTrace_Command, W("AutoTrace_Command"), "") -#endif // FEATURE_AUTO_TRACE - // // Generational Aware Analysis // diff --git a/src/coreclr/nativeaot/Runtime/eventpipe/ds-rt-aot.h b/src/coreclr/nativeaot/Runtime/eventpipe/ds-rt-aot.h index 5a7ecaa7b2889d..12afb96e4165ec 100644 --- a/src/coreclr/nativeaot/Runtime/eventpipe/ds-rt-aot.h +++ b/src/coreclr/nativeaot/Runtime/eventpipe/ds-rt-aot.h @@ -67,58 +67,6 @@ #undef DS_EXIT_BLOCKING_PAL_SECTION #define DS_EXIT_BLOCKING_PAL_SECTION -/* -* AutoTrace. -*/ - -#ifdef FEATURE_AUTO_TRACE -#include "autotrace.h" -#endif - -static -void -ds_rt_auto_trace_init (void) -{ - STATIC_CONTRACT_NOTHROW; - -#ifdef FEATURE_AUTO_TRACE - auto_trace_init (); -#endif -} - -static -void -ds_rt_auto_trace_launch (void) -{ - STATIC_CONTRACT_NOTHROW; - -#ifdef FEATURE_AUTO_TRACE - auto_trace_launch (); -#endif -} - -static -void -ds_rt_auto_trace_signal (void) -{ - STATIC_CONTRACT_NOTHROW; - -#ifdef FEATURE_AUTO_TRACE - auto_trace_signal (); -#endif -} - -static -void -ds_rt_auto_trace_wait (void) -{ - STATIC_CONTRACT_NOTHROW; - -#ifdef FEATURE_AUTO_TRACE - auto_trace_wait (); -#endif -} - /* * DiagnosticsConfiguration. */ diff --git a/src/coreclr/vm/CMakeLists.txt b/src/coreclr/vm/CMakeLists.txt index a2dbcb4c89c429..aca63f44539e65 100644 --- a/src/coreclr/vm/CMakeLists.txt +++ b/src/coreclr/vm/CMakeLists.txt @@ -18,10 +18,6 @@ if(CLR_CMAKE_TARGET_ANDROID) add_definitions(-DFEATURE_EMULATED_TLS) endif(CLR_CMAKE_TARGET_ANDROID) -if(FEATURE_AUTO_TRACE) - add_definitions(-DFEATURE_AUTO_TRACE) -endif(FEATURE_AUTO_TRACE) - foreach (Config DEBUG CHECKED) add_compile_definitions($<$:WRITE_BARRIER_CHECK>) endforeach (Config) @@ -313,7 +309,6 @@ set(VM_SOURCES_WKS corelib.cpp # true customattribute.cpp custommarshalerinfo.cpp - autotrace.cpp divmodint.cpp dllimport.cpp dllimportcallback.cpp @@ -423,7 +418,6 @@ set(VM_HEADERS_WKS comutilnative.h customattribute.h custommarshalerinfo.h - autotrace.h diagnosticserveradapter.h divmodint.h dllimport.h diff --git a/src/coreclr/vm/autotrace.cpp b/src/coreclr/vm/autotrace.cpp deleted file mode 100644 index c41d55537f5f70..00000000000000 --- a/src/coreclr/vm/autotrace.cpp +++ /dev/null @@ -1,129 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/** - * - * AutoTrace: This infrastructure is used to run automated testing of Diagnostic Server based tracing via - * EventPipe. The feature itself is enabled via the feature flag FEATURE_AUTO_TRACE. - * - * Two environment variables dictate behavior: - * - DOTNET_AutoTrace_N_Tracers: a number in [0,64] where 0 will disable the feature - * - DOTNET_AutoTrace_Command: The path to an executable to be invoked. Typically this will be a "run.sh|cmd". - * > (NB: you should `cd` into the directory you intend to execute `DOTNET_AutoTrace_Command` from as the first line of the script.) - * - * Once turned on, AutoTrace will run the specified command `DOTNET_AutoTrace_N_Tracers` times. There is an event that will pause execution - * of the runtime until all the tracers have attached. Once all the tracers are attached, execution will continue normally. - * - * This logic is easily modified to accommodate testing other mechanisms related to the Diagnostic Server. - * - */ - -#include "common.h" // Required for pre-compiled header - -#ifdef FEATURE_AUTO_TRACE -#ifdef TARGET_UNIX -#include "pal.h" -#endif // TARGET_UNIX - -HANDLE auto_trace_event; -static size_t g_n_tracers = 1; -static WCHAR* command = nullptr; - -void auto_trace_init() -{ - if (CLRConfig::IsConfigEnabled(CLRConfig::INTERNAL_AutoTrace_N_Tracers)) - { - g_n_tracers = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_AutoTrace_N_Tracers); - } - - // Get the command to run auto-trace. Note that the `-p ` option - // will be automatically added for you - LPWSTR commandTextValue = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_AutoTrace_Command); - if (commandTextValue != NULL) - { - // Create a command line with the format: "%s -p %d" - const WCHAR flagFormat[] = W(" -p "); - DWORD currentProcessId = GetCurrentProcessId(); - size_t bufferLen = 8192; - size_t written = 0; - command = new WCHAR[bufferLen]; - - // Copy in the command - %s - wcscpy_s(command, bufferLen, commandTextValue); - written += u16_strlen(commandTextValue); - - // Append " -p " - wcscat_s(command, bufferLen - written, flagFormat); - written += ARRAY_SIZE(flagFormat) - 1; - - // Append the process ID - FormatInteger(command + written, bufferLen - written, "%d", currentProcessId); - } - else - { - // we don't have anything to run, just set - // n tracers to 0... - g_n_tracers = 0; - } - - auto_trace_event = CreateEventW( - /* lpEventAttributes = */ NULL, - /* bManualReset = */ FALSE, - /* bInitialState = */ FALSE, - /* lpName = */ nullptr - ); -} - -void auto_trace_launch_internal() -{ - DWORD currentProcessId = GetCurrentProcessId(); - STARTUPINFO si; - ZeroMemory(&si, sizeof(si)); - si.cb = sizeof(STARTUPINFO); -#ifndef TARGET_UNIX - si.dwFlags = STARTF_USESHOWWINDOW; - si.wShowWindow = SW_HIDE; -#endif - - PROCESS_INFORMATION result; - - BOOL code = CreateProcessW( - /* lpApplicationName = */ nullptr, - /* lpCommandLine = */ command, - /* lpCommandLine = */ nullptr, - /* lpThreadAttributes = */ nullptr, - /* bInheritHandles = */ false, - /* dwCreationFlags = */ CREATE_NEW_CONSOLE, - /* lpEnvironment = */ nullptr, - /* lpCurrentDirectory = */ nullptr, - /* lpStartupInfo = */ &si, - /* lpProcessInformation = */ &result - ); -} - -void auto_trace_launch() -{ - for (size_t i = 0; i < g_n_tracers; ++i) - { - auto_trace_launch_internal(); - } - delete[] command; -} - -void auto_trace_wait() -{ - if (g_n_tracers > 0) - WaitForSingleObject(auto_trace_event, INFINITE); -} - -void auto_trace_signal() -{ - #ifdef SetEvent - #undef SetEvent - #endif - static size_t nCalls = 0; - if (++nCalls == g_n_tracers) - SetEvent(auto_trace_event); -} - -#endif // FEATURE_AUTO_TRACE diff --git a/src/coreclr/vm/autotrace.h b/src/coreclr/vm/autotrace.h deleted file mode 100644 index 086817419d0b8e..00000000000000 --- a/src/coreclr/vm/autotrace.h +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#ifndef __AUTO_TRACE_H__ -#define __AUTO_TRACE_H__ -#ifdef FEATURE_AUTO_TRACE - -void auto_trace_init(); -void auto_trace_launch(); -void auto_trace_launch_internal(); -void auto_trace_wait(); -void auto_trace_signal(); - -#endif // FEATURE_AUTO_TRACE -#endif // __AUTO_TRACE_H__ diff --git a/src/coreclr/vm/eventing/eventpipe/ds-rt-coreclr.h b/src/coreclr/vm/eventing/eventpipe/ds-rt-coreclr.h index 2d49b8f78e35ff..c2d4ab066c76a9 100644 --- a/src/coreclr/vm/eventing/eventpipe/ds-rt-coreclr.h +++ b/src/coreclr/vm/eventing/eventpipe/ds-rt-coreclr.h @@ -70,78 +70,6 @@ #undef DS_EXIT_BLOCKING_PAL_SECTION #define DS_EXIT_BLOCKING_PAL_SECTION -/* -* AutoTrace. -*/ - -#ifdef FEATURE_AUTO_TRACE -#include "autotrace.h" -#endif - -static -void -ds_rt_auto_trace_init (void) -{ - STATIC_CONTRACT_NOTHROW; - -#ifdef FEATURE_AUTO_TRACE - EX_TRY - { - auto_trace_init (); - } - EX_CATCH {} - EX_END_CATCH -#endif -} - -static -void -ds_rt_auto_trace_launch (void) -{ - STATIC_CONTRACT_NOTHROW; - -#ifdef FEATURE_AUTO_TRACE - EX_TRY - { - auto_trace_launch (); - } - EX_CATCH {} - EX_END_CATCH -#endif -} - -static -void -ds_rt_auto_trace_signal (void) -{ - STATIC_CONTRACT_NOTHROW; - -#ifdef FEATURE_AUTO_TRACE - EX_TRY - { - auto_trace_signal (); - } - EX_CATCH {} - EX_END_CATCH -#endif -} - -static -void -ds_rt_auto_trace_wait (void) -{ - STATIC_CONTRACT_NOTHROW; - -#ifdef FEATURE_AUTO_TRACE - EX_TRY - { - auto_trace_wait (); - } - EX_CATCH {} - EX_END_CATCH -#endif -} - /* * DiagnosticsConfiguration. */ diff --git a/src/mono/mono/eventpipe/ds-rt-mono.h b/src/mono/mono/eventpipe/ds-rt-mono.h index 66e39b7078c4a4..60ffd0592c4efb 100644 --- a/src/mono/mono/eventpipe/ds-rt-mono.h +++ b/src/mono/mono/eventpipe/ds-rt-mono.h @@ -73,42 +73,6 @@ ds_rt_mono_transport_get_default_name ( const ep_char8_t *group_id, const ep_char8_t *suffix); -/* -* AutoTrace. -*/ - -static -inline -void -ds_rt_auto_trace_init (void) -{ - // TODO: Implement. -} - -static -inline -void -ds_rt_auto_trace_launch (void) -{ - // TODO: Implement. -} - -static -inline -void -ds_rt_auto_trace_signal (void) -{ - // TODO: Implement. -} - -static -inline -void -ds_rt_auto_trace_wait (void) -{ - // TODO: Implement. -} - /* * DiagnosticsConfiguration. */ diff --git a/src/native/eventpipe/ds-rt.h b/src/native/eventpipe/ds-rt.h index a5f9fce15611c6..985aba5bd33f58 100644 --- a/src/native/eventpipe/ds-rt.h +++ b/src/native/eventpipe/ds-rt.h @@ -27,26 +27,6 @@ extern const ep_char8_t *_ds_portable_rid_info; -/* -* AutoTrace. -*/ - -static -void -ds_rt_auto_trace_init (void); - -static -void -ds_rt_auto_trace_launch (void); - -static -void -ds_rt_auto_trace_signal (void); - -static -void -ds_rt_auto_trace_wait (void); - /* * DiagnosticsConfiguration. */ diff --git a/src/native/eventpipe/ds-server.c b/src/native/eventpipe/ds-server.c index a89f757970167c..cfd4d9c7c1f173 100644 --- a/src/native/eventpipe/ds-server.c +++ b/src/native/eventpipe/ds-server.c @@ -119,8 +119,6 @@ static size_t server_loop_tick (void* data) { if (!stream) return 0; // continue - ds_rt_auto_trace_signal (); - DiagnosticsIpcMessage message; if (!ds_ipc_message_init (&message)) return 0; // continue @@ -223,9 +221,6 @@ ds_server_init (void) } if (ds_ipc_stream_factory_has_active_ports ()) { - ds_rt_auto_trace_init (); - ds_rt_auto_trace_launch (); - #ifndef PERFTRACING_DISABLE_THREADS ep_rt_thread_id_t thread_id = ep_rt_uint64_t_to_thread_id_t (0); @@ -234,8 +229,6 @@ ds_server_init (void) ds_ipc_stream_factory_close_ports (NULL); DS_LOG_ERROR_1 ("Failed to create diagnostic server thread (%d).", ep_rt_get_last_error ()); ep_raise_error (); - } else { - ds_rt_auto_trace_wait (); } #else ep_rt_queue_job ((void *)server_loop_tick, NULL); From 676440f23c6a73d88a12654c2bf0d931530cca68 Mon Sep 17 00:00:00 2001 From: Milos Kotlar Date: Fri, 1 May 2026 08:00:17 +0200 Subject: [PATCH 059/115] [clr-ios] Advance past owning InterpreterFrame at `StackFrameIterator::Init` call sites (#127560) Follow-up to #126953 ## Description When a thread is suspended inside the interpreter, `InterpreterFrame::SetContextToInterpMethodContextFrame` rewrites its CONTEXT so that the IP points at the executing interpreted bytecode and the first-arg register holds the owning `InterpreterFrame*`. Debugger code paths that walk such a thread forwards this CONTEXT to `StackFrameIterator::Init`. Currently, `Init` fails with `_ASSERTE(!m_crawl.codeInfo.IsInterpretedCode())`. The assert checks a property of the IP, but the IP points at interpreter code in the cases above, so the assert fires on every such walk. ## Approach `StackFrameIterator::Init` resolves the start frame itself when the supplied CONTEXT references interpreted code, so debugger DAC callers no longer need to recompute it. cc @noahfalk @janvorli --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/daccess/dacdbiimpl.cpp | 2 +- src/coreclr/vm/stackwalk.cpp | 26 +++++++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/coreclr/debug/daccess/dacdbiimpl.cpp b/src/coreclr/debug/daccess/dacdbiimpl.cpp index bd31eee7c233bf..0eb96e0af27b9b 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.cpp +++ b/src/coreclr/debug/daccess/dacdbiimpl.cpp @@ -5657,7 +5657,7 @@ BOOL DacDbiInterfaceImpl::IsThreadAtGCSafePlace(VMPTR_Thread vmThread) ULONG32 flags = (QUICKUNWIND | HANDLESKIPPEDFRAMES | DISABLE_MISSING_FRAME_DETECTION); StackFrameIterator iter; - iter.Init(pThread, pThread->GetFrame(), &rd, flags); + iter.Init(pThread, NULL, &rd, flags); CrawlFrame * pCF = &(iter.m_crawl); if (pCF->IsFrameless() && pCF->IsActiveFunc()) diff --git a/src/coreclr/vm/stackwalk.cpp b/src/coreclr/vm/stackwalk.cpp index 00f2123f005fe0..f8e344552c303e 100644 --- a/src/coreclr/vm/stackwalk.cpp +++ b/src/coreclr/vm/stackwalk.cpp @@ -1088,7 +1088,25 @@ BOOL StackFrameIterator::Init(Thread * pThread, // process the REGDISPLAY and stop at the first frame ProcessIp(GetControlPC(m_crawl.pRD)); #ifdef FEATURE_INTERPRETER - _ASSERTE(!m_crawl.codeInfo.IsInterpretedCode()); + if (m_crawl.codeInfo.IsInterpretedCode()) + { + // CONTEXT is in interpreted code where the first-arg register holds the owning InterpreterFrame. + // Skip past it so we don't re-enter its frame chain. + PTR_InterpreterFrame pOwning = + dac_cast((TADDR)GetFirstArgReg(m_crawl.pRD->pCurrentContext)); + _ASSERTE(pOwning != NULL); + _ASSERTE(pOwning->GetFrameIdentifier() == FrameIdentifier::InterpreterFrame); + + if (pFrame == NULL) + { + m_crawl.pFrame = pOwning->PtrNextFrame(); + } + else + { + // Explicit pFrame must already be past the owner (callee Frames have lower addresses than their callers). + _ASSERTE(dac_cast(m_crawl.pFrame) > dac_cast(pOwning)); + } + } #endif // FEATURE_INTERPRETER if (m_crawl.isFrameless && !!(m_crawl.pRD->pCurrentContext->ContextFlags & CONTEXT_EXCEPTION_ACTIVE)) { @@ -1166,10 +1184,8 @@ BOOL StackFrameIterator::ResetRegDisp(PREGDISPLAY pRegDisp, #ifdef FEATURE_INTERPRETER if (m_crawl.codeInfo.IsInterpretedCode()) { - // The CONTEXT carries the owning InterpreterFrame in the first-arg register - // (set by InterpreterFrame::SetContextToInterpMethodContextFrame). Advance - // m_crawl.pFrame past it so the iterator does not re-enter the same - // InterpMethodContextFrame chain via the explicit frame link. + // CONTEXT is in interpreted code where the first-arg register holds the owning InterpreterFrame. + // Skip past it so we don't re-enter its frame chain. PTR_InterpreterFrame pOwningInterpFrame = dac_cast((TADDR)GetFirstArgReg(m_crawl.pRD->pCurrentContext)); _ASSERTE(pOwningInterpFrame != NULL); From 6320fe0dfd57b506eef52b6501c1ab54d0e53ce9 Mon Sep 17 00:00:00 2001 From: Rachel Jarvi Date: Thu, 30 Apr 2026 23:24:13 -0700 Subject: [PATCH 060/115] [cDAC] turning a few heap into full dumps (#127491) I was a bit too aggressive turning full into heap dumps, and this is showing up in the new DacDbi dump tests. Switching the CCW, RCW, and BasicThreads dumps to full dumps. --- .../cdac/tests/DumpTests/CCWDumpTests.cs | 1 + .../cdac/tests/DumpTests/ClrMdDumpHost.cs | 67 ++++++++++--------- .../DacDbi/DacDbiAppDomainDumpTests.cs | 1 + .../DumpTests/DacDbi/DacDbiCCWDumpTests.cs | 1 + .../DacDbi/DacDbiDebuggerDumpTests.cs | 1 + .../DumpTests/DacDbi/DacDbiGCDumpTests.cs | 1 + .../DumpTests/DacDbi/DacDbiObjectDumpTests.cs | 1 + .../DumpTests/DacDbi/DacDbiRCWDumpTests.cs | 1 + .../DumpTests/DacDbi/DacDbiThreadDumpTests.cs | 1 + .../BasicThreads/BasicThreads.csproj | 3 + .../tests/DumpTests/Debuggees/CCW/CCW.csproj | 2 +- .../tests/DumpTests/Debuggees/RCW/RCW.csproj | 2 +- .../cdac/tests/DumpTests/RCWDumpTests.cs | 1 + .../tests/DumpTests/RuntimeInfoDumpTests.cs | 1 + .../cdac/tests/DumpTests/ThreadDumpTests.cs | 1 + 15 files changed, 53 insertions(+), 32 deletions(-) diff --git a/src/native/managed/cdac/tests/DumpTests/CCWDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/CCWDumpTests.cs index 87092fefe01950..6e08d149c42639 100644 --- a/src/native/managed/cdac/tests/DumpTests/CCWDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/CCWDumpTests.cs @@ -17,6 +17,7 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; public class CCWDumpTests : DumpTestBase { protected override string DebuggeeName => "CCW"; + protected override string DumpType => "full"; /// /// Enumerates all strong GC handles from the dump, dereferences each one to get diff --git a/src/native/managed/cdac/tests/DumpTests/ClrMdDumpHost.cs b/src/native/managed/cdac/tests/DumpTests/ClrMdDumpHost.cs index c17344d52b0501..ffcef872209bfa 100644 --- a/src/native/managed/cdac/tests/DumpTests/ClrMdDumpHost.cs +++ b/src/native/managed/cdac/tests/DumpTests/ClrMdDumpHost.cs @@ -55,46 +55,53 @@ public static ClrMdDumpHost Open(string dumpPath, List additionalSymbolP /// public int ReadFromTarget(ulong address, Span buffer) { - int bytesRead = _dataTarget.DataReader.Read(address, buffer); - if (bytesRead == buffer.Length) - return 0; // success - - // If we couldn't read the full buffer, maybe it's in a PE image - ModuleInfo? info = GetModuleForAddress(address); - if (info is null || info.FileName is null) + try { - return -1; - } + int bytesRead = _dataTarget.DataReader.Read(address, buffer); + if (bytesRead == buffer.Length) + return 0; // success - string? foundFile = FindFileOnDisk(info.FileName); - if (foundFile is null) - { - return -1; - } - - using FileStream fs = File.OpenRead(foundFile); - using PEReader peReader = new PEReader(fs); + // If we couldn't read the full buffer, maybe it's in a PE image + ModuleInfo? info = GetModuleForAddress(address); + if (info is null || info.FileName is null) + { + return -1; + } - int filled = bytesRead; - ulong current = address + (ulong)bytesRead; - while (filled < buffer.Length) - { - PEMemoryBlock block = peReader.GetSectionData((int)(current - info.ImageBase)); - if (block.Length == 0) + string? foundFile = FindFileOnDisk(info.FileName); + if (foundFile is null) { return -1; } - int toCopy = Math.Min(block.Length, buffer.Length - filled); - unsafe + using FileStream fs = File.OpenRead(foundFile); + using PEReader peReader = new PEReader(fs); + + int filled = bytesRead; + ulong current = address + (ulong)bytesRead; + while (filled < buffer.Length) { - new ReadOnlySpan(block.Pointer, toCopy).CopyTo(buffer.Slice(filled)); + PEMemoryBlock block = peReader.GetSectionData((int)(current - info.ImageBase)); + if (block.Length == 0) + { + return -1; + } + + int toCopy = Math.Min(block.Length, buffer.Length - filled); + unsafe + { + new ReadOnlySpan(block.Pointer, toCopy).CopyTo(buffer.Slice(filled)); + } + filled += toCopy; + current += (ulong)toCopy; } - filled += toCopy; - current += (ulong)toCopy; - } - return 0; + return 0; + } + catch + { + return -1; + } } /// diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiAppDomainDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiAppDomainDumpTests.cs index aa5022e8935ef1..11c8aae2934c2e 100644 --- a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiAppDomainDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiAppDomainDumpTests.cs @@ -15,6 +15,7 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; public class DacDbiAppDomainDumpTests : DumpTestBase { protected override string DebuggeeName => "BasicThreads"; + protected override string DumpType => "full"; private DacDbiImpl CreateDacDbi() => new DacDbiImpl(Target, legacyObj: null); diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiCCWDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiCCWDumpTests.cs index e7bd9737845485..82ac243be2518f 100644 --- a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiCCWDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiCCWDumpTests.cs @@ -17,6 +17,7 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; public class DacDbiCCWDumpTests : DumpTestBase { protected override string DebuggeeName => "CCW"; + protected override string DumpType => "full"; private DacDbiImpl CreateDacDbi() => new DacDbiImpl(Target, legacyObj: null); private (TargetPointer Ccw, TargetPointer InterfacePointer) FindBuiltInComCcwWithInterface() diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiDebuggerDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiDebuggerDumpTests.cs index b4743abe6ca36c..c6f43f88fe2f34 100644 --- a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiDebuggerDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiDebuggerDumpTests.cs @@ -14,6 +14,7 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; public class DacDbiDebuggerDumpTests : DumpTestBase { protected override string DebuggeeName => "BasicThreads"; + protected override string DumpType => "full"; private DacDbiImpl CreateDacDbi() => new DacDbiImpl(Target, legacyObj: null); diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiGCDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiGCDumpTests.cs index 55de104c4c978c..31bf25e7053691 100644 --- a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiGCDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiGCDumpTests.cs @@ -14,6 +14,7 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; public class DacDbiGCDumpTests : DumpTestBase { protected override string DebuggeeName => "BasicThreads"; + protected override string DumpType => "full"; private DacDbiImpl CreateDacDbi() => new DacDbiImpl(Target, legacyObj: null); diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiObjectDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiObjectDumpTests.cs index 282387e49d3355..bfc99d47991f09 100644 --- a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiObjectDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiObjectDumpTests.cs @@ -13,6 +13,7 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; public class DacDbiObjectDumpTests : DumpTestBase { protected override string DebuggeeName => "BasicThreads"; + protected override string DumpType => "full"; private DacDbiImpl CreateDacDbi() => new DacDbiImpl(Target, legacyObj: null); diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRCWDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRCWDumpTests.cs index 90f0a7be73d2b6..c3958e5318f49c 100644 --- a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRCWDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRCWDumpTests.cs @@ -15,6 +15,7 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; public class DacDbiRCWDumpTests : DumpTestBase { protected override string DebuggeeName => "RCW"; + protected override string DumpType => "full"; private DacDbiImpl CreateDacDbi() => new DacDbiImpl(Target, legacyObj: null); [ConditionalTheory] diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiThreadDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiThreadDumpTests.cs index 7d4024d02c21da..433b7cf0385cfd 100644 --- a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiThreadDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiThreadDumpTests.cs @@ -17,6 +17,7 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; public class DacDbiThreadDumpTests : DumpTestBase { protected override string DebuggeeName => "BasicThreads"; + protected override string DumpType => "full"; private DacDbiImpl CreateDacDbi() => new DacDbiImpl(Target, legacyObj: null); diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/BasicThreads/BasicThreads.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/BasicThreads/BasicThreads.csproj index 35e3d8428b7cfc..b5bf84aa517d8a 100644 --- a/src/native/managed/cdac/tests/DumpTests/Debuggees/BasicThreads/BasicThreads.csproj +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/BasicThreads/BasicThreads.csproj @@ -1,2 +1,5 @@ + + Heap;Full + diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/CCW/CCW.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/CCW/CCW.csproj index af8d23ccbb4fb4..9d3e9f517d21fd 100644 --- a/src/native/managed/cdac/tests/DumpTests/Debuggees/CCW/CCW.csproj +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/CCW/CCW.csproj @@ -1,6 +1,6 @@ - Heap + Full true diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/RCW/RCW.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/RCW/RCW.csproj index 4ddb526b439e94..8fbeb470e46055 100644 --- a/src/native/managed/cdac/tests/DumpTests/Debuggees/RCW/RCW.csproj +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/RCW/RCW.csproj @@ -2,7 +2,7 @@ $(NoWarn);CA1416 - Heap + Full true diff --git a/src/native/managed/cdac/tests/DumpTests/RCWDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/RCWDumpTests.cs index 78d8dcda0848c2..c2d2e7a26d9592 100644 --- a/src/native/managed/cdac/tests/DumpTests/RCWDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/RCWDumpTests.cs @@ -16,6 +16,7 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; public class RCWDumpTests : DumpTestBase { protected override string DebuggeeName => "RCW"; + protected override string DumpType => "full"; /// /// Walks all strong GC handles and returns all RCW pointers found, diff --git a/src/native/managed/cdac/tests/DumpTests/RuntimeInfoDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/RuntimeInfoDumpTests.cs index 5faac6d4794b64..751ffa69c6b256 100644 --- a/src/native/managed/cdac/tests/DumpTests/RuntimeInfoDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/RuntimeInfoDumpTests.cs @@ -14,6 +14,7 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; public class RuntimeInfoDumpTests : DumpTestBase { protected override string DebuggeeName => "BasicThreads"; + protected override string DumpType => "heap"; [ConditionalTheory] [MemberData(nameof(TestConfigurations))] diff --git a/src/native/managed/cdac/tests/DumpTests/ThreadDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/ThreadDumpTests.cs index 8e24fafc4d12f6..6d29f6433e3d5a 100644 --- a/src/native/managed/cdac/tests/DumpTests/ThreadDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/ThreadDumpTests.cs @@ -16,6 +16,7 @@ public class ThreadDumpTests : DumpTestBase private const int SpawnedThreadCount = 5; protected override string DebuggeeName => "BasicThreads"; + protected override string DumpType => "heap"; [ConditionalTheory] [MemberData(nameof(TestConfigurations))] From bfaabae213cf02d343846d253b7149a03819680d Mon Sep 17 00:00:00 2001 From: Adeel Mujahid <3840695+am11@users.noreply.github.com> Date: Fri, 1 May 2026 09:40:56 +0300 Subject: [PATCH 061/115] Update DNNEVersion to 2.1.2 (#127549) Fixes https://github.com/dotnet/runtime/issues/127414. --- eng/Versions.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/Versions.props b/eng/Versions.props index d445186875aa0f..6b58c62b178aa3 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -134,7 +134,7 @@ 2.1.0 2.0.3 1.0.4-preview6.19326.1 - 2.1.1 + 2.1.2 17.11.48 17.11.48 17.11.48 From 93e28fa6b4c3681c01518a8c2d54b2290de39940 Mon Sep 17 00:00:00 2001 From: Andrew Au <3410332+cshung@users.noreply.github.com> Date: Fri, 1 May 2026 06:52:08 -0700 Subject: [PATCH 062/115] Disable Collect_Aggressive_LargePages for R2R/CrossGen2 (#127571) CrossGen2 reserves 6 GiB for regions which exceeds the 3 GiB GCHeapHardLimit configured in this test, causing GC heap initialization to fail. Fix #127541 --- src/tests/GC/API/GC/Collect_Aggressive_LargePages.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tests/GC/API/GC/Collect_Aggressive_LargePages.csproj b/src/tests/GC/API/GC/Collect_Aggressive_LargePages.csproj index b781f9f954ecab..be1c0d877cedff 100644 --- a/src/tests/GC/API/GC/Collect_Aggressive_LargePages.csproj +++ b/src/tests/GC/API/GC/Collect_Aggressive_LargePages.csproj @@ -5,6 +5,8 @@ true true + + false 0 From 8e7e7589051881332c387c46760773d1caed5712 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 07:01:44 -0700 Subject: [PATCH 063/115] Removes unused/unreachable WinRT ContentType plumbing from assembly binding (#127629) Removes unused/unreachable WinRT `ContentType` plumbing from the CoreCLR native assembly binder and `AssemblySpec`. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: elinor-fung <47805090+elinor-fung@users.noreply.github.com> --- src/coreclr/binder/assemblyname.cpp | 32 ++------------------ src/coreclr/binder/inc/assemblyidentity.hpp | 3 -- src/coreclr/binder/inc/assemblyname.hpp | 4 --- src/coreclr/binder/inc/assemblyname.inl | 19 ------------ src/coreclr/binder/inc/bindertypes.hpp | 12 +------- src/coreclr/binder/textualidentityparser.cpp | 19 ------------ src/coreclr/vm/assemblyspec.hpp | 19 ------------ src/coreclr/vm/coreassemblyspec.cpp | 24 --------------- 8 files changed, 4 insertions(+), 128 deletions(-) diff --git a/src/coreclr/binder/assemblyname.cpp b/src/coreclr/binder/assemblyname.cpp index 245c72c8d6a1fb..34dc9f423ddc80 100644 --- a/src/coreclr/binder/assemblyname.cpp +++ b/src/coreclr/binder/assemblyname.cpp @@ -113,12 +113,8 @@ namespace BINDER_SPACE SetIsRetargetable(TRUE); } - // Set ContentType - if (IsAfContentType_Default(dwRefOrDefFlags)) - { - SetContentType(AssemblyContentType_Default); - } - else + // Validate ContentType. Only the default content type is supported. + if (!IsAfContentType_Default(dwRefOrDefFlags)) { IF_FAIL_GO(FUSION_E_INVALID_NAME); } @@ -179,7 +175,6 @@ namespace BINDER_SPACE } m_kProcessorArchitecture = data.ProcessorArchitecture; - m_kContentType = data.ContentType; SetHave(flags); @@ -231,10 +226,6 @@ namespace BINDER_SPACE { dwUseIdentityFlags &= ~AssemblyIdentity::IDENTITY_FLAG_RETARGETABLE; } - if ((dwIncludeFlags & INCLUDE_CONTENT_TYPE) == 0) - { - dwUseIdentityFlags &= ~AssemblyIdentity::IDENTITY_FLAG_CONTENT_TYPE; - } if ((dwIncludeFlags & INCLUDE_PUBLIC_KEY_TOKEN) == 0) { dwUseIdentityFlags &= ~AssemblyIdentity::IDENTITY_FLAG_PUBLIC_KEY; @@ -296,13 +287,6 @@ namespace BINDER_SPACE dwHash = _rotl(dwHash, 4); } - if (AssemblyIdentity::Have(dwUseIdentityFlags, - AssemblyIdentity::IDENTITY_FLAG_CONTENT_TYPE)) - { - dwHash ^= static_cast(GetContentType()); - dwHash = _rotl(dwHash, 4); - } - return static_cast(dwHash); } @@ -311,13 +295,7 @@ namespace BINDER_SPACE { BOOL fEquals = FALSE; - if (GetContentType() == AssemblyContentType_WindowsRuntime) - { // Assembly is meaningless for WinRT, all assemblies form one joint type namespace - return (GetContentType() == pAssemblyName->GetContentType()); - } - - if (GetSimpleName().EqualsCaseInsensitive(pAssemblyName->GetSimpleName()) && - (GetContentType() == pAssemblyName->GetContentType())) + if (GetSimpleName().EqualsCaseInsensitive(pAssemblyName->GetSimpleName())) { fEquals = TRUE; @@ -368,10 +346,6 @@ namespace BINDER_SPACE { dwUseIdentityFlags &= ~AssemblyIdentity::IDENTITY_FLAG_RETARGETABLE; } - if ((dwIncludeFlags & INCLUDE_CONTENT_TYPE) == 0) - { - dwUseIdentityFlags &= ~AssemblyIdentity::IDENTITY_FLAG_CONTENT_TYPE; - } TextualIdentityParser::ToString(this, dwUseIdentityFlags, displayName); } diff --git a/src/coreclr/binder/inc/assemblyidentity.hpp b/src/coreclr/binder/inc/assemblyidentity.hpp index 4372357567b6fb..4fac3f3186be89 100644 --- a/src/coreclr/binder/inc/assemblyidentity.hpp +++ b/src/coreclr/binder/inc/assemblyidentity.hpp @@ -33,7 +33,6 @@ namespace BINDER_SPACE IDENTITY_FLAG_PROCESSOR_ARCHITECTURE = 0x040, IDENTITY_FLAG_RETARGETABLE = 0x080, IDENTITY_FLAG_PUBLIC_KEY_TOKEN_NULL = 0x100, - IDENTITY_FLAG_CONTENT_TYPE = 0x800, IDENTITY_FLAG_FULL_NAME = (IDENTITY_FLAG_SIMPLE_NAME | IDENTITY_FLAG_VERSION) }; @@ -42,7 +41,6 @@ namespace BINDER_SPACE { m_dwIdentityFlags = IDENTITY_FLAG_EMPTY; m_kProcessorArchitecture = peNone; - m_kContentType = AssemblyContentType_Default; // Need to pre-populate SBuffers because of bogus asserts static const BYTE byteArr[] = { 0 }; @@ -78,7 +76,6 @@ namespace BINDER_SPACE SString m_cultureOrLanguage; SBuffer m_publicKeyOrTokenBLOB; PEKIND m_kProcessorArchitecture; - AssemblyContentType m_kContentType; DWORD m_dwIdentityFlags; }; }; diff --git a/src/coreclr/binder/inc/assemblyname.hpp b/src/coreclr/binder/inc/assemblyname.hpp index 0b878102e97404..40bb065e4aa12f 100644 --- a/src/coreclr/binder/inc/assemblyname.hpp +++ b/src/coreclr/binder/inc/assemblyname.hpp @@ -30,13 +30,11 @@ namespace BINDER_SPACE INCLUDE_VERSION = 0x01, INCLUDE_ARCHITECTURE = 0x02, INCLUDE_RETARGETABLE = 0x04, - INCLUDE_CONTENT_TYPE = 0x08, INCLUDE_PUBLIC_KEY_TOKEN = 0x10, EXCLUDE_CULTURE = 0x20, INCLUDE_ALL = INCLUDE_VERSION | INCLUDE_ARCHITECTURE | INCLUDE_RETARGETABLE - | INCLUDE_CONTENT_TYPE | INCLUDE_PUBLIC_KEY_TOKEN, } INCLUDE_FLAGS; @@ -58,8 +56,6 @@ namespace BINDER_SPACE inline SBuffer &GetPublicKeyTokenBLOB(); inline PEKIND GetArchitecture(); inline void SetArchitecture(PEKIND kArchitecture); - inline AssemblyContentType GetContentType(); - inline void SetContentType(AssemblyContentType kContentType); inline BOOL GetIsRetargetable(); inline void SetIsRetargetable(BOOL fIsRetargetable); inline BOOL GetIsDefinition(); diff --git a/src/coreclr/binder/inc/assemblyname.inl b/src/coreclr/binder/inc/assemblyname.inl index 5946a7e3ed77cb..0349076c342e2e 100644 --- a/src/coreclr/binder/inc/assemblyname.inl +++ b/src/coreclr/binder/inc/assemblyname.inl @@ -70,25 +70,6 @@ void AssemblyName::SetArchitecture(PEKIND kArchitecture) } } -AssemblyContentType AssemblyName::GetContentType() -{ - return m_kContentType; -} - -void AssemblyName::SetContentType(AssemblyContentType kContentType) -{ - m_kContentType = kContentType; - - if (kContentType != AssemblyContentType_Default) - { - SetHave(AssemblyIdentity::IDENTITY_FLAG_CONTENT_TYPE); - } - else - { - SetClear(AssemblyIdentity::IDENTITY_FLAG_CONTENT_TYPE); - } -} - BOOL AssemblyName::GetIsRetargetable() { return m_dwIdentityFlags & AssemblyIdentity::IDENTITY_FLAG_RETARGETABLE; diff --git a/src/coreclr/binder/inc/bindertypes.hpp b/src/coreclr/binder/inc/bindertypes.hpp index 8ec573f8040d5d..2c29265679b550 100644 --- a/src/coreclr/binder/inc/bindertypes.hpp +++ b/src/coreclr/binder/inc/bindertypes.hpp @@ -28,13 +28,6 @@ namespace BINDER_SPACE class BindResult; }; -typedef enum __AssemblyContentType -{ - AssemblyContentType_Default = 0, - AssemblyContentType_WindowsRuntime = 0x1, - AssemblyContentType_Invalid = 0xffffffff, -} AssemblyContentType; - typedef enum __ASM_DISPLAY_FLAGS { ASM_DISPLAYF_VERSION = 0x1, @@ -47,13 +40,11 @@ typedef enum __ASM_DISPLAY_FLAGS ASM_DISPLAYF_RETARGET = 0x80, ASM_DISPLAYF_CONFIG_MASK = 0x100, ASM_DISPLAYF_MVID = 0x200, - ASM_DISPLAYF_CONTENT_TYPE = 0x400, ASM_DISPLAYF_FULL = ASM_DISPLAYF_VERSION | ASM_DISPLAYF_CULTURE | ASM_DISPLAYF_PUBLIC_KEY_TOKEN | ASM_DISPLAYF_RETARGET - | ASM_DISPLAYF_PROCESSORARCHITECTURE - | ASM_DISPLAYF_CONTENT_TYPE, + | ASM_DISPLAYF_PROCESSORARCHITECTURE, } ASM_DISPLAY_FLAGS; typedef enum __PEKIND @@ -82,7 +73,6 @@ struct AssemblyNameData DWORD RevisionNumber; PEKIND ProcessorArchitecture; - AssemblyContentType ContentType; DWORD IdentityFlags; }; diff --git a/src/coreclr/binder/textualidentityparser.cpp b/src/coreclr/binder/textualidentityparser.cpp index 32482bf74ce343..d9fef6d8e42660 100644 --- a/src/coreclr/binder/textualidentityparser.cpp +++ b/src/coreclr/binder/textualidentityparser.cpp @@ -68,18 +68,6 @@ namespace BINDER_SPACE return NULL; } - LPCWSTR ContentTypeToString(AssemblyContentType kContentType) - { - _ASSERTE(kContentType != AssemblyContentType_Default); - - if (kContentType == AssemblyContentType_WindowsRuntime) - { - return W("WindowsRuntime"); - } - - return NULL; - } - BOOL IsWhitespace(WCHAR wcChar) { return ((wcChar == L'\n') || (wcChar == L'\r') || (wcChar == L' ') || (wcChar == L'\t')); @@ -170,13 +158,6 @@ namespace BINDER_SPACE textualIdentity.Append(W(", Retargetable=Yes")); } - if (AssemblyIdentity::Have(dwIdentityFlags, - AssemblyIdentity::IDENTITY_FLAG_CONTENT_TYPE)) - { - textualIdentity.Append(W(", ContentType=")); - textualIdentity.Append(ContentTypeToString(pAssemblyIdentity->m_kContentType)); - } - } EX_CATCH_HRESULT(hr); diff --git a/src/coreclr/vm/assemblyspec.hpp b/src/coreclr/vm/assemblyspec.hpp index d4e2a57a5ac395..8400fdb762626b 100644 --- a/src/coreclr/vm/assemblyspec.hpp +++ b/src/coreclr/vm/assemblyspec.hpp @@ -220,25 +220,6 @@ class AssemblySpec : public BaseAssemblySpec return m_pAppDomain; } - inline HRESULT SetContentType(AssemblyContentType type) - { - LIMITED_METHOD_CONTRACT; - if (type == AssemblyContentType_Default) - { - m_dwFlags = (m_dwFlags & ~afContentType_Mask) | afContentType_Default; - return S_OK; - } - else if (type == AssemblyContentType_WindowsRuntime) - { - // WinRT assemblies are not supported as direct references. - return COR_E_PLATFORMNOTSUPPORTED; - } - else - { - _ASSERTE(!"Unexpected content type."); - return E_UNEXPECTED; - } - } }; #define INITIAL_ASM_SPEC_HASH_SIZE 7 diff --git a/src/coreclr/vm/coreassemblyspec.cpp b/src/coreclr/vm/coreassemblyspec.cpp index 522a158e4a6461..4342f1b98c0204 100644 --- a/src/coreclr/vm/coreassemblyspec.cpp +++ b/src/coreclr/vm/coreassemblyspec.cpp @@ -286,18 +286,6 @@ void BaseAssemblySpec::InitializeWithAssemblyIdentity(BINDER_SPACE::AssemblyIden { m_dwFlags |= afRetargetable; } - - // Content type - if (identity->Have(BINDER_SPACE::AssemblyIdentity::IDENTITY_FLAG_CONTENT_TYPE)) - { - DWORD dwContentType = identity->m_kContentType; - - _ASSERTE((dwContentType == AssemblyContentType_Default) || (dwContentType == AssemblyContentType_WindowsRuntime)); - if (dwContentType == AssemblyContentType_WindowsRuntime) - { - m_dwFlags |= afContentType_WindowsRuntime; - } - } } namespace @@ -406,12 +394,6 @@ VOID BaseAssemblySpec::GetDisplayName(DWORD flags, SString &result) const assemblyIdentity.SetHave(BINDER_SPACE::AssemblyIdentity::IDENTITY_FLAG_RETARGETABLE); } - if ((flags & ASM_DISPLAYF_CONTENT_TYPE) && (m_dwFlags & afContentType_Mask) == afContentType_WindowsRuntime) - { - assemblyIdentity.SetHave(BINDER_SPACE::AssemblyIdentity::IDENTITY_FLAG_CONTENT_TYPE); - assemblyIdentity.m_kContentType = AssemblyContentType_WindowsRuntime; - } - IfFailThrow(BINDER_SPACE::TextualIdentityParser::ToString(&assemblyIdentity, assemblyIdentity.m_dwIdentityFlags, result)); @@ -460,10 +442,4 @@ void BaseAssemblySpec::PopulateAssemblyNameData(AssemblyNameData &data) const { data.IdentityFlags |= BINDER_SPACE::AssemblyIdentity::IDENTITY_FLAG_RETARGETABLE; } - - if ((m_dwFlags & afContentType_Mask) == afContentType_WindowsRuntime) - { - data.ContentType = AssemblyContentType_WindowsRuntime; - data.IdentityFlags |= BINDER_SPACE::AssemblyIdentity::IDENTITY_FLAG_CONTENT_TYPE; - } } From dc3d5a0874151a2201459a8abbade5650d72c717 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Fri, 1 May 2026 10:30:40 -0400 Subject: [PATCH 064/115] Remove ExInfo::m_hThrowable - use direct pointer for exception objects (#127300) > [!NOTE] > This PR was authored with the assistance of GitHub Copilot. ## Summary Replace the GCHandle-based `m_hThrowable` field in ExInfo with direct use of the existing `m_exception` OBJECTREF field, matching NativeAOT's approach. ## Motivation CoreCLR's ExInfo stored exception objects via **two redundant fields**: `OBJECTHANDLE m_hThrowable` (GC handle table indirection) and `OBJECTREF m_exception` (direct pointer used by the new EH path shared with NativeAOT). NativeAOT only has `m_exception`. The handle added allocation/deallocation overhead (~5 interlocked ops per throw) and an extra pointer indirection on every read, but **none of the 15 consumers actually required OBJECTHANDLE guarantees** - they all ran in cooperative GC mode and immediately dereferenced the handle. ## Key Changes - **Remove `OBJECTHANDLE m_hThrowable`** from ExInfo, saving 8 bytes (x64) / 4 bytes (x86) - **Add GC root scanning** of ExInfo chain in `ScanStackRoots` (gcenv.ee.cpp), mirroring NativeAOT's `GcScanRootsWorker` - this keeps superseded exception objects alive without handles - **Remove `GCPROTECT_BEGIN(exInfo.m_exception)`** from all 3 dispatch entry points - the chain scanning already reports `&m_exception` to the GC, and reporting the same location twice corrupts the GC's relocation logic (clr-code-guide.md section 2.1.5). Debug OBJECTREF tracking is satisfied via `Thread::ObjectRefProtected` in the ExInfo constructor. - **Add `ExInfo::GetThrowableAsPseudoHandle()`** - returns the target address of `m_exception` as a pseudo-handle (not a real GC handle table entry). Uses `PTR_HOST_MEMBER_TADDR` for correct DAC target address computation. The slot is updated during GC by the ExInfo chain scanner. - **Remove `GetThrowableAsHandle()`** - replaced by `GetThrowableAsPseudoHandle()` on ExInfo, ThreadExceptionState, and Thread - **Remove `SetThrowable()`** entirely - managed EH code writes `m_exception` directly; the `SetThrowableErrorChecking` enum and `STEC_*` constants are also removed. - **Remove `GetMyThread()`** - dead function with zero callers - **Merge `AppendElementImpl` into `AppendElement`** - only one caller remained after removing the OBJECTHANDLE overload. The merged function handles foreign exception semantics and preallocated exception checks. - **Remove OBJECTHANDLE overload of `AppendElement`** - all callers now pass OBJECTREF directly (including prestub.cpp and threads.cpp via `ObjectFromHandle`) - **Fix `StackTraceInfo::AppendElement`** preallocated-exception check: changed from `IsPreallocatedExceptionHandle` to `IsPreallocatedExceptionObject(ObjectFromHandle(...))` - **Update AsmOffsets** constants for the new field layout (validated by static_asserts) - **Update Interop propagation callback** to take OBJECTREF instead of OBJECTHANDLE - **Update DAC code**: `GetCurrentException` reads from ExInfo first via `GetThrowableAsPseudoHandle()`, falls back to `m_LastThrownObjectHandle`. `GetThreadException` uses same pattern. - **Update cDAC**: `ThrownObjectHandle` -> `ThrownObject` (direct pointer, no handle dereference); `GetCurrentExceptionHandle` returns field address as pseudo-handle for backward compatibility ## What stays unchanged `Thread::m_LastThrownObjectHandle` remains as an OBJECTHANDLE - it is required by the ICorDebug managed debugging protocol (`SendExceptionHelperAndBlock` is `MODE_ANY`, right-side debugger reads through handle cross-process via `BuildFromGCHandle`). ## Efficiency Per exception throw, this eliminates: - ~5 interlocked operations (handle alloc/destroy) - 1 handle table slot allocation + bookkeeping - 1 pointer indirection per throwable read (2-hop -> 1-hop) - 8 bytes from ExInfo struct (x64) - 3 GCFrame constructions per dispatch entry point The only added cost is ~2 pointer reads per thread per GC for ExInfo chain walking - negligible since exception chains are almost always 1-2 nodes deep. ## Testing - Checked + Release CLR builds: 0 errors, 0 warnings - GCStress=0xC + HeapVerify=1: 100/100 nested exception iterations pass (20 levels, 100 iterations) - All 1731 cDAC tests pass - AsmOffset static_asserts validate all field offsets --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jan Kotas --- .../Runtime/ExceptionServices/AsmOffsets.cs | 24 +++--- src/coreclr/debug/daccess/dacdbiimpl.cpp | 5 +- src/coreclr/debug/daccess/request.cpp | 47 +++++------ src/coreclr/debug/daccess/task.cpp | 4 +- src/coreclr/debug/ee/debugger.cpp | 4 +- src/coreclr/vm/clrex.h | 2 - .../vm/datadescriptor/datadescriptor.inc | 2 +- src/coreclr/vm/eedbginterfaceimpl.cpp | 18 +---- src/coreclr/vm/eepolicy.cpp | 3 +- src/coreclr/vm/excep.cpp | 52 ++---------- src/coreclr/vm/exceptionhandling.cpp | 28 +++---- src/coreclr/vm/exinfo.cpp | 20 ++--- src/coreclr/vm/exinfo.h | 36 ++++----- src/coreclr/vm/exstate.cpp | 79 ++++--------------- src/coreclr/vm/exstate.h | 14 +--- src/coreclr/vm/gcenv.ee.cpp | 14 ++++ src/coreclr/vm/interoplibinterface.h | 4 +- src/coreclr/vm/interoplibinterface_objc.cpp | 9 +-- src/coreclr/vm/interoplibinterface_shared.cpp | 4 +- src/coreclr/vm/prestub.cpp | 4 +- src/coreclr/vm/threads.cpp | 37 +++------ src/coreclr/vm/threads.h | 19 ++--- src/coreclr/vm/threads.inl | 7 -- .../Contracts/Exception_1.cs | 10 ++- .../Contracts/Thread_1.cs | 26 +++--- .../Data/ExceptionInfo.cs | 4 +- .../MockDescriptors/MockDescriptors.Thread.cs | 10 +-- src/native/managed/cdac/tests/ThreadTests.cs | 16 ++-- 28 files changed, 185 insertions(+), 317 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/ExceptionServices/AsmOffsets.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/ExceptionServices/AsmOffsets.cs index 3ffb3201206c2b..23474fdb1e629d 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Runtime/ExceptionServices/AsmOffsets.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/ExceptionServices/AsmOffsets.cs @@ -189,12 +189,12 @@ class AsmOffsets public const int SIZEOF__EHEnum = 0x20; public const int OFFSETOF__StackFrameIterator__m_pRegDisplay = 0x20; public const int OFFSETOF__ExInfo__m_pPrevExInfo = 0; - public const int OFFSETOF__ExInfo__m_pExContext = 0xa8; - public const int OFFSETOF__ExInfo__m_exception = 0xb0; - public const int OFFSETOF__ExInfo__m_kind = 0xb8; - public const int OFFSETOF__ExInfo__m_passNumber = 0xb9; - public const int OFFSETOF__ExInfo__m_idxCurClause = 0xbc; - public const int OFFSETOF__ExInfo__m_frameIter = 0xc0; + public const int OFFSETOF__ExInfo__m_pExContext = 0xa0; + public const int OFFSETOF__ExInfo__m_exception = 0xa8; + public const int OFFSETOF__ExInfo__m_kind = 0xb0; + public const int OFFSETOF__ExInfo__m_passNumber = 0xb1; + public const int OFFSETOF__ExInfo__m_idxCurClause = 0xb4; + public const int OFFSETOF__ExInfo__m_frameIter = 0xb8; public const int OFFSETOF__ExInfo__m_notifyDebuggerSP = OFFSETOF__ExInfo__m_frameIter + SIZEOF__StackFrameIterator; public const int OFFSETOF__ExInfo__m_pCatchHandler = OFFSETOF__ExInfo__m_frameIter + SIZEOF__StackFrameIterator + 0x48; public const int OFFSETOF__ExInfo__m_handlingFrameSP = OFFSETOF__ExInfo__m_frameIter + SIZEOF__StackFrameIterator + 0x50; @@ -217,12 +217,12 @@ class AsmOffsets public const int SIZEOF__EHEnum = 0x10; public const int OFFSETOF__StackFrameIterator__m_pRegDisplay = 0x14; public const int OFFSETOF__ExInfo__m_pPrevExInfo = 0; - public const int OFFSETOF__ExInfo__m_pExContext = 0x5c; - public const int OFFSETOF__ExInfo__m_exception = 0x60; - public const int OFFSETOF__ExInfo__m_kind = 0x64; - public const int OFFSETOF__ExInfo__m_passNumber = 0x65; - public const int OFFSETOF__ExInfo__m_idxCurClause = 0x68; - public const int OFFSETOF__ExInfo__m_frameIter = 0x6c; + public const int OFFSETOF__ExInfo__m_pExContext = 0x58; + public const int OFFSETOF__ExInfo__m_exception = 0x5c; + public const int OFFSETOF__ExInfo__m_kind = 0x60; + public const int OFFSETOF__ExInfo__m_passNumber = 0x61; + public const int OFFSETOF__ExInfo__m_idxCurClause = 0x64; + public const int OFFSETOF__ExInfo__m_frameIter = 0x68; public const int OFFSETOF__ExInfo__m_notifyDebuggerSP = OFFSETOF__ExInfo__m_frameIter + SIZEOF__StackFrameIterator; public const int OFFSETOF__ExInfo__m_pCatchHandler = OFFSETOF__ExInfo__m_frameIter + SIZEOF__StackFrameIterator + 0x2c; public const int OFFSETOF__ExInfo__m_handlingFrameSP = OFFSETOF__ExInfo__m_frameIter + SIZEOF__StackFrameIterator + 0x30; diff --git a/src/coreclr/debug/daccess/dacdbiimpl.cpp b/src/coreclr/debug/daccess/dacdbiimpl.cpp index 0eb96e0af27b9b..dfb417762d77d8 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.cpp +++ b/src/coreclr/debug/daccess/dacdbiimpl.cpp @@ -4795,7 +4795,7 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::HasUnhandledException(VMPTR_Threa { // most managed exceptions are just a throwable bound to a // native exception. In that case this handle will be non-null - OBJECTHANDLE ohException = pThread->GetThrowableAsHandle(); + OBJECTHANDLE ohException = pThread->GetThrowableAsPseudoHandle(); if (ohException != (OBJECTHANDLE)NULL) { // during the UEF we set the unhandled bit, if it is set the exception @@ -4932,8 +4932,7 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetCurrentException(VMPTR_Thread Thread * pThread = vmThread.GetDacPtr(); - // OBJECTHANDLEs are really just TADDRs. - OBJECTHANDLE ohException = pThread->GetThrowableAsHandle(); // ohException can be NULL + OBJECTHANDLE ohException = pThread->GetThrowableAsPseudoHandle(); if (ohException == (OBJECTHANDLE)NULL) { diff --git a/src/coreclr/debug/daccess/request.cpp b/src/coreclr/debug/daccess/request.cpp index 10b78d156aa84a..c68ee4c8530fd4 100644 --- a/src/coreclr/debug/daccess/request.cpp +++ b/src/coreclr/debug/daccess/request.cpp @@ -3308,7 +3308,7 @@ ClrDataAccess::GetNestedExceptionData(CLRDATA_ADDRESS exception, CLRDATA_ADDRESS } else { - *exceptionObject = TO_CDADDR(*PTR_TADDR(pExData->m_hThrowable)); + *exceptionObject = dac_cast(pExData->m_exception); *nextNestedException = PTR_HOST_TO_TADDR(pExData->m_pPrevNestedInfo); } @@ -4051,35 +4051,30 @@ HRESULT ClrDataAccess::GetClrWatsonBucketsWorker(Thread * pThread, GenericModeBl // By default, there are no buckets PTR_VOID pBuckets = NULL; - // Get the handle to the throwble - OBJECTHANDLE ohThrowable = pThread->GetThrowableAsHandle(); - if (ohThrowable != NULL) + // Get the current throwable + OBJECTREF oThrowable = pThread->GetExceptionState()->GetThrowable(); + if (oThrowable != NULL) { - // Get the object from handle and check if the throwable is preallocated or not - OBJECTREF oThrowable = ObjectFromHandle(ohThrowable); - if (oThrowable != NULL) + // Does the throwable have buckets? + U1ARRAYREF refWatsonBucketArray = ((EXCEPTIONREF)oThrowable)->GetWatsonBucketReference(); + if (refWatsonBucketArray != NULL) { - // Does the throwable have buckets? - U1ARRAYREF refWatsonBucketArray = ((EXCEPTIONREF)oThrowable)->GetWatsonBucketReference(); - if (refWatsonBucketArray != NULL) - { - // Get the watson buckets from the throwable for non-preallocated - // exceptions - pBuckets = dac_cast(refWatsonBucketArray->GetDataPtr()); - } - else + // Get the watson buckets from the throwable for non-preallocated + // exceptions + pBuckets = dac_cast(refWatsonBucketArray->GetDataPtr()); + } + else + { + // This is a preallocated exception object - check if the UE Watson bucket tracker + // has any bucket details + pBuckets = pThread->GetExceptionState()->GetUEWatsonBucketTracker()->RetrieveWatsonBuckets(); + if (pBuckets == NULL) { - // This is a preallocated exception object - check if the UE Watson bucket tracker - // has any bucket details - pBuckets = pThread->GetExceptionState()->GetUEWatsonBucketTracker()->RetrieveWatsonBuckets(); - if (pBuckets == NULL) + // Since the UE watson bucket tracker does not have them, look up the current + // exception tracker + if (pThread->GetExceptionState()->GetCurrentExceptionTracker() != NULL) { - // Since the UE watson bucket tracker does not have them, look up the current - // exception tracker - if (pThread->GetExceptionState()->GetCurrentExceptionTracker() != NULL) - { - pBuckets = pThread->GetExceptionState()->GetCurrentExceptionTracker()->GetWatsonBucketTracker()->RetrieveWatsonBuckets(); - } + pBuckets = pThread->GetExceptionState()->GetCurrentExceptionTracker()->GetWatsonBucketTracker()->RetrieveWatsonBuckets(); } } } diff --git a/src/coreclr/debug/daccess/task.cpp b/src/coreclr/debug/daccess/task.cpp index c8ed4df807d135..4fc7604440aca0 100644 --- a/src/coreclr/debug/daccess/task.cpp +++ b/src/coreclr/debug/daccess/task.cpp @@ -4562,7 +4562,7 @@ ClrDataExceptionState::GetPrevious( m_thread, CLRDATA_EXCEPTION_DEFAULT, m_prevExInfo, - m_prevExInfo->m_hThrowable, + m_prevExInfo->GetThrowableAsPseudoHandle(), m_prevExInfo->m_pPrevNestedInfo); status = *exState ? S_OK : E_OUTOFMEMORY; } @@ -4926,7 +4926,7 @@ ClrDataExceptionState::NewFromThread(ClrDataAccess* dac, thread, CLRDATA_EXCEPTION_DEFAULT, exState, - exState->m_hThrowable, + exState->GetThrowableAsPseudoHandle(), exState->m_pPrevNestedInfo); if (!exIf) { diff --git a/src/coreclr/debug/ee/debugger.cpp b/src/coreclr/debug/ee/debugger.cpp index b0641ac5daa5bd..1e853b149fbc7c 100644 --- a/src/coreclr/debug/ee/debugger.cpp +++ b/src/coreclr/debug/ee/debugger.cpp @@ -7878,8 +7878,8 @@ BOOL Debugger::ShouldSendCatchHandlerFound(Thread* pThread) else { BOOL forceSendCatchHandlerFound = FALSE; - OBJECTHANDLE objHandle = pThread->GetThrowableAsHandle(); - OBJECTHANDLE retrievedHandle = m_pForceCatchHandlerFoundEventsTable->Lookup(objHandle); //destroy handle + OBJECTHANDLE objHandle = pThread->GetThrowableAsPseudoHandle(); + OBJECTHANDLE retrievedHandle = m_pForceCatchHandlerFoundEventsTable->Lookup(objHandle); if (retrievedHandle != NULL) { forceSendCatchHandlerFound = TRUE; diff --git a/src/coreclr/vm/clrex.h b/src/coreclr/vm/clrex.h index 01fbce5df1a1c7..e05e25c615989d 100644 --- a/src/coreclr/vm/clrex.h +++ b/src/coreclr/vm/clrex.h @@ -70,9 +70,7 @@ class StackTraceInfo static OBJECTREF GetKeepAliveObject(MethodDesc* pMethod); static void EnsureStackTraceArray(StackTraceArrayProtect *pStackTraceArrayProtected, size_t neededSize); static void EnsureKeepAliveArray(PTRARRAYREF *ppKeepAliveArray, size_t neededSize); - static void AppendElementImpl(OBJECTREF pThrowable, UINT_PTR currentIP, UINT_PTR currentSP, MethodDesc* pFunc, CrawlFrame* pCf, Thread* pThread, BOOL fRaisingForeignException); public: - static void AppendElement(OBJECTHANDLE hThrowable, UINT_PTR currentIP, UINT_PTR currentSP, MethodDesc* pFunc, CrawlFrame* pCf); static void AppendElement(OBJECTREF pThrowable, UINT_PTR currentIP, UINT_PTR currentSP, MethodDesc* pFunc, CrawlFrame* pCf); }; diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index ff8738e9860743..f1026a91424dea 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -135,7 +135,7 @@ CDAC_TYPE_END(Exception) CDAC_TYPE_BEGIN(ExceptionInfo) CDAC_TYPE_INDETERMINATE(ExceptionInfo) CDAC_TYPE_FIELD(ExceptionInfo, T_POINTER, PreviousNestedInfo, offsetof(ExInfo, m_pPrevNestedInfo)) -CDAC_TYPE_FIELD(ExceptionInfo, TYPE(ObjectHandle), ThrownObjectHandle, offsetof(ExInfo, m_hThrowable)) +CDAC_TYPE_FIELD(ExceptionInfo, T_POINTER, ThrownObject, offsetof(ExInfo, m_exception)) CDAC_TYPE_FIELD(ExceptionInfo, T_UINT32, ExceptionFlags, cdac_data::ExceptionFlagsValue) CDAC_TYPE_FIELD(ExceptionInfo, T_POINTER, StackLowBound, cdac_data::StackLowBound) CDAC_TYPE_FIELD(ExceptionInfo, T_POINTER, StackHighBound, cdac_data::StackHighBound) diff --git a/src/coreclr/vm/eedbginterfaceimpl.cpp b/src/coreclr/vm/eedbginterfaceimpl.cpp index b2c9276425a5e7..2c33b9c94a7fc7 100644 --- a/src/coreclr/vm/eedbginterfaceimpl.cpp +++ b/src/coreclr/vm/eedbginterfaceimpl.cpp @@ -239,7 +239,7 @@ OBJECTHANDLE EEDbgInterfaceImpl::GetThreadException(Thread *pThread) } CONTRACTL_END; - OBJECTHANDLE oh = pThread->GetThrowableAsHandle(); + OBJECTHANDLE oh = pThread->GetThrowableAsPseudoHandle(); if (oh != NULL) { @@ -261,21 +261,7 @@ bool EEDbgInterfaceImpl::IsThreadExceptionNull(Thread *pThread) } CONTRACTL_END; - // - // We're assuming that the handle on the - // thread is a strong handle and we're goona check it for - // NULL. We're also assuming something about the - // implementation of the handle here, too. - // - OBJECTHANDLE h = pThread->GetThrowableAsHandle(); - if (h == NULL) - { - return true; - } - - void *pThrowable = *((void**)h); - - return (pThrowable == NULL); + return pThread->IsThrowableNull(); } void EEDbgInterfaceImpl::ClearThreadException(Thread *pThread) diff --git a/src/coreclr/vm/eepolicy.cpp b/src/coreclr/vm/eepolicy.cpp index f756091105964e..6fcbaec5aaf61a 100644 --- a/src/coreclr/vm/eepolicy.cpp +++ b/src/coreclr/vm/eepolicy.cpp @@ -789,8 +789,7 @@ void DECLSPEC_NORETURN EEPolicy::HandleFatalStackOverflow(EXCEPTION_POINTERS *pE OBJECTHANDLE ohSO = CLRException::GetPreallocatedStackOverflowExceptionHandle(); if (ohSO != NULL) { - pThread->SafeSetThrowables(ObjectFromHandle(ohSO) - DEBUG_ARG(ThreadExceptionState::STEC_CurrentTrackerEqualNullOkHackForFatalStackOverflow), + pThread->SafeSetThrowables(ObjectFromHandle(ohSO), TRUE); } else diff --git a/src/coreclr/vm/excep.cpp b/src/coreclr/vm/excep.cpp index 49b323075cda1b..a841feafc3aca4 100644 --- a/src/coreclr/vm/excep.cpp +++ b/src/coreclr/vm/excep.cpp @@ -1849,7 +1849,7 @@ BOOL IsInFirstFrameOfHandler(Thread *pThread, IJitManager *pJitManager, const ME CONTRACTL_END; // if don't have a throwable the aren't processing an exception - if (IsHandleNullUnchecked(pThread->GetThrowableAsHandle())) + if (pThread->IsThrowableNull()) return FALSE; EH_CLAUSE_ENUMERATOR pEnumState; @@ -2546,9 +2546,9 @@ OBJECTREF StackTraceInfo::GetKeepAliveObject(MethodDesc* pMethod) } // -// Append stack frame to an exception stack trace - handle version. +// Append stack frame to an exception stack trace // -void StackTraceInfo::AppendElement(OBJECTHANDLE hThrowable, UINT_PTR currentIP, UINT_PTR currentSP, MethodDesc* pFunc, CrawlFrame* pCf) +void StackTraceInfo::AppendElement(OBJECTREF pThrowable, UINT_PTR currentIP, UINT_PTR currentSP, MethodDesc* pFunc, CrawlFrame* pCf) { CONTRACTL { @@ -2559,7 +2559,7 @@ void StackTraceInfo::AppendElement(OBJECTHANDLE hThrowable, UINT_PTR currentIP, CONTRACTL_END Thread *pThread = GetThread(); - MethodTable* pMT = ObjectFromHandle(hThrowable)->GetMethodTable(); + MethodTable* pMT = pThrowable->GetMethodTable(); _ASSERTE(IsException(pMT)); PTR_ThreadExceptionState pCurTES = pThread->GetExceptionState(); @@ -2572,48 +2572,11 @@ void StackTraceInfo::AppendElement(OBJECTHANDLE hThrowable, UINT_PTR currentIP, LOG((LF_EH, LL_INFO10000, "StackTraceInfo::AppendElement IP = %p, SP = %p, %s::%s\n", currentIP, currentSP, pFunc ? pFunc->m_pszDebugClassName : "", pFunc ? pFunc->m_pszDebugMethodName : "" )); // Do not save stacktrace to preallocated exception. These are shared. - if (CLRException::IsPreallocatedExceptionHandle(hThrowable)) + if (CLRException::IsPreallocatedExceptionObject(pThrowable)) { return; } - AppendElementImpl(ObjectFromHandle(hThrowable), currentIP, currentSP, pFunc, pCf, pThread, fRaisingForeignException); -} - -// -// Append stack frame to an exception stack trace - objectref version for runtime-async stack frames. -// -void StackTraceInfo::AppendElement(OBJECTREF pThrowable, UINT_PTR currentIP, UINT_PTR currentSP, MethodDesc* pFunc, CrawlFrame* pCf) -{ - CONTRACTL - { - GC_TRIGGERS; - NOTHROW; - MODE_COOPERATIVE; - } - CONTRACTL_END - - Thread *pThread = GetThread(); - MethodTable* pMT = pThrowable->GetMethodTable(); - _ASSERTE(IsException(pMT)); - - LOG((LF_EH, LL_INFO10000, "StackTraceInfo::AppendElement IP = %p, SP = %p, %s::%s\n", currentIP, currentSP, pFunc ? pFunc->m_pszDebugClassName : "", pFunc ? pFunc->m_pszDebugMethodName : "" )); - AppendElementImpl(pThrowable, currentIP, currentSP, pFunc, pCf, pThread, FALSE /* fRaisingForeignException */); -} - -// -// Append stack frame to an exception stack trace. -// -void StackTraceInfo::AppendElementImpl(OBJECTREF pThrowable, UINT_PTR currentIP, UINT_PTR currentSP, MethodDesc* pFunc, CrawlFrame* pCf, Thread* pThread, BOOL fRaisingForeignException) -{ - CONTRACTL - { - GC_TRIGGERS; - NOTHROW; - MODE_COOPERATIVE; - } - CONTRACTL_END - if ((pFunc != NULL && pFunc->IsDiagnosticsHidden())) return; @@ -5645,7 +5608,9 @@ void HandleManagedFault(EXCEPTION_RECORD* pExceptionRecord, CONTEXT* pContext) } } - GCPROTECT_BEGIN(exInfo.m_exception); + // m_exception is GC-reported via ExInfo chain scanning in ScanStackRoots. + // Do NOT also GCPROTECT it - reporting the same location twice corrupts + // the GC's relocation logic (see clr-code-guide.md §2.1.5). UnmanagedCallersOnlyCaller throwHwEx(METHOD__EH__RH_THROWHW_EX); pThread->IncPreventAbort(); @@ -5655,7 +5620,6 @@ void HandleManagedFault(EXCEPTION_RECORD* pExceptionRecord, CONTEXT* pContext) DispatchExSecondPass(&exInfo); - GCPROTECT_END(); UNREACHABLE(); } diff --git a/src/coreclr/vm/exceptionhandling.cpp b/src/coreclr/vm/exceptionhandling.cpp index 00f6db4a17d612..6473f4d069958c 100644 --- a/src/coreclr/vm/exceptionhandling.cpp +++ b/src/coreclr/vm/exceptionhandling.cpp @@ -1447,7 +1447,9 @@ BOOL HandleHardwareException(PAL_SEHException* ex) exInfo.TakeExceptionPointersOwnership(ex); } - GCPROTECT_BEGIN(exInfo.m_exception); + // m_exception is GC-reported via ExInfo chain scanning in ScanStackRoots. + // Do NOT also GCPROTECT it - reporting the same location twice corrupts + // the GC's relocation logic (see clr-code-guide.md §2.1.5). UnmanagedCallersOnlyCaller throwHwEx(METHOD__EH__RH_THROWHW_EX); pThread->IncPreventAbort(); @@ -1457,8 +1459,6 @@ BOOL HandleHardwareException(PAL_SEHException* ex) DispatchExSecondPass(&exInfo); - GCPROTECT_END(); - UNREACHABLE(); } else @@ -1620,8 +1620,9 @@ VOID DECLSPEC_NORETURN DispatchManagedException(OBJECTREF throwable, CONTEXT* pE } } - GCPROTECT_BEGIN(exInfo.m_exception); - + // m_exception is GC-reported via ExInfo chain scanning in ScanStackRoots. + // Do NOT also GCPROTECT it - reporting the same location twice corrupts + // the GC's relocation logic (see clr-code-guide.md §2.1.5). UnmanagedCallersOnlyCaller throwEx(METHOD__EH__RH_THROW_EX); pThread->IncPreventAbort(); @@ -1631,7 +1632,6 @@ VOID DECLSPEC_NORETURN DispatchManagedException(OBJECTREF throwable, CONTEXT* pE DispatchExSecondPass(&exInfo); - GCPROTECT_END(); GCPROTECT_END(); UNREACHABLE(); @@ -1674,7 +1674,9 @@ VOID DECLSPEC_NORETURN DispatchRethrownManagedException(CONTEXT* pExceptionConte ExInfo exInfo(pThread, pActiveExInfo->m_ptrs.ExceptionRecord, pExceptionContext, ExKind::None); - GCPROTECT_BEGIN(exInfo.m_exception); + // m_exception is GC-reported via ExInfo chain scanning in ScanStackRoots. + // Do NOT also GCPROTECT it - reporting the same location twice corrupts + // the GC's relocation logic (see clr-code-guide.md §2.1.5). UnmanagedCallersOnlyCaller rethrow(METHOD__EH__RH_RETHROW); pThread->IncPreventAbort(); @@ -1683,8 +1685,6 @@ VOID DECLSPEC_NORETURN DispatchRethrownManagedException(CONTEXT* pExceptionConte rethrow.InvokeDirect(pActiveExInfo, &exInfo); DispatchExSecondPass(&exInfo); - GCPROTECT_END(); - UNREACHABLE(); } @@ -2922,7 +2922,7 @@ ExInfo::StackRange::StackRange() void ExInfo::EnumMemoryRegions(CLRDataEnumMemoryFlags flags) { // ExInfo is embedded so don't enum 'this'. - OBJECTHANDLE_EnumMemoryRegions(m_hThrowable); + OBJECTREF_EnumMemoryRegions(m_exception); m_ptrs.ExceptionRecord.EnumMem(); m_ptrs.ContextRecord.EnumMem(); } @@ -2973,7 +2973,7 @@ extern "C" void QCALLTYPE AppendExceptionStackFrame(QCall::ObjectHandleOnStack e _ASSERTE(pMD == codeInfo.GetMethodDesc()); #endif // _DEBUG - StackTraceInfo::AppendElement(pExInfo->m_hThrowable, ip, sp, pMD, &pExInfo->m_frameIter.m_crawl); + StackTraceInfo::AppendElement(pExInfo->m_exception, ip, sp, pMD, &pExInfo->m_frameIter.m_crawl); } } @@ -3772,7 +3772,7 @@ CLR_BOOL SfiInitWorker(StackFrameIterator* pThis, CONTEXT* pStackwalkCtx, CLR_BO if (pMD != NULL) { GCX_COOP(); - StackTraceInfo::AppendElement(pExInfo->m_hThrowable, 0, GetRegdisplaySP(pExInfo->m_frameIter.m_crawl.GetRegisterSet()), pMD, &pExInfo->m_frameIter.m_crawl); + StackTraceInfo::AppendElement(pExInfo->m_exception, 0, GetRegdisplaySP(pExInfo->m_frameIter.m_crawl.GetRegisterSet()), pMD, &pExInfo->m_frameIter.m_crawl); #if defined(DEBUGGING_SUPPORTED) if (NotifyDebuggerOfStub(pThread, pFrame)) @@ -3954,7 +3954,7 @@ CLR_BOOL SfiNextWorker(StackFrameIterator* pThis, uint* uExCollideClauseIdx, CLR void* callbackCxt = NULL; Interop::ManagedToNativeExceptionCallback callback = Interop::GetPropagatingExceptionCallback( &codeInfo, - pTopExInfo->m_hThrowable, + pTopExInfo->m_exception, &callbackCxt); if (callback != NULL) @@ -4091,7 +4091,7 @@ CLR_BOOL SfiNextWorker(StackFrameIterator* pThis, uint* uExCollideClauseIdx, CLR if (pMD != NULL) { GCX_COOP(); - StackTraceInfo::AppendElement(pTopExInfo->m_hThrowable, 0, GetRegdisplaySP(pTopExInfo->m_frameIter.m_crawl.GetRegisterSet()), pMD, &pTopExInfo->m_frameIter.m_crawl); + StackTraceInfo::AppendElement(pTopExInfo->m_exception, 0, GetRegdisplaySP(pTopExInfo->m_frameIter.m_crawl.GetRegisterSet()), pMD, &pTopExInfo->m_frameIter.m_crawl); #if defined(DEBUGGING_SUPPORTED) if (NotifyDebuggerOfStub(pThread, pFrame)) diff --git a/src/coreclr/vm/exinfo.cpp b/src/coreclr/vm/exinfo.cpp index da05037a9f4531..76d02424215de6 100644 --- a/src/coreclr/vm/exinfo.cpp +++ b/src/coreclr/vm/exinfo.cpp @@ -13,7 +13,6 @@ ExInfo::ExInfo(Thread *pThread, EXCEPTION_RECORD *pExceptionRecord, CONTEXT *pExceptionContext, ExKind exceptionKind) : m_pPrevNestedInfo(pThread->GetExceptionState()->GetCurrentExceptionTracker()), - m_hThrowable{}, m_ptrs({pExceptionRecord, pExceptionContext}), m_fDeliveredFirstChanceNotification(FALSE), m_ExceptionCode((pExceptionRecord != PTR_NULL) ? pExceptionRecord->ExceptionCode : 0), @@ -39,6 +38,16 @@ ExInfo::ExInfo(Thread *pThread, EXCEPTION_RECORD *pExceptionRecord, CONTEXT *pEx #endif // HOST_WINDOWS { pThread->GetExceptionState()->m_pCurrentTracker = this; + + // m_exception is GC-reported via ExInfo chain scanning in ScanStackRoots + // (not via GCPROTECT - reporting the same location twice corrupts the GC + // relocation logic; see clr-code-guide.md §2.1.5). Mark the slot as + // protected in the debug OBJECTREF tracking table so that checked-build + // validation knows the reference is rooted. +#ifdef USE_CHECKED_OBJECTREFS + Thread::ObjectRefProtected(&m_exception); +#endif + m_pInitialFrame = pThread->GetFrame(); if (exceptionKind == ExKind::HardwareFault) { @@ -72,14 +81,7 @@ void ExInfo::TakeExceptionPointersOwnership(PAL_SEHException* ex) void ExInfo::ReleaseResources() { - if (m_hThrowable) - { - if (!CLRException::IsPreallocatedExceptionHandle(m_hThrowable)) - { - DestroyHandle(m_hThrowable); - } - m_hThrowable = NULL; - } + m_exception = NULL; #ifndef TARGET_UNIX // Clear any held Watson Bucketing details diff --git a/src/coreclr/vm/exinfo.h b/src/coreclr/vm/exinfo.h index d6a3483c9f9100..1b1db4c1b9d122 100644 --- a/src/coreclr/vm/exinfo.h +++ b/src/coreclr/vm/exinfo.h @@ -92,8 +92,6 @@ struct ExInfo // Previous ExInfo in the chain of exceptions rethrown from their catch / finally handlers PTR_ExInfo m_pPrevNestedInfo; - // thrown exception object handle - OBJECTHANDLE m_hThrowable; // EXCEPTION_RECORD and CONTEXT_RECORD describing the exception and its location DAC_EXCEPTION_POINTERS m_ptrs; // Information for the funclet we are calling @@ -218,12 +216,23 @@ struct ExInfo } CONTRACTL_END; - if (0 != m_hThrowable) - { - return ObjectFromHandle(m_hThrowable); - } + return m_exception; + } + + // Returns the target address of the m_exception field as a pseudo-handle. + // This is NOT a real GC handle table entry - it is the address of an + // OBJECTREF slot on the stack. Reading through it yields the exception + // Object*. The slot is updated during GC by the ExInfo chain scanner + // in ScanStackRoots (gcenv.ee.cpp). The pseudo-handle has the same + // lifetime as the ExInfo. + inline OBJECTHANDLE GetThrowableAsPseudoHandle() + { + LIMITED_METHOD_DAC_CONTRACT; + + if (m_exception == NULL) + return (OBJECTHANDLE)NULL; - return NULL; + return (OBJECTHANDLE)PTR_HOST_MEMBER_TADDR(ExInfo, this, m_exception); } inline BOOL DeliveredFirstChanceNotification() @@ -257,19 +266,6 @@ struct ExInfo return !m_ExceptionFlags.UnwindHasStarted(); } -#ifndef DACCESS_COMPILE - void DestroyExceptionHandle() - { - // Never, ever destroy a preallocated exception handle. - if ((m_hThrowable != NULL) && !CLRException::IsPreallocatedExceptionHandle(m_hThrowable)) - { - DestroyHandle(m_hThrowable); - } - - m_hThrowable = NULL; - } -#endif // !DACCESS_COMPILE - #ifdef DACCESS_COMPILE void EnumMemoryRegions(CLRDataEnumMemoryFlags flags); #endif diff --git a/src/coreclr/vm/exstate.cpp b/src/coreclr/vm/exstate.cpp index 50ce0024b3a41f..0738f6b2bb3615 100644 --- a/src/coreclr/vm/exstate.cpp +++ b/src/coreclr/vm/exstate.cpp @@ -13,13 +13,15 @@ #include "comutilnative.h" // for assertions only #endif -OBJECTHANDLE ThreadExceptionState::GetThrowableAsHandle() + +// See ExInfo::GetThrowableAsPseudoHandle for details on the pseudo-handle. +OBJECTHANDLE ThreadExceptionState::GetThrowableAsPseudoHandle() { - WRAPPER_NO_CONTRACT; + LIMITED_METHOD_DAC_CONTRACT; if (m_pCurrentTracker) { - return m_pCurrentTracker->m_hThrowable; + return m_pCurrentTracker->GetThrowableAsPseudoHandle(); } return (OBJECTHANDLE)NULL; @@ -46,14 +48,6 @@ ThreadExceptionState::~ThreadExceptionState() #endif // !TARGET_UNIX } -#ifndef DACCESS_COMPILE - -Thread* ThreadExceptionState::GetMyThread() -{ - return (Thread*)(((BYTE*)this) - offsetof(Thread, m_ExceptionState)); -} - - OBJECTREF ThreadExceptionState::GetThrowable() { CONTRACTL @@ -64,71 +58,26 @@ OBJECTREF ThreadExceptionState::GetThrowable() } CONTRACTL_END; - if (m_pCurrentTracker && m_pCurrentTracker->m_hThrowable) + if (m_pCurrentTracker) { - return ObjectFromHandle(m_pCurrentTracker->m_hThrowable); + return m_pCurrentTracker->m_exception; } return NULL; } -void ThreadExceptionState::SetThrowable(OBJECTREF throwable DEBUG_ARG(SetThrowableErrorChecking stecFlags)) +BOOL ThreadExceptionState::IsThrowableNull() { - CONTRACTL - { - if ((throwable == NULL) || CLRException::IsPreallocatedExceptionObject(throwable)) NOTHROW; else THROWS; // From CreateHandle - GC_NOTRIGGER; - if (throwable == NULL) MODE_ANY; else MODE_COOPERATIVE; - } - CONTRACTL_END; - - if (m_pCurrentTracker) - { - m_pCurrentTracker->DestroyExceptionHandle(); - } - - if (throwable != NULL) - { - // Non-compliant exceptions are always wrapped. - // The use of the ExceptionNative:: helper here (rather than the global ::IsException helper) - // is hokey, but we need a GC_NOTRIGGER version and it's only for an ASSERT. - _ASSERTE(IsException(throwable->GetMethodTable())); + LIMITED_METHOD_DAC_CONTRACT; - OBJECTHANDLE hNewThrowable; + if (m_pCurrentTracker == NULL) + return TRUE; - // If we're tracking one of the preallocated exception objects, then just use the global handle that - // matches it rather than creating a new one. - if (CLRException::IsPreallocatedExceptionObject(throwable)) - { - hNewThrowable = CLRException::GetPreallocatedHandleForObject(throwable); - } - else - { - AppDomain* pDomain = AppDomain::GetCurrentDomain(); - _ASSERTE(pDomain != NULL); - hNewThrowable = pDomain->CreateHandle(throwable); - } + return m_pCurrentTracker->m_exception == NULL; +} -#ifdef _DEBUG - // - // Fatal stack overflow policy ends up short-circuiting the normal exception handling - // flow such that there could be no Tracker for this SO that is in flight. In this - // situation there is no place to store the throwable in the exception state, and instead - // it is presumed that the handle to the SO exception is elsewhere. (Current knowledge - // as of 7/15/05 is that it is stored in Thread::m_LastThrownObjectHandle; - // - if (stecFlags != STEC_CurrentTrackerEqualNullOkHackForFatalStackOverflow) - { - CONSISTENCY_CHECK(CheckPointer(m_pCurrentTracker)); - } -#endif +#ifndef DACCESS_COMPILE - if (m_pCurrentTracker != NULL) - { - m_pCurrentTracker->m_hThrowable = hNewThrowable; - } - } -} DWORD ThreadExceptionState::GetExceptionCode() { diff --git a/src/coreclr/vm/exstate.h b/src/coreclr/vm/exstate.h index 86a838d70309be..690ee6237196f8 100644 --- a/src/coreclr/vm/exstate.h +++ b/src/coreclr/vm/exstate.h @@ -50,17 +50,9 @@ class ThreadExceptionState public: -#ifdef _DEBUG - typedef enum - { - STEC_All, - STEC_CurrentTrackerEqualNullOkHackForFatalStackOverflow, - } SetThrowableErrorChecking; -#endif - - void SetThrowable(OBJECTREF throwable DEBUG_ARG(SetThrowableErrorChecking stecFlags = STEC_All)); OBJECTREF GetThrowable(); - OBJECTHANDLE GetThrowableAsHandle(); + OBJECTHANDLE GetThrowableAsPseudoHandle(); + BOOL IsThrowableNull(); DWORD GetExceptionCode(); BOOL IsComPlusException(); EXCEPTION_POINTERS* GetExceptionPointers(); @@ -123,8 +115,6 @@ class ThreadExceptionState } private: - Thread* GetMyThread(); - PTR_ExInfo m_pCurrentTracker; public: PTR_ExInfo GetCurrentExceptionTracker() diff --git a/src/coreclr/vm/gcenv.ee.cpp b/src/coreclr/vm/gcenv.ee.cpp index 6e7d047e6aa2a0..f1b400afbbac6c 100644 --- a/src/coreclr/vm/gcenv.ee.cpp +++ b/src/coreclr/vm/gcenv.ee.cpp @@ -13,6 +13,7 @@ #include "../gc/env/gcenv.ee.h" #include "threadsuspend.h" #include "interoplibinterface.h" +#include "exinfo.h" #ifdef FEATURE_COMINTEROP #include "runtimecallablewrapper.h" @@ -204,6 +205,19 @@ static void ScanStackRoots(Thread * pThread, promote_func* fn, ScanContext* sc) pGCFrame->GcScanRoots(fn, sc); pGCFrame = pGCFrame->PtrNextFrame(); } + + // Scan the ExInfo chain for exception objects held by direct pointer. + // Superseded ExInfo objects may live in logically dead parts of the stack + // that the normal GC stackwalk skips (e.g., when one exception dispatch + // supersedes a previous one). We keep them alive for post-mortem debugging + // and SOS. This mirrors NativeAOT's GcScanRootsWorker (thread.cpp:569-573). + PTR_ExInfo pExInfo = pThread->GetExceptionState()->GetCurrentExceptionTracker(); + while (pExInfo != NULL) + { + PTR_PTR_Object pRef = dac_cast(&pExInfo->m_exception); + fn(pRef, sc, 0); + pExInfo = pExInfo->GetPreviousExceptionTracker(); + } } static void ScanTailCallArgBufferRoots(Thread* pThread, promote_func* fn, ScanContext* sc) diff --git a/src/coreclr/vm/interoplibinterface.h b/src/coreclr/vm/interoplibinterface.h index 4ab07ddf5979a8..e7d8ea3e209fbb 100644 --- a/src/coreclr/vm/interoplibinterface.h +++ b/src/coreclr/vm/interoplibinterface.h @@ -43,7 +43,7 @@ class ObjCMarshalNative public: // Exceptions static void* GetPropagatingExceptionCallback( _In_ EECodeInfo* codeInfo, - _In_ OBJECTHANDLE throwable, + _In_ OBJECTREF throwable, _Outptr_ void** context); public: // GC interaction @@ -105,7 +105,7 @@ class Interop static ManagedToNativeExceptionCallback GetPropagatingExceptionCallback( _In_ EECodeInfo* codeInfo, - _In_ OBJECTHANDLE throwable, + _In_ OBJECTREF throwable, _Outptr_ void** context); // Notify started/finished when GC is running. diff --git a/src/coreclr/vm/interoplibinterface_objc.cpp b/src/coreclr/vm/interoplibinterface_objc.cpp index d44165731bb407..9fb23c76451ad1 100644 --- a/src/coreclr/vm/interoplibinterface_objc.cpp +++ b/src/coreclr/vm/interoplibinterface_objc.cpp @@ -271,15 +271,15 @@ namespace void* ObjCMarshalNative::GetPropagatingExceptionCallback( _In_ EECodeInfo* codeInfo, - _In_ OBJECTHANDLE throwable, + _In_ OBJECTREF throwableRef, _Outptr_ void** context) { CONTRACT(void*) { THROWS; - MODE_PREEMPTIVE; + MODE_COOPERATIVE; PRECONDITION(codeInfo != NULL); - PRECONDITION(throwable != NULL); + PRECONDITION(throwableRef != NULL); PRECONDITION(context != NULL); } CONTRACT_END; @@ -301,11 +301,8 @@ void* ObjCMarshalNative::GetPropagatingExceptionCallback( } { - GCX_COOP(); - OBJECTREF throwableRef = NULL; GCPROTECT_BEGIN(throwableRef); - throwableRef = ObjectFromHandle(throwable); callback = CallInvokeUnhandledExceptionPropagation( &throwableRef, method, diff --git a/src/coreclr/vm/interoplibinterface_shared.cpp b/src/coreclr/vm/interoplibinterface_shared.cpp index 5cd3378ff66b72..eb20a641b4b090 100644 --- a/src/coreclr/vm/interoplibinterface_shared.cpp +++ b/src/coreclr/vm/interoplibinterface_shared.cpp @@ -34,13 +34,13 @@ bool Interop::ShouldCheckForPendingException(_In_ PInvokeMethodDesc* md) ManagedToNativeExceptionCallback Interop::GetPropagatingExceptionCallback( _In_ EECodeInfo* codeInfo, - _In_ OBJECTHANDLE throwable, + _In_ OBJECTREF throwable, _Outptr_ void** context) { CONTRACT(ManagedToNativeExceptionCallback) { NOTHROW; - MODE_PREEMPTIVE; + MODE_COOPERATIVE; PRECONDITION(codeInfo != NULL); PRECONDITION(throwable != NULL); PRECONDITION(context != NULL); diff --git a/src/coreclr/vm/prestub.cpp b/src/coreclr/vm/prestub.cpp index c5b01c62128233..d5e7ee2d2893b9 100644 --- a/src/coreclr/vm/prestub.cpp +++ b/src/coreclr/vm/prestub.cpp @@ -1935,7 +1935,7 @@ extern "C" PCODE STDCALL PreStubWorker(TransitionBlock* pTransitionBlock, Method { OBJECTHANDLE ohThrowable = CURRENT_THREAD->LastThrownObjectHandle(); _ASSERTE(ohThrowable); - StackTraceInfo::AppendElement(ohThrowable, 0, (UINT_PTR)pTransitionBlock, pMD, NULL); + StackTraceInfo::AppendElement(ObjectFromHandle(ohThrowable), 0, (UINT_PTR)pTransitionBlock, pMD, NULL); EX_RETHROW; } EX_END_CATCH @@ -2140,7 +2140,7 @@ void ExecuteInterpretedMethodWithArgs_PortableEntryPoint_Complex(PCODE portableE _ASSERTE(ohThrowable); if (finishedPrestubPortion) { - StackTraceInfo::AppendElement(ohThrowable, 0, (UINT_PTR)block, pMethod, NULL); + StackTraceInfo::AppendElement(ObjectFromHandle(ohThrowable), 0, (UINT_PTR)block, pMethod, NULL); } EX_RETHROW; } diff --git a/src/coreclr/vm/threads.cpp b/src/coreclr/vm/threads.cpp index ae61f430add3de..5e1f114f56d5a4 100644 --- a/src/coreclr/vm/threads.cpp +++ b/src/coreclr/vm/threads.cpp @@ -3221,12 +3221,12 @@ OBJECTREF Thread::SafeSetLastThrownObject(OBJECTREF throwable) } // -// This is a nice wrapper for SetThrowable and SetLastThrownObject, which catches any exceptions caused by not -// being able to create the handle for the throwable, and sets the throwable to the preallocated out of memory -// exception instead. It also updates the last thrown object, which is always updated when the throwable is -// updated. +// This is a nice wrapper for updating the last thrown object handle, which catches any exceptions caused by not +// being able to create the handle for the throwable, and falls back to the preallocated out of memory exception +// for the last thrown object instead. The throwable itself is stored directly in ExInfo::m_exception by managed +// EH code, so this helper only updates the last thrown object state. // -OBJECTREF Thread::SafeSetThrowables(OBJECTREF throwable DEBUG_ARG(ThreadExceptionState::SetThrowableErrorChecking stecFlags), +OBJECTREF Thread::SafeSetThrowables(OBJECTREF throwable, BOOL isUnhandled) { CONTRACTL @@ -3242,11 +3242,8 @@ OBJECTREF Thread::SafeSetThrowables(OBJECTREF throwable DEBUG_ARG(ThreadExceptio EX_TRY { - // Try to set the throwable. - SetThrowable(throwable DEBUG_ARG(stecFlags)); - - // Now, if the last thrown object is different, go ahead and update it. This makes sure that we re-throw - // the right object when we rethrow. + // The exception object is stored directly in ExInfo::m_exception by managed EH code, + // so we only need to update the last thrown object handle here. if (LastThrownObject() != throwable) { SetLastThrownObject(throwable); @@ -3259,12 +3256,9 @@ OBJECTREF Thread::SafeSetThrowables(OBJECTREF throwable DEBUG_ARG(ThreadExceptio } EX_CATCH { - // If either set didn't work, then set both throwables to the preallocated OOM exception, and return that - // object instead of the original throwable. + // If we can't create a handle, set the last thrown object to the preallocated OOM exception. ret = CLRException::GetPreallocatedOutOfMemoryException(); - // Neither of these will throw because we're setting with a preallocated exception. - SetThrowable(ret DEBUG_ARG(stecFlags)); SetLastThrownObject(ret, isUnhandled); } EX_END_CATCH @@ -3326,22 +3320,17 @@ void Thread::SafeUpdateLastThrownObject(void) } CONTRACTL_END; - OBJECTHANDLE hThrowable = GetThrowableAsHandle(); + OBJECTREF throwable = GetExceptionState()->GetThrowable(); - if (hThrowable != NULL) + if (throwable != NULL) { EX_TRY { - IGCHandleManager *pHandleTable = GCHandleUtilities::GetGCHandleManager(); - - // Creating a duplicate handle here ensures that the AD of the last thrown object - // matches the domain of the current throwable. - OBJECTHANDLE duplicateHandle = pHandleTable->CreateDuplicateHandle(hThrowable); - SetLastThrownObjectHandle(duplicateHandle); + SetLastThrownObject(throwable); } EX_CATCH { - // If we can't create a duplicate handle, we set both throwables to the preallocated OOM exception. + // If we can't create a handle, set the last thrown object to the preallocated OOM exception. SafeSetThrowables(CLRException::GetPreallocatedOutOfMemoryException()); } EX_END_CATCH @@ -6703,7 +6692,7 @@ extern "C" InterpThreadContext* STDCALL GetInterpThreadContextWithPossiblyMissin { OBJECTHANDLE ohThrowable = CURRENT_THREAD->LastThrownObjectHandle(); _ASSERTE(ohThrowable); - StackTraceInfo::AppendElement(ohThrowable, 0, (UINT_PTR)pTransitionBlock, pByteCodeStart->Method->methodHnd, NULL); + StackTraceInfo::AppendElement(ObjectFromHandle(ohThrowable), 0, (UINT_PTR)pTransitionBlock, pByteCodeStart->Method->methodHnd, NULL); EX_RETHROW; } EX_END_CATCH diff --git a/src/coreclr/vm/threads.h b/src/coreclr/vm/threads.h index 18aad8dcd2c5e3..195a01434d8ed3 100644 --- a/src/coreclr/vm/threads.h +++ b/src/coreclr/vm/threads.h @@ -1433,8 +1433,6 @@ class Thread //--------------------------------------------------------------- // Last exception to be thrown //--------------------------------------------------------------- - inline void SetThrowable(OBJECTREF pThrowable - DEBUG_ARG(ThreadExceptionState::SetThrowableErrorChecking stecFlags = ThreadExceptionState::STEC_All)); OBJECTREF GetThrowable() { @@ -1443,25 +1441,25 @@ class Thread return m_ExceptionState.GetThrowable(); } - // An unmnaged thread can check if a managed is processing an exception BOOL HasException() { LIMITED_METHOD_CONTRACT; - OBJECTHANDLE pThrowable = m_ExceptionState.GetThrowableAsHandle(); - return pThrowable && *PTR_UNCHECKED_OBJECTREF(pThrowable); + return !IsThrowableNull(); } - OBJECTHANDLE GetThrowableAsHandle() + // See ExInfo::GetThrowableAsPseudoHandle for details on the pseudo-handle. + OBJECTHANDLE GetThrowableAsPseudoHandle() { - LIMITED_METHOD_CONTRACT; - return m_ExceptionState.GetThrowableAsHandle(); + LIMITED_METHOD_DAC_CONTRACT; + + return m_ExceptionState.GetThrowableAsPseudoHandle(); } // special null test (for use when we're in the wrong GC mode) BOOL IsThrowableNull() { WRAPPER_NO_CONTRACT; - return IsHandleNullUnchecked(m_ExceptionState.GetThrowableAsHandle()); + return m_ExceptionState.IsThrowableNull(); } BOOL IsExceptionInProgress() @@ -2674,8 +2672,7 @@ class Thread } void SafeUpdateLastThrownObject(void); - OBJECTREF SafeSetThrowables(OBJECTREF pThrowable - DEBUG_ARG(ThreadExceptionState::SetThrowableErrorChecking stecFlags = ThreadExceptionState::STEC_All), + OBJECTREF SafeSetThrowables(OBJECTREF pThrowable, BOOL isUnhandled = FALSE); bool IsLastThrownObjectStackOverflowException() diff --git a/src/coreclr/vm/threads.inl b/src/coreclr/vm/threads.inl index ada63f848853fa..554436b6847f0b 100644 --- a/src/coreclr/vm/threads.inl +++ b/src/coreclr/vm/threads.inl @@ -65,13 +65,6 @@ Frame* Thread::FindFrame(SIZE_T StackPointer) return pFrame; } -inline void Thread::SetThrowable(OBJECTREF pThrowable DEBUG_ARG(ThreadExceptionState::SetThrowableErrorChecking stecFlags)) -{ - WRAPPER_NO_CONTRACT; - - m_ExceptionState.SetThrowable(pThrowable DEBUG_ARG(stecFlags)); -} - // get the current notification (if any) from this thread inline OBJECTHANDLE Thread::GetThreadCurrNotification() { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Exception_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Exception_1.cs index a6911cbad06186..e1adc9b59f5b38 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Exception_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Exception_1.cs @@ -18,8 +18,14 @@ TargetPointer IException.GetNestedExceptionInfo(TargetPointer exceptionInfoAddr, { Data.ExceptionInfo exceptionInfo = _target.ProcessedData.GetOrAdd(exceptionInfoAddr); nextNestedExceptionInfo = exceptionInfo.PreviousNestedInfo; - thrownObjectHandle = exceptionInfo.ThrownObjectHandle.Handle; - return exceptionInfo.ThrownObjectHandle.Object; + // ThrownObject is a direct object pointer stored in ExInfo::m_exception. + // Return the address of the field as a "handle" - reading through it yields the + // exception Object*. This has the same lifetime as the ExInfo (both are invalidated + // when PopExInfos calls ReleaseResources). See dacimpl.h for the equivalent native + // DAC documentation. + Target.TypeInfo type = _target.GetTypeInfo(DataType.ExceptionInfo); + thrownObjectHandle = exceptionInfoAddr + (ulong)type.Fields[nameof(Data.ExceptionInfo.ThrownObject)].Offset; + return exceptionInfo.ThrownObject; } ExceptionData IException.GetExceptionData(TargetPointer exceptionAddr) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs index 7f06982a505985..289ed53eca9374 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs @@ -93,7 +93,7 @@ ThreadData IThread.GetThreadData(TargetPointer threadPointer) Data.ExceptionInfo exceptionInfo = _target.ProcessedData.GetOrAdd(address); firstNestedException = exceptionInfo.PreviousNestedInfo; - if (exceptionInfo.ThrownObjectHandle.Handle != TargetPointer.Null) + if (exceptionInfo.ThrownObject != TargetPointer.Null) { uint exceptionFlags = exceptionInfo.ExceptionFlags; hasUnhandledException = (exceptionFlags & (uint)ExceptionFlags.IsUnhandled) != 0 @@ -210,37 +210,37 @@ TargetPointer IThread.GetThreadLocalStaticBase(TargetPointer threadPointer, Targ return threadLocalStaticBase; } - private (Data.Thread thread, Data.ExceptionInfo? exceptionInfo) GetThreadExceptionInfo(TargetPointer threadPointer) + private (Data.Thread thread, Data.ExceptionInfo? exceptionInfo, TargetPointer exceptionTrackerAddr) GetThreadExceptionInfo(TargetPointer threadPointer) { Data.Thread thread = _target.ProcessedData.GetOrAdd(threadPointer); TargetPointer exceptionTrackerPtr = _target.ReadPointer(thread.ExceptionTracker); Data.ExceptionInfo? exceptionInfo = (exceptionTrackerPtr == TargetPointer.Null) ? null : _target.ProcessedData.GetOrAdd(exceptionTrackerPtr); - return (thread, exceptionInfo); + return (thread, exceptionInfo, exceptionTrackerPtr); } TargetPointer IThread.GetCurrentExceptionHandle(TargetPointer threadPointer) { - var (_, exceptionInfo) = GetThreadExceptionInfo(threadPointer); + var (_, exceptionInfo, exceptionTrackerAddr) = GetThreadExceptionInfo(threadPointer); - if (exceptionInfo == null) - return TargetPointer.Null; - - if (exceptionInfo.ThrownObjectHandle.Handle == TargetPointer.Null || exceptionInfo.ThrownObjectHandle.Object == TargetPointer.Null) + if (exceptionInfo is null || exceptionInfo.ThrownObject == TargetPointer.Null) return TargetPointer.Null; - return exceptionInfo.ThrownObjectHandle.Handle; + // Return the target address of the ThrownObject field as a pseudo-handle. + // Callers dereference this address to read the exception Object*. + Target.TypeInfo type = _target.GetTypeInfo(DataType.ExceptionInfo); + return exceptionTrackerAddr + (ulong)type.Fields[nameof(Data.ExceptionInfo.ThrownObject)].Offset; } byte[] IThread.GetWatsonBuckets(TargetPointer threadPointer) { TargetPointer readFrom; - var (thread, exceptionInfo) = GetThreadExceptionInfo(threadPointer); + var (thread, exceptionInfo, _) = GetThreadExceptionInfo(threadPointer); if (exceptionInfo == null) return Array.Empty(); - Data.ObjectHandle throwableObject = exceptionInfo.ThrownObjectHandle; - if (throwableObject.Object != TargetPointer.Null) + TargetPointer thrownObject = exceptionInfo.ThrownObject; + if (thrownObject != TargetPointer.Null) { - Data.Exception exception = _target.ProcessedData.GetOrAdd(throwableObject.Object); + Data.Exception exception = _target.ProcessedData.GetOrAdd(thrownObject); if (exception.WatsonBuckets != TargetPointer.Null) { readFrom = _target.Contracts.Object.GetArrayData(exception.WatsonBuckets, out _, out _, out _); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs index d582523b5159ec..e8d5095dd10c01 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs @@ -13,7 +13,7 @@ public ExceptionInfo(Target target, TargetPointer address) Target.TypeInfo type = target.GetTypeInfo(DataType.ExceptionInfo); PreviousNestedInfo = target.ReadPointerField(address, type, nameof(PreviousNestedInfo)); - ThrownObjectHandle = target.ReadDataField(address, type, nameof(ThrownObjectHandle)); + ThrownObject = target.ReadPointerField(address, type, nameof(ThrownObject)); if (type.Fields.ContainsKey(nameof(ExceptionWatsonBucketTrackerBuckets))) ExceptionWatsonBucketTrackerBuckets = target.ReadPointerField(address, type, nameof(ExceptionWatsonBucketTrackerBuckets)); ExceptionFlags = target.ReadField(address, type, nameof(ExceptionFlags)); @@ -28,7 +28,7 @@ public ExceptionInfo(Target target, TargetPointer address) } public TargetPointer PreviousNestedInfo { get; } - public ObjectHandle ThrownObjectHandle { get; } + public TargetPointer ThrownObject { get; } public uint ExceptionFlags { get; } public TargetPointer StackLowBound { get; } public TargetPointer StackHighBound { get; } diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs index bf7603a9966c2c..bc3a49679fb3b6 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs @@ -8,7 +8,7 @@ namespace Microsoft.Diagnostics.DataContractReader.Tests; internal sealed class MockExceptionInfo : TypedView { private const string PreviousNestedInfoFieldName = "PreviousNestedInfo"; - private const string ThrownObjectHandleFieldName = "ThrownObjectHandle"; + private const string ThrownObjectFieldName = "ThrownObject"; private const string ExceptionFlagsFieldName = "ExceptionFlags"; private const string StackLowBoundFieldName = "StackLowBound"; private const string StackHighBoundFieldName = "StackHighBound"; @@ -23,7 +23,7 @@ internal sealed class MockExceptionInfo : TypedView public static Layout CreateLayout(MockTarget.Architecture architecture) => new SequentialLayoutBuilder("ExceptionInfo", architecture) .AddPointerField(PreviousNestedInfoFieldName) - .AddPointerField(ThrownObjectHandleFieldName) + .AddPointerField(ThrownObjectFieldName) .AddUInt32Field(ExceptionFlagsFieldName) .AddPointerField(StackLowBoundFieldName) .AddPointerField(StackHighBoundFieldName) @@ -36,10 +36,10 @@ public static Layout CreateLayout(MockTarget.Architecture arc .AddUInt32Field(ClauseForCatchHandlerEndPCFieldName) .Build(); - public ulong ThrownObjectHandle + public ulong ThrownObject { - get => ReadPointerField(ThrownObjectHandleFieldName); - set => WritePointerField(ThrownObjectHandleFieldName, value); + get => ReadPointerField(ThrownObjectFieldName); + set => WritePointerField(ThrownObjectFieldName, value); } } diff --git a/src/native/managed/cdac/tests/ThreadTests.cs b/src/native/managed/cdac/tests/ThreadTests.cs index d314c9f5cf2432..c9b3d9bd30c2bf 100644 --- a/src/native/managed/cdac/tests/ThreadTests.cs +++ b/src/native/managed/cdac/tests/ThreadTests.cs @@ -221,16 +221,14 @@ public void GetCurrentExceptionHandle_WithException(MockTarget.Architecture arch { thread = threadBuilder.AddThread(1, 1234); exceptionInfo = threadBuilder.GetExceptionInfo(thread); - TargetTestHelpers helpers = threadBuilder.Builder.TargetTestHelpers; - MockMemorySpace.BumpAllocator allocator = threadBuilder.Builder.CreateAllocator(0x1_0000, 0x2_0000); - MockMemorySpace.HeapFragment handleFragment = allocator.Allocate((ulong)helpers.PointerSize, "ThrownObjectHandle"); - helpers.WritePointer(handleFragment.Data, expectedObject); - exceptionInfo!.ThrownObjectHandle = handleFragment.Address; + exceptionInfo!.ThrownObject = (ulong)expectedObject; }); IThread contract = target.Contracts.Thread; TargetPointer thrownObjectHandle = contract.GetCurrentExceptionHandle(new TargetPointer(thread!.Address)); - Assert.Equal(new TargetPointer(exceptionInfo!.ThrownObjectHandle), thrownObjectHandle); + // The handle is the address of the ThrownObject field - reading through it gives the object pointer + Assert.NotEqual(TargetPointer.Null, thrownObjectHandle); + Assert.Equal(expectedObject, target.ReadPointer(thrownObjectHandle)); } [Theory] @@ -265,11 +263,7 @@ public void GetCurrentExceptionHandle_HandlePointsToNull(MockTarget.Architecture { thread = threadBuilder.AddThread(1, 1234); exceptionInfo = threadBuilder.GetExceptionInfo(thread); - TargetTestHelpers helpers = threadBuilder.Builder.TargetTestHelpers; - MockMemorySpace.BumpAllocator allocator = threadBuilder.Builder.CreateAllocator(0x1_0000, 0x2_0000); - MockMemorySpace.HeapFragment handleFragment = allocator.Allocate((ulong)helpers.PointerSize, "ThrownObjectHandle"); - helpers.WritePointer(handleFragment.Data, TargetPointer.Null); - exceptionInfo!.ThrownObjectHandle = handleFragment.Address; + exceptionInfo!.ThrownObject = 0; }); IThread contract = target.Contracts.Thread; From 03ab907aca5b49ae9a0778cfb81cda1b13327334 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 09:07:26 -0700 Subject: [PATCH 065/115] Include requesting assembly chain in assembly load failure exceptions (#125795) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request enhances .NET exception diagnostics for assembly loading failures by tracking and surfacing the chain of requesting assemblies when a dependency is missing or invalid. This helps developers more easily identify which assemblies triggered a load failure, especially in complex dependency scenarios. **Exception information:** * The constructors for `BadImageFormatException`, `FileLoadException`, and `FileNotFoundException` now accept an additional `requestingAssemblyChain` parameter, which records the chain of assemblies that led to the load failure. This chain is stored and included in the exception’s string representation. * The `ToString()` methods of these exceptions now append a "Requested by: ..." message listing the requesting assembly chain, improving error clarity for users. * A new resource string, `IO_FileLoad_RequestedBy`, is added for this message. **Runtime and binding cache:** * The runtime now allows querying the parent (requesting) assembly for an assembly in the `AssemblySpecBindingCache`. New methods allow walking the chain of requesting assemblies up to a depth of 10, building a diagnostic chain for exceptions. * The `EEFileLoadException` and related logic are updated to capture and propagate the requesting assembly chain, including in exception cloning and when thrown from the runtime. * The unmanaged-to-managed exception creation path is updated to pass the requesting assembly chain to managed exception constructors. * The chain is a best-effort representation. An assembly can be requested by multiple different parents. The message uses the parent of the first matching cached assembly found during iteration. **Testing and validation:** * Adds new test assemblies (`MissingDependency.Leaf`, `MissingDependency.Mid`) and a test in `AssemblyLoadContextTest` to verify that the requesting assembly chain appears in exception messages when a transitive dependency is missing. These changes significantly improve the developer experience when diagnosing assembly loading failures by providing more actionable information in exception messages. Example: ``` System.IO.FileNotFoundException: Could not load file or assembly 'LibC, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified. File name: 'LibC, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' Requested by: LibB, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null --> LibA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null --> helloworld, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null --> System.Private.CoreLib, Version=11.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e at LibB.LibBClass.GetMessage() in C:\repos\helloworld\LibB\Class1.cs:line 5 at LibB.LibBClass.GetMessage() in C:\repos\helloworld\LibB\Class1.cs:line 5 at LibA.LibAClass.GetMessage() in C:\repos\helloworld\LibA\Class1.cs:line 5 at Program.Main(String[] args) in C:\repos\helloworld\Program.cs:line 329 ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: elinor-fung <47805090+elinor-fung@users.noreply.github.com> Co-authored-by: Elinor Fung Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Aaron R Robinson --- .../System/BadImageFormatException.CoreCLR.cs | 3 +- .../System/IO/FileLoadException.CoreCLR.cs | 12 +++-- .../IO/FileNotFoundException.CoreCLR.cs | 3 +- src/coreclr/vm/appdomain.cpp | 34 ++++++++++++++ src/coreclr/vm/appdomain.hpp | 2 + src/coreclr/vm/assemblyspec.cpp | 36 +++++++++++++++ src/coreclr/vm/assemblyspec.hpp | 3 ++ src/coreclr/vm/clrex.cpp | 46 +++++++++++++++++-- src/coreclr/vm/clrex.h | 11 ++++- src/coreclr/vm/wasm/callhelpers-reverse.cpp | 14 +++--- .../src/Resources/Strings.resx | 3 ++ .../src/System/BadImageFormatException.cs | 6 +++ .../src/System/IO/FileLoadException.cs | 6 +++ .../src/System/IO/FileNotFoundException.cs | 6 +++ .../tests/AssemblyLoadContextTest.cs | 19 ++++++++ .../tests/MissingDependency.Leaf/LeafClass.cs | 10 ++++ .../MissingDependency.Leaf.csproj | 8 ++++ .../tests/MissingDependency.Mid/MidClass.cs | 17 +++++++ .../MissingDependency.Mid.csproj | 13 ++++++ .../MissingDependency.Root.csproj | 11 +++++ .../tests/MissingDependency.Root/RootClass.cs | 17 +++++++ .../tests/System.Runtime.Loader.Tests.csproj | 1 + 22 files changed, 262 insertions(+), 19 deletions(-) create mode 100644 src/libraries/System.Runtime.Loader/tests/MissingDependency.Leaf/LeafClass.cs create mode 100644 src/libraries/System.Runtime.Loader/tests/MissingDependency.Leaf/MissingDependency.Leaf.csproj create mode 100644 src/libraries/System.Runtime.Loader/tests/MissingDependency.Mid/MidClass.cs create mode 100644 src/libraries/System.Runtime.Loader/tests/MissingDependency.Mid/MissingDependency.Mid.csproj create mode 100644 src/libraries/System.Runtime.Loader/tests/MissingDependency.Root/MissingDependency.Root.csproj create mode 100644 src/libraries/System.Runtime.Loader/tests/MissingDependency.Root/RootClass.cs diff --git a/src/coreclr/System.Private.CoreLib/src/System/BadImageFormatException.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/BadImageFormatException.CoreCLR.cs index 88a951412d6a05..920efedf8b1e54 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/BadImageFormatException.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/BadImageFormatException.CoreCLR.cs @@ -5,11 +5,12 @@ namespace System { public partial class BadImageFormatException { - internal BadImageFormatException(string? fileName, int hResult) + internal BadImageFormatException(string? fileName, string? requestingAssemblyChain, int hResult) : base(null) { HResult = hResult; _fileName = fileName; + _requestingAssemblyChain = requestingAssemblyChain; SetMessageField(); } } diff --git a/src/coreclr/System.Private.CoreLib/src/System/IO/FileLoadException.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/IO/FileLoadException.CoreCLR.cs index 927e08905606eb..15a0fd60c01852 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/IO/FileLoadException.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/IO/FileLoadException.CoreCLR.cs @@ -9,11 +9,12 @@ namespace System.IO { public partial class FileLoadException { - private FileLoadException(string? fileName, int hResult) + private FileLoadException(string? fileName, string? requestingAssemblyChain, int hResult) : base(null) { HResult = hResult; FileName = fileName; + _requestingAssemblyChain = requestingAssemblyChain; _message = FormatFileLoadExceptionMessage(FileName, HResult); } @@ -47,18 +48,19 @@ internal enum FileLoadExceptionKind } [UnmanagedCallersOnly] - internal static unsafe void Create(FileLoadExceptionKind kind, char* pFileName, int hresult, object* pThrowable, Exception* pException) + internal static unsafe void Create(FileLoadExceptionKind kind, char* pFileName, char* pRequestingAssemblyChain, int hresult, object* pThrowable, Exception* pException) { try { string? fileName = pFileName is not null ? new string(pFileName) : null; + string? requestingAssemblyChain = pRequestingAssemblyChain is not null ? new string(pRequestingAssemblyChain) : null; Debug.Assert(Enum.IsDefined(kind)); *pThrowable = kind switch { - FileLoadExceptionKind.BadImageFormat => new BadImageFormatException(fileName, hresult), - FileLoadExceptionKind.FileNotFound => new FileNotFoundException(fileName, hresult), + FileLoadExceptionKind.BadImageFormat => new BadImageFormatException(fileName, requestingAssemblyChain, hresult), + FileLoadExceptionKind.FileNotFound => new FileNotFoundException(fileName, requestingAssemblyChain, hresult), FileLoadExceptionKind.OutOfMemory => new OutOfMemoryException(), - _ /* FileLoadExceptionKind.FileLoad */ => new FileLoadException(fileName, hresult), + _ /* FileLoadExceptionKind.FileLoad */ => new FileLoadException(fileName, requestingAssemblyChain, hresult), }; } catch (Exception ex) diff --git a/src/coreclr/System.Private.CoreLib/src/System/IO/FileNotFoundException.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/IO/FileNotFoundException.CoreCLR.cs index 15d54ec5b367c8..2ce6d85af1e22b 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/IO/FileNotFoundException.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/IO/FileNotFoundException.CoreCLR.cs @@ -5,11 +5,12 @@ namespace System.IO { public partial class FileNotFoundException { - internal FileNotFoundException(string? fileName, int hResult) + internal FileNotFoundException(string? fileName, string? requestingAssemblyChain, int hResult) : base(null) { HResult = hResult; FileName = fileName; + _requestingAssemblyChain = requestingAssemblyChain; SetMessageField(); } } diff --git a/src/coreclr/vm/appdomain.cpp b/src/coreclr/vm/appdomain.cpp index 488e407303fd85..5a902c22ebafd3 100644 --- a/src/coreclr/vm/appdomain.cpp +++ b/src/coreclr/vm/appdomain.cpp @@ -3029,6 +3029,40 @@ BOOL AppDomain::IsCached(AssemblySpec *pSpec) return m_AssemblyCache.Contains(pSpec); } +void AppDomain::GetParentAssemblyChain(Assembly *pStartAssembly, SString &chain, int maxDepth) +{ + STANDARD_VM_CONTRACT; + + // Hold the lock for the entire chain build so that all Assembly* + // from the cache are safe from collectible ALC unload. + GCX_PREEMP(); + DomainCacheCrstHolderForGCCoop lock(this); + + MapSHash parentMap; + m_AssemblyCache.GetParentAssemblyMap(parentMap); + + Assembly *pWalkAssembly = pStartAssembly; + for (int depth = 0; depth < maxDepth && pWalkAssembly != NULL; depth++) + { + Assembly *pParent; + if (!parentMap.Lookup(pWalkAssembly, &pParent)) + break; + + if (pParent == pWalkAssembly) + break; + + StackSString parentName; + pParent->GetDisplayName(parentName); + chain.Append(W("\n --> ")); + chain.Append(parentName); + + if (pParent->IsSystem()) + break; + + pWalkAssembly = pParent; + } +} + PEAssembly* AppDomain::FindCachedFile(AssemblySpec* pSpec, BOOL fThrow /*=TRUE*/) { CONTRACTL diff --git a/src/coreclr/vm/appdomain.hpp b/src/coreclr/vm/appdomain.hpp index e5126e40accd96..08138af6a315ae 100644 --- a/src/coreclr/vm/appdomain.hpp +++ b/src/coreclr/vm/appdomain.hpp @@ -1115,6 +1115,8 @@ class AppDomain final return m_AssemblyCache.LookupAssembly(pSpec, fThrow); } + void GetParentAssemblyChain(Assembly *pStartAssembly, SString &chain, int maxDepth); + private: PEAssembly* FindCachedFile(AssemblySpec* pSpec, BOOL fThrow = TRUE); BOOL IsCached(AssemblySpec *pSpec); diff --git a/src/coreclr/vm/assemblyspec.cpp b/src/coreclr/vm/assemblyspec.cpp index 638ff52d5e458a..2d8f08c524f339 100644 --- a/src/coreclr/vm/assemblyspec.cpp +++ b/src/coreclr/vm/assemblyspec.cpp @@ -703,6 +703,38 @@ PEAssembly *AssemblySpecBindingCache::LookupFile(AssemblySpec *pSpec, BOOL fThro } } +// Caller must hold DomainCacheCrst. +// The binding cache may contain multiple entries for the same assembly +// (bound under different AssemblySpecs), possibly with a different parent. +// The first match found during iteration wins, so the map is best-effort. +void AssemblySpecBindingCache::GetParentAssemblyMap(MapSHash &parentMap) +{ + CONTRACTL + { + THROWS; + GC_NOTRIGGER; + MODE_ANY; + } + CONTRACTL_END; + + PtrHashMap::PtrIterator i = m_map.begin(); + while (!i.end()) + { + AssemblyBinding *b = (AssemblyBinding*) i.GetValue(); + if (!b->IsError()) + { + Assembly *pAssembly = b->GetAssembly(); + Assembly *pParent = b->GetParentAssembly(); + if (pAssembly != NULL && pParent != NULL) + { + if (parentMap.LookupPtr(pAssembly) == NULL) + parentMap.Add(pAssembly, pParent); + } + } + ++i; + } +} + class AssemblyBindingHolder { @@ -1042,6 +1074,10 @@ BOOL AssemblySpecBindingCache::RemoveAssembly(Assembly* pAssembly) result = TRUE; } + else if (entry->GetParentAssembly() == pAssembly) + { + entry->ClearParentAssembly(); + } ++i; } diff --git a/src/coreclr/vm/assemblyspec.hpp b/src/coreclr/vm/assemblyspec.hpp index 8400fdb762626b..9e642a73946bef 100644 --- a/src/coreclr/vm/assemblyspec.hpp +++ b/src/coreclr/vm/assemblyspec.hpp @@ -244,6 +244,8 @@ class AssemblySpecBindingCache inline Assembly* GetAssembly(){ LIMITED_METHOD_CONTRACT; return m_pAssembly; }; inline void SetAssembly(Assembly* pAssembly){ LIMITED_METHOD_CONTRACT; m_pAssembly = pAssembly; }; inline PEAssembly* GetFile(){ LIMITED_METHOD_CONTRACT; return m_pPEAssembly;}; + inline Assembly* GetParentAssembly(){ LIMITED_METHOD_CONTRACT; return m_spec.GetParentAssembly(); }; + inline void ClearParentAssembly(){ LIMITED_METHOD_CONTRACT; m_spec.SetParentAssembly(NULL); }; inline BOOL IsError(){ LIMITED_METHOD_CONTRACT; return (m_exceptionType!=EXTYPE_NONE);}; // bound to the file, but failed later @@ -390,6 +392,7 @@ class AssemblySpecBindingCache Assembly *LookupAssembly(AssemblySpec *pSpec, BOOL fThrow=TRUE); PEAssembly *LookupFile(AssemblySpec *pSpec, BOOL fThrow = TRUE); + void GetParentAssemblyMap(MapSHash &parentMap); BOOL StoreAssembly(AssemblySpec *pSpec, Assembly *pAssembly); BOOL StorePEAssembly(AssemblySpec *pSpec, PEAssembly *pPEAssembly); diff --git a/src/coreclr/vm/clrex.cpp b/src/coreclr/vm/clrex.cpp index 565f8ec633b68e..5ba65aaae28977 100644 --- a/src/coreclr/vm/clrex.cpp +++ b/src/coreclr/vm/clrex.cpp @@ -1587,10 +1587,11 @@ OBJECTREF EEFileLoadException::CreateThrowable() GCPROTECT_BEGIN(gc); LPCWSTR pFileName = m_name.GetUnicode(); + LPCWSTR pRequestingChain = m_requestingAssemblyChain.IsEmpty() ? NULL : m_requestingAssemblyChain.GetUnicode(); UnmanagedCallersOnlyCaller createFileLoadEx(METHOD__FILE_LOAD_EXCEPTION__CREATE); FileLoadExceptionKind kind = GetFileLoadExceptionKind(m_hr); - createFileLoadEx.InvokeThrowing(kind, pFileName, (int)m_hr, &gc.pNewException); + createFileLoadEx.InvokeThrowing(kind, pFileName, pRequestingChain, (int)m_hr, &gc.pNewException); _ASSERTE(gc.pNewException->GetMethodTable() == CoreLibBinder::GetException(m_kind)); GCPROTECT_END(); @@ -1624,8 +1625,6 @@ BOOL EEFileLoadException::CheckType(Exception* ex) // @todo: ideally we would use inner exceptions with these routines -/* static */ - /* static */ void DECLSPEC_NORETURN EEFileLoadException::Throw(AssemblySpec *pSpec, HRESULT hr, Exception *pInnerException/* = NULL*/) { @@ -1644,7 +1643,46 @@ void DECLSPEC_NORETURN EEFileLoadException::Throw(AssemblySpec *pSpec, HRESULT StackSString name; pSpec->GetDisplayName(0, name); - EX_THROW_WITH_INNER(EEFileLoadException, (name, hr), pInnerException); + + // Extract the requesting assembly chain for diagnostic purposes + { + FAULT_NOT_FATAL(); + + Exception *inner2 = ExThrowWithInnerHelper(pInnerException); + EEFileLoadException *pException = new EEFileLoadException(name, hr); + pException->SetInnerException(inner2); + + Assembly *pParentAssembly = pSpec->GetParentAssembly(); + if (pParentAssembly != NULL) + { + StackSString requestingChain; + + EX_TRY + { + // Build the requesting assembly chain: start with the immediate parent, + // then walk up the binding cache to find transitive requesting assemblies. + pParentAssembly->GetDisplayName(requestingChain); + + const int MaxChainDepth = 10; + AppDomain::GetCurrentDomain()->GetParentAssemblyChain( + pParentAssembly, requestingChain, MaxChainDepth); + + pException->SetRequestingAssemblyChain(requestingChain); + } + EX_CATCH + { + // Ignore failures while building best-effort diagnostic data and preserve + // the primary file load exception. + } + EX_END_CATCH + } + + STRESS_LOG3(LF_EH, LL_INFO100, "EX_THROW_WITH_INNER Type = 0x%x HR = 0x%x, " + INDEBUG(__FILE__) " line %d\n", EEFileLoadException::GetType(), + pException->GetHR(), __LINE__); + EX_THROW_DEBUG_TRAP(__FUNCTION__, __FILE__, __LINE__, "EEFileLoadException", pException->GetHR(), "(name, hr)"); + PAL_CPP_THROW(EEFileLoadException *, pException); + } } /* static */ diff --git a/src/coreclr/vm/clrex.h b/src/coreclr/vm/clrex.h index e05e25c615989d..201077790a0a9f 100644 --- a/src/coreclr/vm/clrex.h +++ b/src/coreclr/vm/clrex.h @@ -663,12 +663,19 @@ class EEFileLoadException : public EEException private: SString m_name; HRESULT m_hr; + SString m_requestingAssemblyChain; public: EEFileLoadException(const SString &name, HRESULT hr, Exception *pInnerException = NULL); ~EEFileLoadException(); + void SetRequestingAssemblyChain(const SString &requestingAssemblyChain) + { + WRAPPER_NO_CONTRACT; + m_requestingAssemblyChain = requestingAssemblyChain; + } + // virtual overrides HRESULT GetHR() { @@ -690,7 +697,9 @@ class EEFileLoadException : public EEException virtual Exception *CloneHelper() { WRAPPER_NO_CONTRACT; - return new EEFileLoadException(m_name, m_hr); + EEFileLoadException *pClone = new EEFileLoadException(m_name, m_hr); + pClone->SetRequestingAssemblyChain(m_requestingAssemblyChain); + return pClone; } private: diff --git a/src/coreclr/vm/wasm/callhelpers-reverse.cpp b/src/coreclr/vm/wasm/callhelpers-reverse.cpp index 25ccba61ded632..b61aeb0d7e13fe 100644 --- a/src/coreclr/vm/wasm/callhelpers-reverse.cpp +++ b/src/coreclr/vm/wasm/callhelpers-reverse.cpp @@ -287,17 +287,17 @@ static void Call_System_Private_CoreLib_System_Reflection_LoaderAllocator_Create ExecuteInterpretedMethodFromUnmanaged(MD_System_Private_CoreLib_System_Reflection_LoaderAllocator_Create_I32_I32_RetVoid, (int8_t*)args, sizeof(args), nullptr, (PCODE)&Call_System_Private_CoreLib_System_Reflection_LoaderAllocator_Create_I32_I32_RetVoid); } -static MethodDesc* MD_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_RetVoid = nullptr; -static void Call_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_RetVoid(int32_t arg0, void * arg1, int32_t arg2, void * arg3, void * arg4) +static MethodDesc* MD_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_I32_RetVoid = nullptr; +static void Call_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_I32_RetVoid(int32_t arg0, void * arg1, void * arg2, int32_t arg3, void * arg4, void * arg5) { - int64_t args[5] = { (int64_t)arg0, (int64_t)arg1, (int64_t)arg2, (int64_t)arg3, (int64_t)arg4 }; + int64_t args[6] = { (int64_t)arg0, (int64_t)arg1, (int64_t)arg2, (int64_t)arg3, (int64_t)arg4, (int64_t)arg5 }; // Lazy lookup of MethodDesc for the function export scenario. - if (!MD_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_RetVoid) + if (!MD_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_I32_RetVoid) { - LookupUnmanagedCallersOnlyMethodByName("System.IO.FileLoadException, System.Private.CoreLib", "Create", &MD_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_RetVoid); + LookupUnmanagedCallersOnlyMethodByName("System.IO.FileLoadException, System.Private.CoreLib", "Create", &MD_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_I32_RetVoid); } - ExecuteInterpretedMethodFromUnmanaged(MD_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_RetVoid, (int8_t*)args, sizeof(args), nullptr, (PCODE)&Call_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_RetVoid); + ExecuteInterpretedMethodFromUnmanaged(MD_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_I32_RetVoid, (int8_t*)args, sizeof(args), nullptr, (PCODE)&Call_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_I32_RetVoid); } static MethodDesc* MD_System_Private_CoreLib_System_TypeLoadException_Create_I32_I32_I32_I32_I32_I32_RetVoid = nullptr; @@ -1216,7 +1216,7 @@ const ReverseThunkMapEntry g_ReverseThunks[] = { 4090197812, "ConvertToManaged#3:System.Private.CoreLib:System.StubHelpers:BSTRMarshaler", { &MD_System_Private_CoreLib_System_StubHelpers_BSTRMarshaler_ConvertToManaged_I32_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_StubHelpers_BSTRMarshaler_ConvertToManaged_I32_I32_I32_RetVoid } }, { 1901425681, "ConvertToNative#2:System.Private.CoreLib:System.StubHelpers:BSTRMarshaler", { &MD_System_Private_CoreLib_System_StubHelpers_BSTRMarshaler_ConvertToNative_I32_I32_RetI32, (void*)&Call_System_Private_CoreLib_System_StubHelpers_BSTRMarshaler_ConvertToNative_I32_I32_RetI32 } }, { 1243134822, "Create#2:System.Private.CoreLib:System.Reflection:LoaderAllocator", { &MD_System_Private_CoreLib_System_Reflection_LoaderAllocator_Create_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_Reflection_LoaderAllocator_Create_I32_I32_RetVoid } }, - { 1899576323, "Create#5:System.Private.CoreLib:System.IO:FileLoadException", { &MD_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_RetVoid } }, + { 1447412576, "Create#6:System.Private.CoreLib:System.IO:FileLoadException", { &MD_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_IO_FileLoadException_Create_I32_I32_I32_I32_I32_I32_RetVoid } }, { 1263271190, "Create#6:System.Private.CoreLib:System:TypeLoadException", { &MD_System_Private_CoreLib_System_TypeLoadException_Create_I32_I32_I32_I32_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_TypeLoadException_Create_I32_I32_I32_I32_I32_I32_RetVoid } }, { 509807279, "CreateArgumentException#5:System.Private.CoreLib:System:Exception", { &MD_System_Private_CoreLib_System_Exception_CreateArgumentException_I32_I32_I32_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_Exception_CreateArgumentException_I32_I32_I32_I32_I32_RetVoid } }, { 1570902419, "CreateAssemblyName#3:System.Private.CoreLib:System.Reflection:AssemblyName", { &MD_System_Private_CoreLib_System_Reflection_AssemblyName_CreateAssemblyName_I32_I32_I32_RetVoid, (void*)&Call_System_Private_CoreLib_System_Reflection_AssemblyName_CreateAssemblyName_I32_I32_I32_RetVoid } }, diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index cf37982fbe434e..ac51112ce4dbd1 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -2840,6 +2840,9 @@ Could not load the file '{0}'. + + Requested by: {0} + Directory path: '{0}' diff --git a/src/libraries/System.Private.CoreLib/src/System/BadImageFormatException.cs b/src/libraries/System.Private.CoreLib/src/System/BadImageFormatException.cs index 437c20b9c9ab11..75d26cd5070eed 100644 --- a/src/libraries/System.Private.CoreLib/src/System/BadImageFormatException.cs +++ b/src/libraries/System.Private.CoreLib/src/System/BadImageFormatException.cs @@ -17,6 +17,7 @@ public partial class BadImageFormatException : SystemException { private readonly string? _fileName; // The name of the corrupt PE file. private readonly string? _fusionLog; // fusion log (when applicable) + private readonly string? _requestingAssemblyChain; public BadImageFormatException() : base(SR.Arg_BadImageFormatException) @@ -56,6 +57,7 @@ protected BadImageFormatException(SerializationInfo info, StreamingContext conte { _fileName = info.GetString("BadImageFormat_FileName"); _fusionLog = info.GetString("BadImageFormat_FusionLog"); + _requestingAssemblyChain = (string?)info.GetValueNoThrow("BadImageFormat_RequestingAssemblyChain", typeof(string)); } [Obsolete(Obsoletions.LegacyFormatterImplMessage, DiagnosticId = Obsoletions.LegacyFormatterImplDiagId, UrlFormat = Obsoletions.SharedUrlFormat)] @@ -65,6 +67,7 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont base.GetObjectData(info, context); info.AddValue("BadImageFormat_FileName", _fileName, typeof(string)); info.AddValue("BadImageFormat_FusionLog", _fusionLog, typeof(string)); + info.AddValue("BadImageFormat_RequestingAssemblyChain", _requestingAssemblyChain, typeof(string)); } public override string Message @@ -97,6 +100,9 @@ public override string ToString() if (!string.IsNullOrEmpty(_fileName)) s += Environment.NewLineConst + SR.Format(SR.IO_FileName_Name, _fileName); + if (!string.IsNullOrEmpty(_requestingAssemblyChain)) + s += Environment.NewLineConst + SR.Format(SR.IO_FileLoad_RequestedBy, _requestingAssemblyChain.ReplaceLineEndings()); + if (InnerException != null) s += InnerExceptionPrefix + InnerException.ToString(); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileLoadException.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileLoadException.cs index 2413ce80c2b0a3..040a4bcbc11ad3 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileLoadException.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileLoadException.cs @@ -46,6 +46,7 @@ public FileLoadException(string? message, string? fileName, Exception? inner) public string? FileName { get; } public string? FusionLog { get; } + private readonly string? _requestingAssemblyChain; public override string ToString() { @@ -54,6 +55,9 @@ public override string ToString() if (!string.IsNullOrEmpty(FileName)) s += Environment.NewLineConst + SR.Format(SR.IO_FileName_Name, FileName); + if (!string.IsNullOrEmpty(_requestingAssemblyChain)) + s += Environment.NewLineConst + SR.Format(SR.IO_FileLoad_RequestedBy, _requestingAssemblyChain.ReplaceLineEndings()); + if (InnerException != null) s += Environment.NewLineConst + InnerExceptionPrefix + InnerException.ToString(); @@ -76,6 +80,7 @@ protected FileLoadException(SerializationInfo info, StreamingContext context) { FileName = info.GetString("FileLoad_FileName"); FusionLog = info.GetString("FileLoad_FusionLog"); + _requestingAssemblyChain = (string?)info.GetValueNoThrow("FileLoad_RequestingAssemblyChain", typeof(string)); } [Obsolete(Obsoletions.LegacyFormatterImplMessage, DiagnosticId = Obsoletions.LegacyFormatterImplDiagId, UrlFormat = Obsoletions.SharedUrlFormat)] @@ -85,6 +90,7 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont base.GetObjectData(info, context); info.AddValue("FileLoad_FileName", FileName, typeof(string)); info.AddValue("FileLoad_FusionLog", FusionLog, typeof(string)); + info.AddValue("FileLoad_RequestingAssemblyChain", _requestingAssemblyChain, typeof(string)); } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileNotFoundException.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileNotFoundException.cs index 1175b9c9fcda0b..0cb4bc3b1241c3 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileNotFoundException.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileNotFoundException.cs @@ -69,6 +69,7 @@ private void SetMessageField() public string? FileName { get; } public string? FusionLog { get; } + private readonly string? _requestingAssemblyChain; public override string ToString() { @@ -77,6 +78,9 @@ public override string ToString() if (!string.IsNullOrEmpty(FileName)) s += Environment.NewLineConst + SR.Format(SR.IO_FileName_Name, FileName); + if (!string.IsNullOrEmpty(_requestingAssemblyChain)) + s += Environment.NewLineConst + SR.Format(SR.IO_FileLoad_RequestedBy, _requestingAssemblyChain.ReplaceLineEndings()); + if (InnerException != null) s += Environment.NewLineConst + InnerExceptionPrefix + InnerException.ToString(); @@ -98,6 +102,7 @@ protected FileNotFoundException(SerializationInfo info, StreamingContext context { FileName = info.GetString("FileNotFound_FileName"); FusionLog = info.GetString("FileNotFound_FusionLog"); + _requestingAssemblyChain = (string?)info.GetValueNoThrow("FileNotFound_RequestingAssemblyChain", typeof(string)); } [Obsolete(Obsoletions.LegacyFormatterImplMessage, DiagnosticId = Obsoletions.LegacyFormatterImplDiagId, UrlFormat = Obsoletions.SharedUrlFormat)] @@ -107,6 +112,7 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont base.GetObjectData(info, context); info.AddValue("FileNotFound_FileName", FileName, typeof(string)); info.AddValue("FileNotFound_FusionLog", FusionLog, typeof(string)); + info.AddValue("FileNotFound_RequestingAssemblyChain", _requestingAssemblyChain, typeof(string)); } } } diff --git a/src/libraries/System.Runtime.Loader/tests/AssemblyLoadContextTest.cs b/src/libraries/System.Runtime.Loader/tests/AssemblyLoadContextTest.cs index 323a9706dc351e..19604e0455aa74 100644 --- a/src/libraries/System.Runtime.Loader/tests/AssemblyLoadContextTest.cs +++ b/src/libraries/System.Runtime.Loader/tests/AssemblyLoadContextTest.cs @@ -278,6 +278,25 @@ public static void LoadNonRuntimeAssembly() Assert.IsType(error.InnerException); } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsCoreCLR))] + public static void MissingTransitiveDependency_ShowsRequestingAssemblyChain() + { + // MissingDependency.Root depends on MissingDependency.Mid which depends on MissingDependency.Leaf. + // MissingDependency.Leaf.dll is not deployed (via PrivateAssets=all in Mid's project reference). + // When Root calls Mid's method that uses Leaf types, the runtime throws FileNotFoundException + // for the missing Leaf assembly. The exception message should include the full requesting + // assembly chain (Mid and Root) so users can diagnose dependency loading issues. + FileNotFoundException ex = Assert.Throws( + () => MissingDependency.Root.RootClass.UseMiddle()); + + Assert.NotNull(ex.FileName); + Assert.Contains("MissingDependency.Leaf", ex.FileName); + string exString = ex.ToString(); + Assert.Contains("MissingDependency.Mid", exString); + Assert.Contains(" --> ", exString); + Assert.Contains("MissingDependency.Root", exString); + } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsAssemblyLoadingSupported), nameof(PlatformDetection.IsCoreCLR), nameof(PlatformDetection.HasAssemblyFiles))] public static void InvalidCastException_DifferentALC_ShowsAssemblyInfo() { diff --git a/src/libraries/System.Runtime.Loader/tests/MissingDependency.Leaf/LeafClass.cs b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Leaf/LeafClass.cs new file mode 100644 index 00000000000000..f6da4365e871ae --- /dev/null +++ b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Leaf/LeafClass.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace MissingDependency.Leaf +{ + public class LeafClass + { + public string GetValue() => "Leaf"; + } +} diff --git a/src/libraries/System.Runtime.Loader/tests/MissingDependency.Leaf/MissingDependency.Leaf.csproj b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Leaf/MissingDependency.Leaf.csproj new file mode 100644 index 00000000000000..95057b96d5d206 --- /dev/null +++ b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Leaf/MissingDependency.Leaf.csproj @@ -0,0 +1,8 @@ + + + $(NetCoreAppCurrent) + + + + + diff --git a/src/libraries/System.Runtime.Loader/tests/MissingDependency.Mid/MidClass.cs b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Mid/MidClass.cs new file mode 100644 index 00000000000000..59766f3d60fc4c --- /dev/null +++ b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Mid/MidClass.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using MissingDependency.Leaf; + +namespace MissingDependency.Mid +{ + public class MidClass + { + [MethodImpl(MethodImplOptions.NoInlining)] + public static string UseLeaf() + { + return new LeafClass().GetValue(); + } + } +} diff --git a/src/libraries/System.Runtime.Loader/tests/MissingDependency.Mid/MissingDependency.Mid.csproj b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Mid/MissingDependency.Mid.csproj new file mode 100644 index 00000000000000..dfb002634d4be4 --- /dev/null +++ b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Mid/MissingDependency.Mid.csproj @@ -0,0 +1,13 @@ + + + $(NetCoreAppCurrent) + + + + + + + all + + + diff --git a/src/libraries/System.Runtime.Loader/tests/MissingDependency.Root/MissingDependency.Root.csproj b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Root/MissingDependency.Root.csproj new file mode 100644 index 00000000000000..01dcb3ef46c3ba --- /dev/null +++ b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Root/MissingDependency.Root.csproj @@ -0,0 +1,11 @@ + + + $(NetCoreAppCurrent) + + + + + + + + diff --git a/src/libraries/System.Runtime.Loader/tests/MissingDependency.Root/RootClass.cs b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Root/RootClass.cs new file mode 100644 index 00000000000000..7a665a8874249f --- /dev/null +++ b/src/libraries/System.Runtime.Loader/tests/MissingDependency.Root/RootClass.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using MissingDependency.Mid; + +namespace MissingDependency.Root +{ + public class RootClass + { + [MethodImpl(MethodImplOptions.NoInlining)] + public static string UseMiddle() + { + return MidClass.UseLeaf(); + } + } +} diff --git a/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Tests.csproj b/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Tests.csproj index 073972916ff2cd..bfbbc14c89f004 100644 --- a/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Tests.csproj +++ b/src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Tests.csproj @@ -60,6 +60,7 @@ + From 0a017ffd4c094205e951ea5965c69c5a0fee8b01 Mon Sep 17 00:00:00 2001 From: Adam Perlin Date: Fri, 1 May 2026 09:55:08 -0700 Subject: [PATCH 066/115] JIT: Simplify ifdef for R2R needsIndirectionCellArg condition in morph (#127624) This was a bit of remaining feedback from #115375; Makes the condition for `needsIndirectionCellArg` more explicit in the ifdef chain and also moves and clarifies some comments. --- src/coreclr/jit/morph.cpp | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/coreclr/jit/morph.cpp b/src/coreclr/jit/morph.cpp index fe982668bce51c..a48e09d08bb82e 100644 --- a/src/coreclr/jit/morph.cpp +++ b/src/coreclr/jit/morph.cpp @@ -1748,26 +1748,23 @@ void CallArgs::AddFinalArgsAndDetermineABIInfo(Compiler* comp, GenTreeCall* call call->gtCallMethHnd = comp->eeFindHelper(CORINFO_HELP_PINVOKE_CALLI); } #if defined(FEATURE_READYTORUN) - // For arm/arm64, we dispatch code same as VSD using virtualStubParamInfo->GetReg() - // for indirection cell address, which ZapIndirectHelperThunk expects. - // For x64/x86 we use return address to get the indirection cell by disassembling the call site. - // That is not possible for fast tailcalls, so we only need this logic for fast tailcalls on xarch. - // Note that we call this before we know if something will be a fast tailcall or not. - // That's ok; after making something a tailcall, we will invalidate this information - // and reconstruct it if necessary. The tailcalling decision does not change since - // this is a non-standard arg in a register. -#ifndef TARGET_WASM - bool needsIndirectionCellArg = call->IsR2RRelativeIndir() && !call->IsDelegateInvoke(); - -#if defined(TARGET_XARCH) - needsIndirectionCellArg &= call->IsFastTailCall(); -#endif -#else - // TARGET_WASM does not use an explicit indirection cell arg for the R2R calling convention, +#ifdef TARGET_WASM + // TARGET_WASM does not use an explicit indirection cell arg for the R2R calling convention since // the address of the indirection cell is recoverable from the portable entrypoint which - // we pass as part of the Wasm managed calling convention (See LowerPEPCall). + // we pass already as part of the Wasm managed calling convention (See LowerPEPCall). bool needsIndirectionCellArg = false; +#elif defined(TARGET_XARCH) + // For x64/x86 we use the return address to get the indirection cell by disassembling the call site. + // That is not possible for fast tailcalls, so we do need to pass the indirection cell address for fast tailcalls on + // xarch. Note that we call this before we know if something will be a fast tailcall or not. That's ok; after making + // something a tailcall, we will invalidate this information and reconstruct it if necessary. The tailcalling + // decision does not change since this is a non-standard arg in a register. + bool needsIndirectionCellArg = call->IsR2RRelativeIndir() && !call->IsDelegateInvoke() && call->IsFastTailCall(); +#else + // For arm/arm64 and other non-xarch targets, we dispatch code the same as VSD using virtualStubParamInfo->GetReg() + // for the indirection cell address, which the ReadyToRun DelayLoad helpers expect. + bool needsIndirectionCellArg = call->IsR2RRelativeIndir() && !call->IsDelegateInvoke(); #endif if (needsIndirectionCellArg) From a52a00562bb81eed3f16fd283f3f2a96a7e65e45 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 10:51:47 -0700 Subject: [PATCH 067/115] Add runtime-coreclr jit-cfg to CI pipeline monitor (#127627) Adds `runtime-coreclr jit-cfg` (Control Flow Guard) to the CI pipeline monitor's tracked pipeline list. ## Description - Added `runtime-coreclr jit-cfg` to the **Pipeline Details** table with schedule `Sat-Sun 22:00 UTC` and note `Control flow guard.` - Added corresponding entry to the **Cached Definition ID Mapping** table with Def ID `155` (no note, following the convention of only including skip-related notes in that table), placed in sorted order by ID after `runtime-coreclr superpmi-asmdiffs-checked-release` (ID 153) --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: JulieLeeMSFT <63486087+JulieLeeMSFT@users.noreply.github.com> --- .github/skills/ci-pipeline-monitor/pipelines.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/skills/ci-pipeline-monitor/pipelines.md b/.github/skills/ci-pipeline-monitor/pipelines.md index fd0a97c98edd9a..165ea41533b4ac 100644 --- a/.github/skills/ci-pipeline-monitor/pipelines.md +++ b/.github/skills/ci-pipeline-monitor/pipelines.md @@ -36,6 +36,7 @@ cached mapping table). | runtime-coreclr pgo | | | runtime-coreclr libraries-pgo | | | runtime-coreclr pgostress | | +| runtime-coreclr jit-cfg | Sat-Sun 22:00 UTC | Control flow guard. | ## Cached Definition ID Mapping @@ -62,6 +63,7 @@ entries via the AzDO Definitions API, and adds new rows here. | runtime-coreclr libraries-pgo | 145 | | | runtime-coreclr superpmi-replay | 150 | | | runtime-coreclr superpmi-asmdiffs-checked-release | 153 | | +| runtime-coreclr jit-cfg | 155 | | | runtime-coreclr jitstress-random | 159 | | | runtime-coreclr libraries-jitstress-random | 160 | | | runtime-coreclr pgostress | 230 | | From 3c87c97f4dfe7e8059d90e9b5ce19c1ead1c2d8b Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Fri, 1 May 2026 11:54:27 -0700 Subject: [PATCH 068/115] JIT: avoid store forward stall for struct params in GS frames (#127487) If we have a struct param in a GS frame, we will spill it using narrow writes and then copy it to the shadow param with wide stores, causing a store-forward stall. Try and avoid this by forcing the copies to be int-register sized. Addresses #121248. --- src/coreclr/jit/compiler.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index 35be79978cc0e3..f86c520837532c 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -11997,7 +11997,7 @@ class Compiler static bool mayNeedShadowCopy(LclVarDsc* varDsc) { -#if defined(TARGET_AMD64) +#if defined(WINDOWS_AMD64_ABI) // GS cookie logic to create shadow slots, create trees to copy reg args to shadow // slots and update all trees to refer to shadow slots is done immediately after // fgMorph(). Lsra could potentially mark a param as DoNotEnregister after JIT determines @@ -12023,7 +12023,7 @@ class Compiler // - Whenever a parameter passed in an argument register needs to be spilled by LSRA, we // create a new spill temp if the method needs GS cookie check. return varDsc->lvIsParam; -#else // !defined(TARGET_AMD64) +#else // !defined(WINDOWS_AMD64_ABI) return varDsc->lvIsParam && !varDsc->lvIsRegArg; #endif } From 3c0181dd0623e7c4e9f8cc5bd55a9eb4c421298b Mon Sep 17 00:00:00 2001 From: Rachel Jarvi Date: Fri, 1 May 2026 11:58:17 -0700 Subject: [PATCH 069/115] Re-enable OSX runtime-diagnostics legs (#127492) Fixes https://github.com/dotnet/runtime/issues/126435 --------- Co-authored-by: Juan Sebastian Hoyos Ayala <19413848+hoyosjs@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Juan Sebastian Hoyos Ayala --- eng/pipelines/runtime-diagnostics.yml | 7 ++----- .../managed/cdac/tests/DumpTests/cdac-dump-helix.proj | 4 ++++ .../cdac/tests/DumpTests/cdac-dump-xplat-test-helix.proj | 4 ++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/eng/pipelines/runtime-diagnostics.yml b/eng/pipelines/runtime-diagnostics.yml index 7adc3792d1375f..3c47d4522dfa92 100644 --- a/eng/pipelines/runtime-diagnostics.yml +++ b/eng/pipelines/runtime-diagnostics.yml @@ -15,11 +15,8 @@ parameters: - windows_arm64 - linux_arm64 - linux_arm - # TODO: Re-enable osx once disk space issue is resolved. - # macOS full dumps are ~5.7GB each; with 8+ full-dump debuggees the Helix - # machines run out of disk space (~45GB total). See PR #124782 for details. - # - osx_arm64 - # - osx_x64 + - osx_arm64 + - osx_x64 - name: cdacDumpTestMode displayName: cDAC Dump Test Mode type: string diff --git a/src/native/managed/cdac/tests/DumpTests/cdac-dump-helix.proj b/src/native/managed/cdac/tests/DumpTests/cdac-dump-helix.proj index cfa86393468487..a1e9d7206db94d 100644 --- a/src/native/managed/cdac/tests/DumpTests/cdac-dump-helix.proj +++ b/src/native/managed/cdac/tests/DumpTests/cdac-dump-helix.proj @@ -127,6 +127,10 @@ + + + + diff --git a/src/native/managed/cdac/tests/DumpTests/cdac-dump-xplat-test-helix.proj b/src/native/managed/cdac/tests/DumpTests/cdac-dump-xplat-test-helix.proj index d518c3ba23c16a..a8e869fca5bde3 100644 --- a/src/native/managed/cdac/tests/DumpTests/cdac-dump-xplat-test-helix.proj +++ b/src/native/managed/cdac/tests/DumpTests/cdac-dump-xplat-test-helix.proj @@ -69,6 +69,10 @@ + + + + From 200888c98c185d97beca6f6b348334dc5cc7a263 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 1 May 2026 12:16:34 -0700 Subject: [PATCH 070/115] Move built-in array marshalling to managed and make olevariant.cpp Windows-only (#126911) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot --- .../InteropServices/Marshal.CoreCLR.cs | 29 +- .../src/System/StubHelpers.cs | 1803 ++++++++----- src/coreclr/vm/CMakeLists.txt | 4 +- src/coreclr/vm/binder.cpp | 1 - src/coreclr/vm/ceemain.cpp | 1 - src/coreclr/vm/comcallablewrapper.cpp | 1 - src/coreclr/vm/corelib.h | 49 +- src/coreclr/vm/dispatchinfo.cpp | 17 +- src/coreclr/vm/dispparammarshaler.cpp | 31 +- src/coreclr/vm/dispparammarshaler.h | 9 +- src/coreclr/vm/dllimport.cpp | 6 +- src/coreclr/vm/fieldmarshaler.cpp | 229 +- src/coreclr/vm/fieldmarshaler.h | 4 +- src/coreclr/vm/ilmarshalers.cpp | 801 +++--- src/coreclr/vm/ilmarshalers.h | 113 +- src/coreclr/vm/interopconverter.cpp | 1 - src/coreclr/vm/marshalnative.cpp | 8 + src/coreclr/vm/metasig.h | 2 + src/coreclr/vm/mlinfo.cpp | 20 +- src/coreclr/vm/mlinfo.h | 13 +- src/coreclr/vm/olevariant.cpp | 2306 +++-------------- src/coreclr/vm/olevariant.h | 181 +- src/coreclr/vm/qcallentrypoints.cpp | 9 - src/coreclr/vm/stubhelpers.cpp | 1 + .../SafeArray/SafeArrayNative.cpp | 206 ++ .../SafeArray/SafeArrayTest.cs | 135 + .../COM/NETClients/IDispatch/Program.cs | 15 + .../Interop/COM/NETServer/DispatchTesting.cs | 11 + .../COM/NativeClients/Dispatch/Client.cpp | 71 + .../COM/NativeServer/DispatchTesting.h | 62 +- .../COM/ServerContracts/Server.Contracts.cs | 2 + .../COM/ServerContracts/Server.Contracts.h | 4 + 32 files changed, 2863 insertions(+), 3282 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshal.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshal.CoreCLR.cs index ac0b2277ba1491..ffa20878cba85e 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshal.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshal.CoreCLR.cs @@ -110,8 +110,7 @@ private static unsafe T ReadValueSlow(object ptr, int ofs, Func(object ptr, int ofs, T val, Action< (int)AsAnyMarshaler.AsAnyFlags.IsAnsi | (int)AsAnyMarshaler.AsAnyFlags.IsBestFit; - MngdNativeArrayMarshaler.MarshalerState nativeArrayMarshalerState = default; - AsAnyMarshaler marshaler = new AsAnyMarshaler(new IntPtr(&nativeArrayMarshalerState)); + AsAnyMarshaler marshaler = new(ptr, Flags); IntPtr pNativeHome = IntPtr.Zero; @@ -240,22 +238,22 @@ internal sealed class LayoutTypeMarshalerMethods : RuntimeType.IGenericCacheEntr private static MemberInfo ConvertToManagedMethod => field ??= typeof(BoxedLayoutTypeMarshaler<>).GetMethod(nameof(BoxedLayoutTypeMarshaler.ConvertToManaged), BindingFlags.Public | BindingFlags.Static)!; private static MemberInfo FreeMethod => field ??= typeof(BoxedLayoutTypeMarshaler<>).GetMethod(nameof(BoxedLayoutTypeMarshaler.Free), BindingFlags.Public | BindingFlags.Static)!; - private unsafe delegate void ConvertToUnmanagedDelegate(object obj, byte* native, int nativeSize, ref CleanupWorkListElement? cleanupWorkList); + private unsafe delegate void ConvertToUnmanagedDelegate(object obj, byte* native, ref CleanupWorkListElement? cleanupWorkList); private unsafe delegate void ConvertToManagedDelegate(object obj, byte* native, ref CleanupWorkListElement? cleanupWorkList); - private unsafe delegate void FreeDelegate(object? obj, byte* native, int nativeSize, ref CleanupWorkListElement? cleanupWorkList); + private unsafe delegate void FreeDelegate(object? obj, byte* native, ref CleanupWorkListElement? cleanupWorkList); private readonly ConvertToUnmanagedDelegate _convertToUnmanaged; private readonly ConvertToManagedDelegate _convertToManaged; private readonly FreeDelegate _free; - private readonly int _nativeSize; + private readonly bool _isBlittable; - internal LayoutTypeMarshalerMethods(Type instantiatedType, int nativeSize) + internal LayoutTypeMarshalerMethods(Type instantiatedType, bool isBlittable) { _convertToUnmanaged = ((MethodInfo)instantiatedType.GetMemberWithSameMetadataDefinitionAs(ConvertToUnmanagedMethod)).CreateDelegate(); _convertToManaged = ((MethodInfo)instantiatedType.GetMemberWithSameMetadataDefinitionAs(ConvertToManagedMethod)).CreateDelegate(); _free = ((MethodInfo)instantiatedType.GetMemberWithSameMetadataDefinitionAs(FreeMethod)).CreateDelegate(); - _nativeSize = nativeSize; + _isBlittable = isBlittable; } public unsafe void ConvertToManaged(object obj, byte* native) @@ -265,12 +263,17 @@ public unsafe void ConvertToManaged(object obj, byte* native) public unsafe void ConvertToUnmanaged(object obj, byte* native, ref CleanupWorkListElement? cleanupWorkList) { - _convertToUnmanaged(obj, native, _nativeSize, ref cleanupWorkList); + _convertToUnmanaged(obj, native, ref cleanupWorkList); } public unsafe void Free(byte* native) { - _free(null, native, _nativeSize, ref Unsafe.NullRef()); + // For blittable types, FreeCore is a no-op and there are no native sub-structures to free. + // Calling NativeMemory.Clear on a potentially invalid pointer (e.g., in Marshal.DestroyStructure tests) + // would cause a fault, so we skip cleanup entirely for blittable types. + if (_isBlittable) + return; + _free(null, native, ref Unsafe.NullRef()); } internal static LayoutTypeMarshalerMethods GetMarshalMethodsForType(RuntimeType t) @@ -281,11 +284,11 @@ internal static LayoutTypeMarshalerMethods GetMarshalMethodsForType(RuntimeType [RequiresDynamicCode("Marshalling code for the object might not be available.")] public static LayoutTypeMarshalerMethods Create(RuntimeType type) { - if (!HasLayout(new QCallTypeHandle(ref type), out _, out int size)) + if (!HasLayout(new QCallTypeHandle(ref type), out bool isBlittable, out _)) throw new ArgumentException(SR.Argument_MustHaveLayoutOrBeBlittable, nameof(type)); Type instantiatedMarshaler = typeof(BoxedLayoutTypeMarshaler<>).MakeGenericType([type]); - return new LayoutTypeMarshalerMethods(instantiatedMarshaler, size); + return new LayoutTypeMarshalerMethods(instantiatedMarshaler, isBlittable); } public static ref LayoutTypeMarshalerMethods? GetStorageRef(RuntimeType.CompositeCacheEntry compositeEntry) diff --git a/src/coreclr/System.Private.CoreLib/src/System/StubHelpers.cs b/src/coreclr/System.Private.CoreLib/src/System/StubHelpers.cs index 890bff5ec3c7ec..ae34c9ab034225 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/StubHelpers.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/StubHelpers.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Numerics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; @@ -53,7 +54,7 @@ internal static char ConvertToManaged(byte nativeChar) internal static class CSTRMarshaler { - internal static unsafe IntPtr ConvertToNative(int flags, string strManaged, IntPtr pNativeBuffer) + internal static unsafe IntPtr ConvertToNative(int flags, string? strManaged, IntPtr pNativeBuffer) { if (null == strManaged) { @@ -265,7 +266,7 @@ private static void SetTrailByte(string strManaged, byte trailByte) s_trailByteTable!.Add(strManaged, new TrailByte(trailByte)); } - internal static unsafe IntPtr ConvertToNative(string strManaged, IntPtr pNativeBuffer) + internal static unsafe IntPtr ConvertToNative(string? strManaged, IntPtr pNativeBuffer) { if (null == strManaged) { @@ -591,7 +592,7 @@ internal static void ThrowCriticalHandleFieldChanged() } } - internal static class DateMarshaler + internal sealed class DateMarshaler : IArrayElementMarshaler { internal static double ConvertToNative(DateTime managedDate) { @@ -602,6 +603,22 @@ internal static long ConvertToManaged(double nativeDate) { return DateTime.DoubleDateToTicks(nativeDate); } + + static unsafe void IArrayElementMarshaler.ConvertToUnmanaged(ref DateTime managed, byte* unmanaged) + { + Unsafe.WriteUnaligned(unmanaged, ConvertToNative(managed)); + } + + static unsafe void IArrayElementMarshaler.ConvertToManaged(ref DateTime managed, byte* unmanaged) + { + managed = new DateTime(ConvertToManaged(Unsafe.ReadUnaligned(unmanaged))); + } + + static unsafe void IArrayElementMarshaler.Free(byte* unmanaged) + { + } + + static unsafe nuint IArrayElementMarshaler.UnmanagedSize => (nuint)sizeof(double); } // class DateMarshaler #if FEATURE_COMINTEROP @@ -654,169 +671,13 @@ internal static object GetObjectForComCallableWrapperIUnknown(IntPtr unk) } // class InterfaceMarshaler #endif // FEATURE_COMINTEROP - internal static partial class MngdNativeArrayMarshaler - { - // Needs to match exactly with MngdNativeArrayMarshaler in ilmarshalers.h - internal struct MarshalerState - { - internal IntPtr m_pElementMT; - internal TypeHandle m_Array; - internal int m_NativeDataValid; - internal int m_BestFitMap; - internal int m_ThrowOnUnmappableChar; - internal short m_vt; - } - - internal static unsafe void CreateMarshaler(IntPtr pMarshalState, IntPtr pMT, int dwFlags, bool nativeDataValid) - { - MarshalerState* pState = (MarshalerState*)pMarshalState; - pState->m_pElementMT = pMT; - pState->m_Array = default; - pState->m_NativeDataValid = nativeDataValid ? 1 : 0; - pState->m_BestFitMap = (byte)(dwFlags >> 16); - pState->m_ThrowOnUnmappableChar = (byte)(dwFlags >> 24); - pState->m_vt = (short)dwFlags; - } - - internal static void ConvertSpaceToNative(IntPtr pMarshalState, in object pManagedHome, IntPtr pNativeHome) - { - object managedHome = pManagedHome; - ConvertSpaceToNative(pMarshalState, ObjectHandleOnStack.Create(ref managedHome), pNativeHome); - } - - [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "MngdNativeArrayMarshaler_ConvertSpaceToNative")] - private static partial void ConvertSpaceToNative(IntPtr pMarshalState, ObjectHandleOnStack pManagedHome, IntPtr pNativeHome); - - internal static void ConvertContentsToNative(IntPtr pMarshalState, in object pManagedHome, IntPtr pNativeHome) - { - object managedHome = pManagedHome; - ConvertContentsToNative(pMarshalState, ObjectHandleOnStack.Create(ref managedHome), pNativeHome); - } - - [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "MngdNativeArrayMarshaler_ConvertContentsToNative")] - private static partial void ConvertContentsToNative(IntPtr pMarshalState, ObjectHandleOnStack pManagedHome, IntPtr pNativeHome); - - internal static void ConvertSpaceToManaged(IntPtr pMarshalState, ref object? pManagedHome, IntPtr pNativeHome, - int cElements) - { - object? managedHome = null; - ConvertSpaceToManaged(pMarshalState, ObjectHandleOnStack.Create(ref managedHome), pNativeHome, cElements); - pManagedHome = managedHome; - } - - [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "MngdNativeArrayMarshaler_ConvertSpaceToManaged")] - private static partial void ConvertSpaceToManaged(IntPtr pMarshalState, ObjectHandleOnStack pManagedHome, IntPtr pNativeHome, - int cElements); - - internal static void ConvertContentsToManaged(IntPtr pMarshalState, in object pManagedHome, IntPtr pNativeHome) - { - object managedHome = pManagedHome; - ConvertContentsToManaged(pMarshalState, ObjectHandleOnStack.Create(ref managedHome), pNativeHome); - } - - [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "MngdNativeArrayMarshaler_ConvertContentsToManaged")] - private static partial void ConvertContentsToManaged(IntPtr pMarshalState, ObjectHandleOnStack pManagedHome, IntPtr pNativeHome); - - internal static unsafe void ClearNative(IntPtr pMarshalState, IntPtr pNativeHome, int cElements) - { - IntPtr nativeHome = *(IntPtr*)pNativeHome; - - if (nativeHome != IntPtr.Zero) - { - ClearNativeContents(pMarshalState, pNativeHome, cElements); - Marshal.FreeCoTaskMem(nativeHome); - } - } - - [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "MngdNativeArrayMarshaler_ClearNativeContents")] - internal static partial void ClearNativeContents(IntPtr pMarshalState, IntPtr pNativeHome, int cElements); - } // class MngdNativeArrayMarshaler - - internal static partial class MngdFixedArrayMarshaler - { - // Needs to match exactly with MngdFixedArrayMarshaler in ilmarshalers.h - private struct MarshalerState - { -#pragma warning disable CA1823, IDE0044 // not used by managed code - internal IntPtr m_pElementMT; - internal IntPtr m_Array; - internal int m_BestFitMap; - internal int m_ThrowOnUnmappableChar; - internal ushort m_vt; - internal uint m_cElements; -#pragma warning restore CA1823, IDE0044 - } - - internal static unsafe void CreateMarshaler(IntPtr pMarshalState, IntPtr pMT, int dwFlags, int cElements) - { - MarshalerState* pState = (MarshalerState*)pMarshalState; - pState->m_pElementMT = pMT; - pState->m_Array = default; - pState->m_BestFitMap = (byte)(dwFlags >> 16); - pState->m_ThrowOnUnmappableChar = (byte)(dwFlags >> 24); - pState->m_vt = (ushort)dwFlags; - pState->m_cElements = (uint)cElements; - } - - internal static unsafe void ConvertSpaceToNative(IntPtr pMarshalState, in object pManagedHome, IntPtr pNativeHome) - { - // We don't actually need to allocate native space here as the space is inline in the native layout. - // However, we need to validate that we can fit the contents of the managed array in the native space. - Array arr = (Array)pManagedHome; - MarshalerState* pState = (MarshalerState*)pMarshalState; - - if (arr is not null && (uint)arr.Length < pState->m_cElements) - { - throw new ArgumentException(SR.Argument_WrongSizeArrayInNativeStruct); - } - } - - internal static void ConvertContentsToNative(IntPtr pMarshalState, in object pManagedHome, IntPtr pNativeHome) - { - object managedHome = pManagedHome; - ConvertContentsToNative(pMarshalState, ObjectHandleOnStack.Create(ref managedHome), pNativeHome); - } - - [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "MngdFixedArrayMarshaler_ConvertContentsToNative")] - private static partial void ConvertContentsToNative(IntPtr pMarshalState, ObjectHandleOnStack pManagedHome, IntPtr pNativeHome); - - internal static void ConvertSpaceToManaged(IntPtr pMarshalState, ref object pManagedHome, IntPtr pNativeHome) - { - object managedHome = pManagedHome; - ConvertSpaceToManaged(pMarshalState, ObjectHandleOnStack.Create(ref managedHome), pNativeHome); - pManagedHome = managedHome; - } - - [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "MngdFixedArrayMarshaler_ConvertSpaceToManaged")] - private static partial void ConvertSpaceToManaged(IntPtr pMarshalState, ObjectHandleOnStack pManagedHome, IntPtr pNativeHome); - - internal static void ConvertContentsToManaged(IntPtr pMarshalState, in object pManagedHome, IntPtr pNativeHome) - { - object managedHome = pManagedHome; - ConvertContentsToManaged(pMarshalState, ObjectHandleOnStack.Create(ref managedHome), pNativeHome); - } - - [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "MngdFixedArrayMarshaler_ConvertContentsToManaged")] - private static partial void ConvertContentsToManaged(IntPtr pMarshalState, ObjectHandleOnStack pManagedHome, IntPtr pNativeHome); - -#pragma warning disable IDE0060 // Remove unused parameter. These APIs need to match a the shape of a "managed" marshaler. - internal static void ClearNativeContents(IntPtr pMarshalState, in object pManagedHome, IntPtr pNativeHome) - { - ClearNativeContents(pMarshalState, pNativeHome); - } -#pragma warning restore IDE0060 - - [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "MngdFixedArrayMarshaler_ClearNativeContents")] - private static partial void ClearNativeContents(IntPtr pMarshalState, IntPtr pNativeHome); - } // class MngdFixedArrayMarshaler - #if FEATURE_COMINTEROP internal static partial class MngdSafeArrayMarshaler { [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "MngdSafeArrayMarshaler_CreateMarshaler")] [SuppressGCTransition] - internal static partial void CreateMarshaler(IntPtr pMarshalState, IntPtr pMT, int iRank, int dwFlags); + internal static partial void CreateMarshaler(IntPtr pMarshalState, IntPtr pMT, int iRank, int dwFlags, IntPtr pConvertToNative, IntPtr pConvertToManaged); internal static void ConvertSpaceToNative(IntPtr pMarshalState, in object pManagedHome, IntPtr pNativeHome) { @@ -1021,673 +882,1238 @@ internal static void GetCustomMarshalerInstance(void* pMT, byte* pCookie, int cC internal struct AsAnyMarshaler { - private const ushort VTHACK_ANSICHAR = 253; - private const ushort VTHACK_WINBOOL = 254; + private AsAnyMarshalerImplementation? _impl; - private enum BackPropAction + private abstract class AsAnyMarshalerImplementation { - None, - Array, - Layout, - StringBuilderAnsi, - StringBuilderUnicode + public abstract IntPtr ConvertToNative(object managed, int dwFlags); + public abstract void ConvertToManaged(object managed, IntPtr native); + public abstract void ClearNative(IntPtr native); } - // Pointer to MngdNativeArrayMarshaler, ownership not assumed. - private readonly IntPtr pvArrayMarshaler; - - // Type of action to perform after the CLR-to-unmanaged call. - private BackPropAction backPropAction; - - // The managed layout type for BackPropAction.Layout. - private Type? layoutType; + private sealed class ArrayImplementation : AsAnyMarshalerImplementation + where TMarshaler : IArrayMarshaler + { + private readonly bool _isOut; - // Cleanup list to be destroyed when clearing the native view (for layouts with SafeHandles). - private CleanupWorkListElement? cleanupWorkList; + public ArrayImplementation(bool isOut) { _isOut = isOut; } - [Flags] - internal enum AsAnyFlags - { - In = 0x10000000, - Out = 0x20000000, - IsAnsi = 0x00FF0000, - IsThrowOn = 0x0000FF00, - IsBestFit = 0x000000FF - } + public override unsafe IntPtr ConvertToNative(object managed, int dwFlags) + { + Array array = (Array)managed; + byte* pNative = TMarshaler.AllocateSpaceForUnmanaged(array); + try + { + if (IsIn(dwFlags)) + TMarshaler.ConvertContentsToUnmanaged(array, pNative, array.Length); + } + catch + { + Marshal.FreeCoTaskMem((IntPtr)pNative); + throw; + } - private static bool IsIn(int dwFlags) => (dwFlags & (int)AsAnyFlags.In) != 0; - private static bool IsOut(int dwFlags) => (dwFlags & (int)AsAnyFlags.Out) != 0; - private static bool IsAnsi(int dwFlags) => (dwFlags & (int)AsAnyFlags.IsAnsi) != 0; - private static bool IsThrowOn(int dwFlags) => (dwFlags & (int)AsAnyFlags.IsThrowOn) != 0; - private static bool IsBestFit(int dwFlags) => (dwFlags & (int)AsAnyFlags.IsBestFit) != 0; + return (IntPtr)pNative; + } - internal AsAnyMarshaler(IntPtr pvArrayMarshaler) - { - // we need this in case the value being marshaled turns out to be array - Debug.Assert(pvArrayMarshaler != IntPtr.Zero, "pvArrayMarshaler must not be null"); + public override unsafe void ConvertToManaged(object managed, IntPtr native) + { + if (!_isOut) return; + Array array = (Array)managed; + TMarshaler.ConvertContentsToManaged(array, (byte*)native, array.Length); + } - this.pvArrayMarshaler = pvArrayMarshaler; - backPropAction = BackPropAction.None; - layoutType = null; - cleanupWorkList = null; + public override void ClearNative(IntPtr native) + { + if (native != IntPtr.Zero) + Marshal.FreeCoTaskMem(native); + } } - #region ConvertToNative helpers - - private unsafe IntPtr ConvertArrayToNative(object pManagedHome, int dwFlags) + private sealed class StringImplementation : AsAnyMarshalerImplementation { - Type elementType = pManagedHome.GetType().GetElementType()!; - VarEnum vt; - - switch (Type.GetTypeCode(elementType)) + public override unsafe IntPtr ConvertToNative(object managed, int dwFlags) { - case TypeCode.SByte: vt = VarEnum.VT_I1; break; - case TypeCode.Byte: vt = VarEnum.VT_UI1; break; - case TypeCode.Int16: vt = VarEnum.VT_I2; break; - case TypeCode.UInt16: vt = VarEnum.VT_UI2; break; - case TypeCode.Int32: vt = VarEnum.VT_I4; break; - case TypeCode.UInt32: vt = VarEnum.VT_UI4; break; - case TypeCode.Int64: vt = VarEnum.VT_I8; break; - case TypeCode.UInt64: vt = VarEnum.VT_UI8; break; - case TypeCode.Single: vt = VarEnum.VT_R4; break; - case TypeCode.Double: vt = VarEnum.VT_R8; break; - case TypeCode.Char: vt = (IsAnsi(dwFlags) ? (VarEnum)VTHACK_ANSICHAR : VarEnum.VT_UI2); break; - case TypeCode.Boolean: vt = (VarEnum)VTHACK_WINBOOL; break; + string str = (string)managed; + + // IsIn, IsOut are ignored for strings - they're always in-only + if (IsAnsi(dwFlags)) + { + return CSTRMarshaler.ConvertToNative( + dwFlags & 0xFFFF, // (throw on unmappable char << 8 | best fit) + str, + IntPtr.Zero); // unmanaged buffer will be allocated + } - case TypeCode.Object: - { - if (elementType == typeof(IntPtr)) - { - vt = (IntPtr.Size == 4 ? VarEnum.VT_I4 : VarEnum.VT_I8); - } - else if (elementType == typeof(UIntPtr)) - { - vt = (IntPtr.Size == 4 ? VarEnum.VT_UI4 : VarEnum.VT_UI8); - } - else goto default; - break; - } + int allocSize = (str.Length + 1) * 2; + IntPtr pNative = Marshal.AllocCoTaskMem(allocSize); + Buffer.Memmove(ref *(char*)pNative, ref str.GetRawStringData(), (nuint)str.Length + 1); - default: - throw new ArgumentException(SR.Arg_PInvokeBadObject); + return pNative; } - // marshal the object as C-style array (UnmanagedType.LPArray) - int dwArrayMarshalerFlags = (int)vt; - if (IsBestFit(dwFlags)) dwArrayMarshalerFlags |= (1 << 16); - if (IsThrowOn(dwFlags)) dwArrayMarshalerFlags |= (1 << 24); + public override void ConvertToManaged(object managed, IntPtr native) { } - MngdNativeArrayMarshaler.CreateMarshaler( - pvArrayMarshaler, - IntPtr.Zero, // not needed as we marshal primitive VTs only - dwArrayMarshalerFlags, - nativeDataValid: false); - - IntPtr pNativeHome; - IntPtr pNativeHomeAddr = new IntPtr(&pNativeHome); + public override void ClearNative(IntPtr native) + { + if (native != IntPtr.Zero) + Marshal.FreeCoTaskMem(native); + } + } - MngdNativeArrayMarshaler.ConvertSpaceToNative( - pvArrayMarshaler, - in pManagedHome, - pNativeHomeAddr); + private sealed class LayoutImplementation : AsAnyMarshalerImplementation + { + private readonly Type _layoutType; + private readonly bool _isOut; + internal CleanupWorkListElement? _cleanupWorkList; - if (IsIn(dwFlags)) + public LayoutImplementation(Type layoutType, bool isOut) { - MngdNativeArrayMarshaler.ConvertContentsToNative( - pvArrayMarshaler, - in pManagedHome, - pNativeHomeAddr); + _layoutType = layoutType; + _isOut = isOut; } - if (IsOut(dwFlags)) + + public override unsafe IntPtr ConvertToNative(object managed, int dwFlags) { - backPropAction = BackPropAction.Array; - } + // Note that the following call will not throw exception if the type + // of managed is not marshalable. That's intentional because we + // want to maintain the original behavior where this was indicated + // by TypeLoadException during the actual field marshaling. + int allocSize = Marshal.SizeOfHelper((RuntimeType)_layoutType, false); + IntPtr pNative = Marshal.AllocCoTaskMem(allocSize); - return pNativeHome; - } + if (IsIn(dwFlags)) + { + StubHelpers.LayoutTypeConvertToUnmanaged(managed, (byte*)pNative, ref _cleanupWorkList); + } - private static IntPtr ConvertStringToNative(string pManagedHome, int dwFlags) - { - IntPtr pNativeHome; + return pNative; + } - // IsIn, IsOut are ignored for strings - they're always in-only - if (IsAnsi(dwFlags)) + public override unsafe void ConvertToManaged(object managed, IntPtr native) { - // marshal the object as Ansi string (UnmanagedType.LPStr) - pNativeHome = CSTRMarshaler.ConvertToNative( - dwFlags & 0xFFFF, // (throw on unmappable char << 8 | best fit) - pManagedHome, // - IntPtr.Zero); // unmanaged buffer will be allocated + if (_isOut) + StubHelpers.LayoutTypeConvertToManaged(managed, (byte*)native); } - else + + public override void ClearNative(IntPtr native) { - // marshal the object as Unicode string (UnmanagedType.LPWStr) - int allocSize = (pManagedHome.Length + 1) * 2; - pNativeHome = Marshal.AllocCoTaskMem(allocSize); - unsafe + if (native != IntPtr.Zero) { - Buffer.Memmove(ref *(char*)pNativeHome, ref pManagedHome.GetRawStringData(), (nuint)pManagedHome.Length + 1); + Marshal.DestroyStructure(native, _layoutType); + Marshal.FreeCoTaskMem(native); } + StubHelpers.DestroyCleanupList(ref _cleanupWorkList); } - - return pNativeHome; } - private unsafe IntPtr ConvertStringBuilderToNative(StringBuilder pManagedHome, int dwFlags) + private sealed class StringBuilderAnsiImplementation : AsAnyMarshalerImplementation { - IntPtr pNativeHome; + private readonly bool _isOut; - // P/Invoke can be used to call Win32 apis that don't strictly follow CLR in/out semantics and thus may - // leave garbage in the buffer in circumstances that we can't detect. To prevent us from crashing when - // converting the contents back to managed, put a hidden NULL terminator past the end of the official buffer. + public StringBuilderAnsiImplementation(bool isOut) { _isOut = isOut; } - // Unmanaged layout: - // +====================================+ - // | Extra hidden NULL | - // +====================================+ \ - // | | | - // | [Converted] NULL-terminated string | |- buffer that the target may change - // | | | - // +====================================+ / <-- native home - - // Cache StringBuilder capacity and length to ensure we don't allocate a certain amount of - // native memory and then walk beyond its end if the StringBuilder concurrently grows erroneously. - int pManagedHomeCapacity = pManagedHome.Capacity; - int pManagedHomeLength = pManagedHome.Length; - if (pManagedHomeLength > pManagedHomeCapacity) + public override unsafe IntPtr ConvertToNative(object managed, int dwFlags) { - ThrowHelper.ThrowInvalidOperationException(); - } + StringBuilder sb = (StringBuilder)managed; - // Note that StringBuilder.Capacity is the number of characters NOT including any terminators. + // P/Invoke can be used to call Win32 apis that don't strictly follow CLR in/out semantics and thus may + // leave garbage in the buffer in circumstances that we can't detect. To prevent us from crashing when + // converting the contents back to managed, put a hidden NULL terminator past the end of the official buffer. - if (IsAnsi(dwFlags)) - { - StubHelpers.CheckStringLength(pManagedHomeCapacity); + // Unmanaged layout: + // +====================================+ + // | Extra hidden NULL | + // +====================================+ \ + // | | | + // | [Converted] NULL-terminated string | |- buffer that the target may change + // | | | + // +====================================+ / <-- native home + + // Cache StringBuilder capacity and length to ensure we don't allocate a certain amount of + // native memory and then walk beyond its end if the StringBuilder concurrently grows erroneously. + int capacity = sb.Capacity; + int length = sb.Length; + if (length > capacity) + ThrowHelper.ThrowInvalidOperationException(); - // marshal the object as Ansi string (UnmanagedType.LPStr) - int allocSize = checked((pManagedHomeCapacity * Marshal.SystemMaxDBCSCharSize) + 4); - pNativeHome = Marshal.AllocCoTaskMem(allocSize); + // Note that StringBuilder.Capacity is the number of characters NOT including any terminators. + StubHelpers.CheckStringLength(capacity); - byte* ptr = (byte*)pNativeHome; + int allocSize = checked((capacity * Marshal.SystemMaxDBCSCharSize) + 4); + IntPtr pNative = Marshal.AllocCoTaskMem(allocSize); + + byte* ptr = (byte*)pNative; *(ptr + allocSize - 3) = 0; *(ptr + allocSize - 2) = 0; *(ptr + allocSize - 1) = 0; if (IsIn(dwFlags)) { - int length = Marshal.StringToAnsiString(pManagedHome.ToString(), + int len = Marshal.StringToAnsiString(sb.ToString(), ptr, allocSize, IsBestFit(dwFlags), IsThrowOn(dwFlags)); - Debug.Assert(length < allocSize, "Expected a length less than the allocated size"); - } - if (IsOut(dwFlags)) - { - backPropAction = BackPropAction.StringBuilderAnsi; + Debug.Assert(len < allocSize, "Expected a length less than the allocated size"); } + + return pNative; } - else + + public override unsafe void ConvertToManaged(object managed, IntPtr native) + { + if (!_isOut) return; + int length = native == IntPtr.Zero ? 0 : string.strlen((byte*)native); + ((StringBuilder)managed).ReplaceBufferAnsiInternal((sbyte*)native, length); + } + + public override void ClearNative(IntPtr native) + { + if (native != IntPtr.Zero) + Marshal.FreeCoTaskMem(native); + } + } + + private sealed class StringBuilderUnicodeImplementation : AsAnyMarshalerImplementation + { + private readonly bool _isOut; + + public StringBuilderUnicodeImplementation(bool isOut) { _isOut = isOut; } + + public override unsafe IntPtr ConvertToNative(object managed, int dwFlags) { - // marshal the object as Unicode string (UnmanagedType.LPWStr) - int allocSize = checked((pManagedHomeCapacity * 2) + 4); - pNativeHome = Marshal.AllocCoTaskMem(allocSize); + StringBuilder sb = (StringBuilder)managed; + + // See StringBuilderAnsiImplementation.ConvertToNative for buffer layout explanation. + + // Cache StringBuilder capacity and length to ensure we don't allocate a certain amount of + // native memory and then walk beyond its end if the StringBuilder concurrently grows erroneously. + int capacity = sb.Capacity; + int length = sb.Length; + if (length > capacity) + ThrowHelper.ThrowInvalidOperationException(); - byte* ptr = (byte*)pNativeHome; + // Note that StringBuilder.Capacity is the number of characters NOT including any terminators. + int allocSize = checked((capacity * 2) + 4); + IntPtr pNative = Marshal.AllocCoTaskMem(allocSize); + + byte* ptr = (byte*)pNative; *(ptr + allocSize - 1) = 0; *(ptr + allocSize - 2) = 0; if (IsIn(dwFlags)) { - pManagedHome.InternalCopy(pNativeHome, pManagedHomeLength); + sb.InternalCopy(pNative, length); - // null-terminate the native string - int length = pManagedHomeLength * 2; - *(ptr + length + 0) = 0; - *(ptr + length + 1) = 0; - } - if (IsOut(dwFlags)) - { - backPropAction = BackPropAction.StringBuilderUnicode; + int byteLen = length * 2; + *(ptr + byteLen + 0) = 0; + *(ptr + byteLen + 1) = 0; } + + return pNative; } - return pNativeHome; + public override unsafe void ConvertToManaged(object managed, IntPtr native) + { + if (!_isOut) return; + int length = native == IntPtr.Zero ? 0 : string.wcslen((char*)native); + ((StringBuilder)managed).ReplaceBufferInternal((char*)native, length); + } + + public override void ClearNative(IntPtr native) + { + if (native != IntPtr.Zero) + Marshal.FreeCoTaskMem(native); + } + } + + [Flags] + internal enum AsAnyFlags + { + In = 0x10000000, + Out = 0x20000000, + IsAnsi = 0x00FF0000, + IsThrowOn = 0x0000FF00, + IsBestFit = 0x000000FF + } + + private static bool IsIn(int dwFlags) => (dwFlags & (int)AsAnyFlags.In) != 0; + private static bool IsOut(int dwFlags) => (dwFlags & (int)AsAnyFlags.Out) != 0; + private static bool IsAnsi(int dwFlags) => (dwFlags & (int)AsAnyFlags.IsAnsi) != 0; + private static bool IsThrowOn(int dwFlags) => (dwFlags & (int)AsAnyFlags.IsThrowOn) != 0; + private static bool IsBestFit(int dwFlags) => (dwFlags & (int)AsAnyFlags.IsBestFit) != 0; + + private static AsAnyMarshalerImplementation CreateAnsiCharArrayImplementation(bool isOut, int dwFlags) + { + return (IsBestFit(dwFlags), IsThrowOn(dwFlags)) switch + { + (true, true) => new ArrayImplementation>(isOut), + (true, false) => new ArrayImplementation>(isOut), + (false, true) => new ArrayImplementation>(isOut), + (false, false) => new ArrayImplementation>(isOut), + }; } - private unsafe IntPtr ConvertLayoutToNative(object pManagedHome, int dwFlags) + internal AsAnyMarshaler(object? pManagedHome, int dwFlags) { - // Note that the following call will not throw exception if the type - // of pManagedHome is not marshalable. That's intentional because we - // want to maintain the original behavior where this was indicated - // by TypeLoadException during the actual field marshaling. - int allocSize = Marshal.SizeOfHelper((RuntimeType)pManagedHome.GetType(), false); - IntPtr pNativeHome = Marshal.AllocCoTaskMem(allocSize); + _impl = null; + + if (pManagedHome is null) + return; - // marshal the object as class with layout (UnmanagedType.LPStruct) - if (IsIn(dwFlags)) + if (pManagedHome is ArrayWithOffset) + throw new ArgumentException(SR.Arg_MarshalAsAnyRestriction); + + if (pManagedHome.GetType().IsArray) + { + Type elementType = pManagedHome.GetType().GetElementType()!; + bool isOut = IsOut(dwFlags); + _impl = Type.GetTypeCode(elementType) switch + { + TypeCode.SByte => new ArrayImplementation>(isOut), + TypeCode.Byte => new ArrayImplementation>(isOut), + TypeCode.Int16 => new ArrayImplementation>(isOut), + TypeCode.UInt16 => new ArrayImplementation>(isOut), + TypeCode.Int32 => new ArrayImplementation>(isOut), + TypeCode.UInt32 => new ArrayImplementation>(isOut), + TypeCode.Int64 => new ArrayImplementation>(isOut), + TypeCode.UInt64 => new ArrayImplementation>(isOut), + TypeCode.Single => new ArrayImplementation>(isOut), + TypeCode.Double => new ArrayImplementation>(isOut), + TypeCode.Object when elementType == typeof(nint) => new ArrayImplementation>(isOut), + TypeCode.Object when elementType == typeof(nuint) => new ArrayImplementation>(isOut), + TypeCode.Char when !IsAnsi(dwFlags) => new ArrayImplementation>(isOut), + TypeCode.Char when IsAnsi(dwFlags) => CreateAnsiCharArrayImplementation(isOut, dwFlags), + TypeCode.Boolean => new ArrayImplementation>(isOut), + _ => throw new ArgumentException(SR.Arg_PInvokeBadObject) + }; + } + else if (pManagedHome is string) + { + _impl = new StringImplementation(); + } + else if (pManagedHome is StringBuilder) + { + bool isOut = IsOut(dwFlags); + _impl = IsAnsi(dwFlags) + ? new StringBuilderAnsiImplementation(isOut) + : new StringBuilderUnicodeImplementation(isOut); + } + else if (pManagedHome.GetType().IsLayoutSequential || pManagedHome.GetType().IsExplicitLayout) { - StubHelpers.LayoutTypeConvertToUnmanaged(pManagedHome, (byte*)pNativeHome, ref cleanupWorkList); + _impl = new LayoutImplementation(pManagedHome.GetType(), IsOut(dwFlags)); } - if (IsOut(dwFlags)) + else { - backPropAction = BackPropAction.Layout; + throw new ArgumentException(SR.Arg_PInvokeBadObject); } - layoutType = pManagedHome.GetType(); + } - return pNativeHome; + internal IntPtr ConvertToNative(object pManagedHome, int dwFlags) + { + return _impl?.ConvertToNative(pManagedHome, dwFlags) ?? IntPtr.Zero; } - #endregion + internal void ConvertToManaged(object pManagedHome, IntPtr pNativeHome) + { + _impl?.ConvertToManaged(pManagedHome, pNativeHome); + } - internal IntPtr ConvertToNative(object pManagedHome, int dwFlags) + internal void ClearNative(IntPtr pNativeHome) { - if (pManagedHome == null) - return IntPtr.Zero; + if (_impl is not null) + { + _impl.ClearNative(pNativeHome); + } + else if (pNativeHome != IntPtr.Zero) + { + Marshal.FreeCoTaskMem(pNativeHome); + } + } + } // struct AsAnyMarshaler - if (pManagedHome is ArrayWithOffset) - throw new ArgumentException(SR.Arg_MarshalAsAnyRestriction); + internal interface IArrayMarshaler + where TSelf : IArrayMarshaler + { + static abstract unsafe void ConvertContentsToUnmanaged(Array managedArray, byte* unmanaged, int length); + static abstract unsafe void ConvertContentsToManaged(Array managedArray, byte* unmanaged, int length); + static abstract unsafe void FreeContents(byte* unmanaged, int length); + static abstract unsafe byte* AllocateSpaceForUnmanaged(Array? managedArray); + static abstract unsafe Array? AllocateSpaceForManaged(byte* unmanaged, int length); + } + + internal interface IArrayElementMarshaler : IArrayMarshaler + where TSelf : IArrayElementMarshaler, IArrayMarshaler + { + static unsafe void IArrayMarshaler.ConvertContentsToManaged(Array managedArray, byte* unmanaged, int length) + { + Span elements = new(ref Unsafe.As(ref MemoryMarshal.GetArrayDataReference(managedArray)), managedArray.Length); + for (int i = 0; i < length; i++) + { + TSelf.ConvertToManaged(ref elements[i], unmanaged); + unmanaged += TSelf.UnmanagedSize; + } + } - IntPtr pNativeHome; + static unsafe void IArrayMarshaler.ConvertContentsToUnmanaged(Array managedArray, byte* unmanaged, int length) + { + Span elements = new(ref Unsafe.As(ref MemoryMarshal.GetArrayDataReference(managedArray)), managedArray.Length); + for (int i = 0; i < length; i++) + { + TSelf.ConvertToUnmanaged(ref elements[i], unmanaged); + unmanaged += TSelf.UnmanagedSize; + } + } - if (pManagedHome.GetType().IsArray) + static unsafe void IArrayMarshaler.FreeContents(byte* unmanaged, int length) + { + for (int i = 0; i < length; i++) { - // array (LPArray) - pNativeHome = ConvertArrayToNative(pManagedHome, dwFlags); + TSelf.Free(unmanaged); + unmanaged += TSelf.UnmanagedSize; + } + } + + static unsafe byte* IArrayMarshaler.AllocateSpaceForUnmanaged(Array? managedArray) + { + if (managedArray is null) + { + return null; } else { - if (pManagedHome is string strValue) - { - // string (LPStr or LPWStr) - pNativeHome = ConvertStringToNative(strValue, dwFlags); - } - else if (pManagedHome is StringBuilder sbValue) - { - // StringBuilder (LPStr or LPWStr) - pNativeHome = ConvertStringBuilderToNative(sbValue, dwFlags); - } - else if (pManagedHome.GetType().IsLayoutSequential || pManagedHome.GetType().IsExplicitLayout) - { - // layout (LPStruct) - pNativeHome = ConvertLayoutToNative(pManagedHome, dwFlags); - } - else - { - // this type is not supported for AsAny marshaling - throw new ArgumentException(SR.Arg_PInvokeBadObject); - } + const nuint MaxSizeForInterop = 0x7ffffff0u; + nuint elementCount = (nuint)(uint)managedArray.Length; + nuint elementSize = TSelf.UnmanagedSize; + if (elementCount != 0 && elementSize > MaxSizeForInterop / elementCount) + throw new ArgumentException(SR.Argument_StructArrayTooLarge); + nuint nativeBytes = elementCount * elementSize; + byte* pNative = (byte*)Marshal.AllocCoTaskMem((int)nativeBytes); + NativeMemory.Clear(pNative, nativeBytes); + return pNative; } + } + + static unsafe Array? IArrayMarshaler.AllocateSpaceForManaged(byte* unmanaged, int length) + { + if (unmanaged is null) + { + return null; + } + else + { + return new T[length]; + } + } + + static abstract unsafe void ConvertToUnmanaged(ref T managed, byte* unmanaged); + static abstract unsafe void ConvertToManaged(ref T managed, byte* unmanaged); + static abstract unsafe void Free(byte* unmanaged); + + static abstract nuint UnmanagedSize { get; } + } + + // Constants for direction argument of struct marshalling stub. + internal static class MarshalOperation + { + internal const int ConvertToUnmanaged = 0; + internal const int ConvertToManaged = 1; + internal const int Free = 2; + } + + internal sealed class BlittableArrayMarshaler : IArrayMarshaler> + where T : unmanaged + { + static unsafe void IArrayMarshaler>.ConvertContentsToUnmanaged(Array managedArray, byte* unmanaged, int length) + { + SpanHelpers.Memmove(ref *unmanaged, ref MemoryMarshal.GetArrayDataReference(managedArray), (nuint)length * (nuint)sizeof(T)); + } + + static unsafe void IArrayMarshaler>.ConvertContentsToManaged(Array managedArray, byte* unmanaged, int length) + { + SpanHelpers.Memmove(ref MemoryMarshal.GetArrayDataReference(managedArray), ref *unmanaged, (nuint)length * (nuint)sizeof(T)); + } + + static unsafe void IArrayMarshaler>.FreeContents(byte* unmanaged, int length) + { + } + + static unsafe byte* IArrayMarshaler>.AllocateSpaceForUnmanaged(Array? managedArray) + { + if (managedArray is null) + return null; + + const nuint MaxSizeForInterop = 0x7ffffff0u; + nuint elementCount = (nuint)(uint)managedArray.Length; + nuint elementSize = (nuint)sizeof(T); + if (elementCount != 0 && elementSize > MaxSizeForInterop / elementCount) + throw new ArgumentException(SR.Argument_StructArrayTooLarge); + nuint nativeBytes = elementCount * elementSize; + byte* pNative = (byte*)Marshal.AllocCoTaskMem((int)nativeBytes); + NativeMemory.Clear(pNative, nativeBytes); + + return pNative; + } + + static unsafe Array? IArrayMarshaler>.AllocateSpaceForManaged(byte* unmanaged, int length) + { + if (unmanaged is null) + return null; + + return new T[length]; + } + } + + internal sealed unsafe class StructureMarshaler : IArrayElementMarshaler> where T : notnull + { + static unsafe void IArrayElementMarshaler>.ConvertToManaged(ref T managed, byte* unmanaged) + { + ConvertToManaged(ref managed, unmanaged, ref Unsafe.NullRef()); + } + + static unsafe void IArrayElementMarshaler>.ConvertToUnmanaged(ref T managed, byte* unmanaged) + { + ConvertToUnmanaged(ref managed, unmanaged, ref Unsafe.NullRef()); + } + + static unsafe void IArrayElementMarshaler>.Free(byte* unmanaged) + { + Free(ref Unsafe.NullRef(), unmanaged, ref Unsafe.NullRef()); + } + + static nuint IArrayElementMarshaler>.UnmanagedSize => (nuint)UnmanagedSize; + + private static class SizeHolder + { + public static readonly int UnmanagedSize = typeof(T).IsEnum ? Marshal.SizeOf(Enum.GetUnderlyingType(typeof(T))) : Marshal.SizeOf(); + } + + private static int UnmanagedSize + { + get + { + try + { + return SizeHolder.UnmanagedSize; + } + catch (TypeInitializationException ex) + { + ExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw(); + return 0; + } + } + } + + [Conditional("DEBUG")] + private static void Validate() + { + Debug.Assert(typeof(T).IsValueType, "StructureMarshaler can only be used for value types"); + RuntimeType type = (RuntimeType)typeof(T); + bool hasLayout = Marshal.HasLayout(new QCallTypeHandle(ref type), out bool isBlittable, out int _); + Debug.Assert(hasLayout, "Non-layout classes should not use the layout class marshaler."); + Debug.Assert(isBlittable, "Non-blittable structs should have a custom IL body generated with the marshaling logic."); + } + + [Intrinsic] + private static void ConvertToUnmanagedCore(ref T managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + { + Validate(); + _ = ref cleanupWorkList; + SpanHelpers.Memmove(ref *unmanaged, ref Unsafe.As(ref managed), (nuint)sizeof(T)); + } + + public static void ConvertToUnmanaged(ref T managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + { + try + { + NativeMemory.Clear(unmanaged, (nuint)UnmanagedSize); + ConvertToUnmanagedCore(ref managed, unmanaged, ref cleanupWorkList); + } + catch (Exception) + { + // If Free throws an exception (which it shouldn't as it can leak) + // let that exception supercede the exception from ConvertToUnmanagedCore. + Free(ref managed, unmanaged, ref cleanupWorkList); + throw; + } + } + + [Intrinsic] + public static void ConvertToManaged(ref T managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + { + Validate(); + _ = ref cleanupWorkList; + SpanHelpers.Memmove(ref Unsafe.As(ref managed), ref *unmanaged, (nuint)sizeof(T)); + } + + [Intrinsic] + private static void FreeCore(ref T managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + { + Validate(); +#nullable disable warnings // https://github.com/dotnet/roslyn/issues/82919 + _ = ref managed; +#nullable restore warnings + _ = unmanaged; + _ = ref cleanupWorkList; + } + + public static void Free(ref T managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + { + if (unmanaged != null) + { + FreeCore(ref managed, unmanaged, ref cleanupWorkList); + NativeMemory.Clear(unmanaged, (nuint)UnmanagedSize); + } + } + } + + internal sealed unsafe class LayoutClassMarshaler : IArrayElementMarshaler> where T : notnull + { + // We use a nested Methods class with properties that unwrap the TypeInitializationException + // to ensure that users see a TypeLoadException if the type has a recursive native layout. + // This also ensures that we don't leak internal implementation details about how we generate marshalling stubs. + private static class Methods + { + private static readonly delegate* _convertToUnmanaged; + private static readonly delegate* _convertToManaged; + private static readonly delegate* _free; + + private static readonly nuint s_unmanagedSize; + +#pragma warning disable CA1810 // Static constructor is required to initialize with the out parameters + static Methods() + { + RuntimeTypeHandle th = typeof(T).TypeHandle; + bool hasLayout = Marshal.HasLayout(new QCallTypeHandle(ref th), out bool isBlittable, out int nativeSize); + Debug.Assert(hasLayout, "Non-layout classes should not use the layout class marshaler."); + s_unmanagedSize = (nuint)nativeSize; + if (isBlittable) + { + _convertToUnmanaged = &BlittableConvertToUnmanaged; + _convertToManaged = &BlittableConvertToManaged; + _free = &BlittableFree; + } + else + { + StubHelpers.CreateLayoutClassMarshalStubs(new QCallTypeHandle(ref th), out _convertToUnmanaged, out _convertToManaged, out _free); + } + } +#pragma warning restore CA1810 + + private static void BlittableConvertToUnmanaged(ref byte managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + { + SpanHelpers.Memmove(ref *unmanaged, ref managed, s_unmanagedSize); + } + + private static void BlittableConvertToManaged(ref byte managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + { + SpanHelpers.Memmove(ref managed, ref *unmanaged, s_unmanagedSize); + } + + private static void BlittableFree(ref byte managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + { + // Nothing to do for blittable types. + } + + internal static delegate* ConvertToUnmanaged => _convertToUnmanaged; + + internal static delegate* ConvertToManaged => _convertToManaged; + + internal static delegate* Free => _free; + + internal static nuint UnmanagedSize => s_unmanagedSize; + } + + private static void ConvertToUnmanagedCore(T managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + { + try + { + CallConvertToUnmanaged(ref managed.GetRawData(), unmanaged, ref cleanupWorkList); + } + catch (TypeInitializationException ex) + { + ExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void CallConvertToUnmanaged(ref byte managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + { + Methods.ConvertToUnmanaged(ref managed, unmanaged, ref cleanupWorkList); + } + } + + public static void ConvertToUnmanaged(T managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + { + try + { + NativeMemory.Clear(unmanaged, UnmanagedSize); + ConvertToUnmanagedCore(managed, unmanaged, ref cleanupWorkList); + } + catch (Exception) + { + // If Free throws an exception (which it shouldn't as it can leak) + // let that exception supercede the exception from ConvertToUnmanagedCore. + Free(managed, unmanaged, ref cleanupWorkList); + throw; + } + } + + public static void ConvertToManaged(T managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + { + try + { + CallConvertToManaged(ref managed.GetRawData(), unmanaged, ref cleanupWorkList); + } + catch (TypeInitializationException ex) + { + ExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void CallConvertToManaged(ref byte managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + { + Methods.ConvertToManaged(ref managed, unmanaged, ref cleanupWorkList); + } + } + + private static void FreeCore(T? managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + { + try + { + CallFree(managed, unmanaged, ref cleanupWorkList); + } + catch (TypeInitializationException ex) + { + ExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void CallFree(T? managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + { + if (managed is null) + { + Methods.Free(ref Unsafe.NullRef(), unmanaged, ref cleanupWorkList); + } + else + { + Methods.Free(ref managed.GetRawData(), unmanaged, ref cleanupWorkList); + } + } + } + public static void Free(T? managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + { + if (unmanaged != null) + { + FreeCore(managed, unmanaged, ref cleanupWorkList); + NativeMemory.Clear(unmanaged, UnmanagedSize); + } + } + + private static nuint UnmanagedSize + { + [MethodImpl(MethodImplOptions.NoInlining)] + get + { + try + { + return Methods.UnmanagedSize; + } + catch (TypeInitializationException ex) + { + ExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw(); + // Unreachable + return 0; + } + } + } + + static unsafe void IArrayElementMarshaler>.ConvertToManaged(ref T managed, byte* unmanaged) + { + ConvertToManaged(managed, unmanaged, ref Unsafe.NullRef()); + } + + static unsafe void IArrayElementMarshaler>.ConvertToUnmanaged(ref T managed, byte* unmanaged) + { + ConvertToUnmanaged(managed, unmanaged, ref Unsafe.NullRef()); + } + + static unsafe void IArrayElementMarshaler>.Free(byte* unmanaged) + { + Free(default, unmanaged, ref Unsafe.NullRef()); + } + + static nuint IArrayElementMarshaler>.UnmanagedSize => UnmanagedSize; + } + + // Marshaller for layout classes and boxed structs. + internal static unsafe class BoxedLayoutTypeMarshaler where T : notnull + { + public static void ConvertToUnmanaged(object managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + { + if (typeof(T).IsValueType) + { + StructureMarshaler.ConvertToUnmanaged(ref Unsafe.As(ref managed.GetRawData()), unmanaged, ref cleanupWorkList); + } + else + { + LayoutClassMarshaler.ConvertToUnmanaged(Unsafe.As(ref managed), unmanaged, ref cleanupWorkList); + } + } + + public static void ConvertToManaged(object managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + { + if (typeof(T).IsValueType) + { + StructureMarshaler.ConvertToManaged(ref Unsafe.As(ref managed.GetRawData()), unmanaged, ref cleanupWorkList); + } + else + { + LayoutClassMarshaler.ConvertToManaged(Unsafe.As(ref managed), unmanaged, ref cleanupWorkList); + } + } + + public static void Free(object? managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + { + if (typeof(T).IsValueType) + { + ref byte managedRef = ref Unsafe.NullRef(); + + if (managed != null) + { + managedRef = ref managed.GetRawData(); + } + + StructureMarshaler.Free(ref Unsafe.As(ref managedRef), unmanaged, ref cleanupWorkList); + } + else + { + LayoutClassMarshaler.Free(Unsafe.As(ref managed), unmanaged, ref cleanupWorkList); + } + } + } + + internal sealed class VariantBoolMarshaler : IArrayElementMarshaler + { + private const ushort VARIANT_TRUE = unchecked((ushort)-1); + private const ushort VARIANT_FALSE = 0; + public static unsafe void ConvertToUnmanaged(ref bool managed, byte* unmanaged) + { + *(ushort*)unmanaged = managed ? VARIANT_TRUE : VARIANT_FALSE; + } + + public static unsafe void ConvertToManaged(ref bool managed, byte* unmanaged) + { + managed = (*(ushort*)unmanaged) != VARIANT_FALSE; + } + + public static unsafe void Free(byte* unmanaged) + { + _ = unmanaged; + // Nothing to free for VARIANT_BOOL. + } + + static nuint IArrayElementMarshaler.UnmanagedSize => (nuint)sizeof(short); + } + + internal sealed class BoolMarshaler : IArrayElementMarshaler> where TUnmanaged : unmanaged, INumberBase + { + public static unsafe void ConvertToUnmanaged(ref bool managed, byte* unmanaged) + { + TUnmanaged value = managed ? TUnmanaged.One : TUnmanaged.Zero; + Unsafe.WriteUnaligned(unmanaged, value); + } + + public static unsafe void ConvertToManaged(ref bool managed, byte* unmanaged) + { + TUnmanaged value = Unsafe.ReadUnaligned(unmanaged); + managed = !value.Equals(TUnmanaged.Zero); + } + + public static unsafe void Free(byte* unmanaged) + { + _ = unmanaged; + // Nothing to free for boolean values. + } + + static unsafe nuint IArrayElementMarshaler>.UnmanagedSize => (nuint)sizeof(TUnmanaged); + } + + internal sealed class LPWSTRMarshaler : IArrayElementMarshaler + { + public static unsafe void ConvertToUnmanaged(ref string? managed, byte* unmanaged) + { + IntPtr native = IntPtr.Zero; + + if (managed is not null) + { + int allocSize = (managed.Length + 1) * sizeof(char); + native = Marshal.AllocCoTaskMem(allocSize); + string.InternalCopy(managed, native, allocSize); + } + + *(IntPtr*)unmanaged = native; + } + + public static unsafe void ConvertToManaged(ref string? managed, byte* unmanaged) + { + IntPtr native = *(IntPtr*)unmanaged; + if (native == IntPtr.Zero) + { + managed = null; + } + else + { + StubHelpers.CheckStringLength((uint)string.wcslen((char*)native)); + managed = new string((char*)native); + } + } + + public static unsafe void Free(byte* unmanaged) + { + IntPtr pNativeHome = *(IntPtr*)unmanaged; + if (pNativeHome != IntPtr.Zero) + { + Marshal.FreeCoTaskMem(pNativeHome); + } + } + + static unsafe nuint IArrayElementMarshaler.UnmanagedSize => (nuint)sizeof(IntPtr); + } + + internal sealed class AnsiCharArrayMarshaler : IArrayMarshaler> + where TBestFit : IMarshalerOption + where TThrowOnUnmappable : IMarshalerOption + { + public static unsafe void ConvertContentsToUnmanaged(Array managedArray, byte* unmanaged, int length) + { + fixed (byte* pCharBytes = &MemoryMarshal.GetArrayDataReference(managedArray)) + { + char* pChars = (char*)pCharBytes; +#if TARGET_WINDOWS + uint flags = TBestFit.Enabled ? 0 : Interop.Kernel32.WC_NO_BEST_FIT_CHARS; + Interop.BOOL defaultCharUsed = Interop.BOOL.FALSE; + int result = Interop.Kernel32.WideCharToMultiByte( + Interop.Kernel32.CP_ACP, + flags, + pChars, + length, + unmanaged, + length, + null, + TThrowOnUnmappable.Enabled ? &defaultCharUsed : null); + + if (result == 0 && length > 0) + { + throw new ArgumentException(SR.Interop_Marshal_Unmappable_Char); + } + + if (defaultCharUsed != Interop.BOOL.FALSE) + { + throw new ArgumentException(SR.Interop_Marshal_Unmappable_Char); + } +#else + Encoding.UTF8.GetBytes(pChars, length, unmanaged, length); +#endif + } + + } + + public static unsafe void ConvertContentsToManaged(Array managedArray, byte* unmanaged, int length) + { + fixed (byte* pCharBytes = &MemoryMarshal.GetArrayDataReference(managedArray)) + { + char* pChars = (char*)pCharBytes; +#if TARGET_WINDOWS + int result = Interop.Kernel32.MultiByteToWideChar( + Interop.Kernel32.CP_ACP, + Interop.Kernel32.MB_PRECOMPOSED, + unmanaged, + length, + pChars, + length); + + if (result == 0 && length > 0) + { + Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); + } +#else + Encoding.UTF8.GetChars(unmanaged, length, pChars, length); +#endif + } + } - return pNativeHome; + public static unsafe void FreeContents(byte* unmanaged, int length) + { } - internal unsafe void ConvertToManaged(object pManagedHome, IntPtr pNativeHome) + public static unsafe byte* AllocateSpaceForUnmanaged(Array? managedArray) { - switch (backPropAction) + if (managedArray is null) { - case BackPropAction.Array: - { - MngdNativeArrayMarshaler.ConvertContentsToManaged( - pvArrayMarshaler, - in pManagedHome, - new IntPtr(&pNativeHome)); - break; - } - - case BackPropAction.Layout: - { - StubHelpers.LayoutTypeConvertToManaged(pManagedHome, (byte*)pNativeHome); - break; - } - - case BackPropAction.StringBuilderAnsi: - { - int length; - if (pNativeHome == IntPtr.Zero) - { - length = 0; - } - else - { - length = string.strlen((byte*)pNativeHome); - } - - ((StringBuilder)pManagedHome).ReplaceBufferAnsiInternal((sbyte*)pNativeHome, length); - break; - } - - case BackPropAction.StringBuilderUnicode: - { - int length; - if (pNativeHome == IntPtr.Zero) - { - length = 0; - } - else - { - length = string.wcslen((char*)pNativeHome); - } - - ((StringBuilder)pManagedHome).ReplaceBufferInternal((char*)pNativeHome, length); - break; - } - - // nothing to do for BackPropAction.None + return null; } + + // Native layout for ANSI char arrays uses 1 byte per element. + int allocSize = managedArray.Length; + byte* pNative = (byte*)Marshal.AllocCoTaskMem(allocSize); + NativeMemory.Clear(pNative, (nuint)allocSize); + return pNative; } - internal void ClearNative(IntPtr pNativeHome) + public static unsafe Array? AllocateSpaceForManaged(byte* unmanaged, int length) { - if (pNativeHome != IntPtr.Zero) + if (unmanaged is null) { - if (layoutType != null) - { - // this must happen regardless of BackPropAction - Marshal.DestroyStructure(pNativeHome, layoutType); - } - Marshal.FreeCoTaskMem(pNativeHome); + return null; } - StubHelpers.DestroyCleanupList(ref cleanupWorkList); - } - } // struct AsAnyMarshaler - // Constants for direction argument of struct marshalling stub. - internal static class MarshalOperation - { - internal const int ConvertToUnmanaged = 0; - internal const int ConvertToManaged = 1; - internal const int Free = 2; + return new char[length]; + } } - internal static unsafe class StructureMarshaler where T : notnull + internal sealed class LPSTRArrayElementMarshaler : IArrayElementMarshaler> + where TBestFit : IMarshalerOption + where TThrowOnUnmappable : IMarshalerOption { - // Blittable types have a no-op FreeCore (the [Intrinsic] C# body is used) and need no NativeMemory.Clear. - // Non-blittable types have a JIT-generated FreeCore stub and require NativeMemory.Clear after cleanup. - private static readonly bool s_isBlittable = InitIsBlittable(); - - private static bool InitIsBlittable() - { - RuntimeType type = (RuntimeType)typeof(T); - Marshal.HasLayout(new QCallTypeHandle(ref type), out bool isBlittable, out _); - return isBlittable; - } - - [Conditional("DEBUG")] - private static void Validate() + public static unsafe void ConvertToUnmanaged(ref string? managed, byte* unmanaged) { - Debug.Assert(typeof(T).IsValueType, "StructureMarshaler can only be used for value types"); - RuntimeType type = (RuntimeType)typeof(T); - bool hasLayout = Marshal.HasLayout(new QCallTypeHandle(ref type), out bool isBlittable, out int _); - Debug.Assert(hasLayout, "Non-layout structs should not be marshalable"); - Debug.Assert(isBlittable, "Non-blittable structs should have a custom IL body generated with the marshaling logic."); + int flags = (TBestFit.Enabled ? 0xFF : 0) | (TThrowOnUnmappable.Enabled ? 0xFF00 : 0); + *(IntPtr*)unmanaged = CSTRMarshaler.ConvertToNative(flags, managed, IntPtr.Zero); } - [Intrinsic] - private static void ConvertToUnmanagedCore(ref T managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + public static unsafe void ConvertToManaged(ref string? managed, byte* unmanaged) { - Validate(); - _ = ref cleanupWorkList; - SpanHelpers.Memmove(ref *unmanaged, ref Unsafe.As(ref managed), (nuint)sizeof(T)); + managed = CSTRMarshaler.ConvertToManaged(*(IntPtr*)unmanaged); } - public static void ConvertToUnmanaged(ref T managed, byte* unmanaged, int nativeSize, ref CleanupWorkListElement? cleanupWorkList) + public static unsafe void Free(byte* unmanaged) { - try - { - NativeMemory.Clear(unmanaged, (nuint)nativeSize); - ConvertToUnmanagedCore(ref managed, unmanaged, ref cleanupWorkList); - } - catch (Exception) + IntPtr ptr = *(IntPtr*)unmanaged; + if (ptr != IntPtr.Zero) { - // If Free throws an exception (which it shouldn't as it can leak) - // let that exception supercede the exception from ConvertToUnmanagedCore. - Free(ref managed, unmanaged, nativeSize, ref cleanupWorkList); - throw; + Marshal.FreeCoTaskMem(ptr); } } - [Intrinsic] - public static void ConvertToManaged(ref T managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + static unsafe nuint IArrayElementMarshaler>.UnmanagedSize => (nuint)sizeof(IntPtr); + } + + internal sealed class BSTRArrayElementMarshaler : IArrayElementMarshaler + { + public static unsafe void ConvertToUnmanaged(ref string? managed, byte* unmanaged) { - Validate(); - _ = ref cleanupWorkList; - SpanHelpers.Memmove(ref Unsafe.As(ref managed), ref *unmanaged, (nuint)sizeof(T)); + *(IntPtr*)unmanaged = BSTRMarshaler.ConvertToNative(managed, IntPtr.Zero); } - [Intrinsic] - private static void FreeCore(ref T managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + public static unsafe void ConvertToManaged(ref string? managed, byte* unmanaged) { - Validate(); -#nullable disable warnings // https://github.com/dotnet/roslyn/issues/82919 - _ = ref managed; -#nullable restore warnings - _ = unmanaged; - _ = ref cleanupWorkList; + managed = BSTRMarshaler.ConvertToManaged(*(IntPtr*)unmanaged); } - public static void Free(ref T managed, byte* unmanaged, int nativeSize, ref CleanupWorkListElement? cleanupWorkList) + public static unsafe void Free(byte* unmanaged) { - // For blittable types, FreeCore is a no-op and there are no native sub-structures to free. - // Calling NativeMemory.Clear on a potentially invalid pointer (e.g., in DestroyStructure tests) - // would cause a fault, so we skip cleanup entirely for blittable types. - if (unmanaged != null && !s_isBlittable) + IntPtr bstr = *(IntPtr*)unmanaged; + if (bstr != IntPtr.Zero) { - FreeCore(ref managed, unmanaged, ref cleanupWorkList); - NativeMemory.Clear(unmanaged, (nuint)nativeSize); + BSTRMarshaler.ClearNative(bstr); } } + + static unsafe nuint IArrayElementMarshaler.UnmanagedSize => (nuint)sizeof(IntPtr); } - internal static unsafe class LayoutClassMarshaler where T : notnull +#if FEATURE_COMINTEROP + internal sealed class CurrencyArrayElementMarshaler : IArrayElementMarshaler { - // We use a nested Methods class with properties that unwrap the TypeInitializationException - // to ensure that users see a TypeLoadException if the type has a recursive native layout. - // This also ensures that we don't leak internal implementation details about how we generate marshalling stubs. - private static class Methods + public static unsafe void ConvertToUnmanaged(ref decimal managed, byte* unmanaged) { - private static readonly delegate* _convertToUnmanaged; - private static readonly delegate* _convertToManaged; - private static readonly delegate* _free; + *(Currency*)unmanaged = new Currency(managed); + } - private static readonly nuint s_nativeSizeForBlittableTypes; + public static unsafe void ConvertToManaged(ref decimal managed, byte* unmanaged) + { + managed = new decimal(*(Currency*)unmanaged); + } -#pragma warning disable CA1810 // Static constructor is required to initialize with the out parameters - static Methods() - { - RuntimeTypeHandle th = typeof(T).TypeHandle; - bool hasLayout = Marshal.HasLayout(new QCallTypeHandle(ref th), out bool isBlittable, out int nativeSize); - Debug.Assert(hasLayout, "Non-layout classes should not use the layout class marshaler."); - if (isBlittable) - { - s_nativeSizeForBlittableTypes = (nuint)nativeSize; - _convertToUnmanaged = &BlittableConvertToUnmanaged; - _convertToManaged = &BlittableConvertToManaged; - _free = &BlittableFree; - } - else - { - s_nativeSizeForBlittableTypes = 0; - StubHelpers.CreateLayoutClassMarshalStubs(new QCallTypeHandle(ref th), out _convertToUnmanaged, out _convertToManaged, out _free); - } - } -#pragma warning restore CA1810 + public static unsafe void Free(byte* unmanaged) + { + } - private static void BlittableConvertToUnmanaged(ref byte managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + static unsafe nuint IArrayElementMarshaler.UnmanagedSize => (nuint)sizeof(Currency); + } + + [SupportedOSPlatform("windows")] + internal sealed class InterfaceArrayElementMarshaler : IArrayElementMarshaler> + where TIsDispatch : IMarshalerOption + { + public static unsafe void ConvertToUnmanaged(ref object? managed, byte* unmanaged) + { + if (managed is null) { - SpanHelpers.Memmove(ref *unmanaged, ref managed, s_nativeSizeForBlittableTypes); + *(IntPtr*)unmanaged = IntPtr.Zero; } - - private static void BlittableConvertToManaged(ref byte managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + else if (TIsDispatch.Enabled) { - SpanHelpers.Memmove(ref managed, ref *unmanaged, s_nativeSizeForBlittableTypes); + *(IntPtr*)unmanaged = Marshal.GetIDispatchForObject(managed); } - - private static void BlittableFree(ref byte managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + else { - // Nothing to do for blittable types. + *(IntPtr*)unmanaged = Marshal.GetIUnknownForObject(managed); } - - internal static delegate* ConvertToUnmanaged => _convertToUnmanaged; - - internal static delegate* ConvertToManaged => _convertToManaged; - - internal static delegate* Free => _free; - - // s_nativeSizeForBlittableTypes is non-zero for blittable types and zero for non-blittable types. - internal static bool IsBlittable => s_nativeSizeForBlittableTypes != 0; } - private static void ConvertToUnmanagedCore(T managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + public static unsafe void ConvertToManaged(ref object? managed, byte* unmanaged) { - try + IntPtr pUnk = *(IntPtr*)unmanaged; + if (pUnk == IntPtr.Zero) { - CallConvertToUnmanaged(ref managed.GetRawData(), unmanaged, ref cleanupWorkList); + managed = null; } - catch (TypeInitializationException ex) + else { - ExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw(); + managed = Marshal.GetObjectForIUnknown(pUnk); } + } - [MethodImpl(MethodImplOptions.NoInlining)] - static void CallConvertToUnmanaged(ref byte managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + public static unsafe void Free(byte* unmanaged) + { + IntPtr pUnk = *(IntPtr*)unmanaged; + if (pUnk != IntPtr.Zero) { - Methods.ConvertToUnmanaged(ref managed, unmanaged, ref cleanupWorkList); + Marshal.Release(pUnk); } } - public static void ConvertToUnmanaged(T managed, byte* unmanaged, int nativeSize, ref CleanupWorkListElement? cleanupWorkList) + static unsafe nuint IArrayElementMarshaler>.UnmanagedSize => (nuint)sizeof(IntPtr); + } + + [SupportedOSPlatform("windows")] + internal sealed class TypedInterfaceArrayElementMarshaler : IArrayElementMarshaler> + where T : class + { + public static unsafe void ConvertToUnmanaged(ref T? managed, byte* unmanaged) { - try + if (managed is null) { - NativeMemory.Clear(unmanaged, (nuint)nativeSize); - ConvertToUnmanagedCore(managed, unmanaged, ref cleanupWorkList); + *(IntPtr*)unmanaged = IntPtr.Zero; } - catch (Exception) + else { - // If Free throws an exception (which it shouldn't as it can leak) - // let that exception supercede the exception from ConvertToUnmanagedCore. - Free(managed, unmanaged, nativeSize, ref cleanupWorkList); - throw; + *(IntPtr*)unmanaged = Marshal.GetComInterfaceForObject(managed, typeof(T)); } } - public static void ConvertToManaged(T managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + public static unsafe void ConvertToManaged(ref T? managed, byte* unmanaged) { - try + IntPtr pUnk = *(IntPtr*)unmanaged; + if (pUnk == IntPtr.Zero) { - CallConvertToManaged(ref managed.GetRawData(), unmanaged, ref cleanupWorkList); + managed = null; } - catch (TypeInitializationException ex) + else { - ExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw(); + managed = (T)Marshal.GetObjectForIUnknown(pUnk); } + } - [MethodImpl(MethodImplOptions.NoInlining)] - static void CallConvertToManaged(ref byte managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + public static unsafe void Free(byte* unmanaged) + { + IntPtr pUnk = *(IntPtr*)unmanaged; + if (pUnk != IntPtr.Zero) { - Methods.ConvertToManaged(ref managed, unmanaged, ref cleanupWorkList); + Marshal.Release(pUnk); } } - private static void FreeCore(T? managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + static unsafe nuint IArrayElementMarshaler>.UnmanagedSize => (nuint)sizeof(IntPtr); + } + + [SupportedOSPlatform("windows")] + internal sealed class HeterogeneousInterfaceArrayElementMarshaler : IArrayElementMarshaler + { + public static unsafe void ConvertToUnmanaged(ref object? managed, byte* unmanaged) { - try - { - CallFree(managed, unmanaged, ref cleanupWorkList); - } - catch (TypeInitializationException ex) + if (managed is null) { - ExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw(); + *(IntPtr*)unmanaged = IntPtr.Zero; } - - [MethodImpl(MethodImplOptions.NoInlining)] - static void CallFree(T? managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + else { - if (managed is null) - { - Methods.Free(ref Unsafe.NullRef(), unmanaged, ref cleanupWorkList); - } - else - { - Methods.Free(ref managed.GetRawData(), unmanaged, ref cleanupWorkList); - } + // Resolve the default COM interface for each element based on its runtime type. + // This matches the heterogeneous path in MarshalInterfaceArrayComToOleHelper + // where GetDefaultInterfaceMTForClass is called per-element. + *(IntPtr*)unmanaged = Marshal.GetComInterfaceForObject(managed, managed.GetType()); } } - private static bool GetIsBlittable() + public static unsafe void ConvertToManaged(ref object? managed, byte* unmanaged) { - try + IntPtr pUnk = *(IntPtr*)unmanaged; + if (pUnk == IntPtr.Zero) { - return CallIsBlittable(); + managed = null; } - catch (TypeInitializationException ex) + else { - ExceptionDispatchInfo.Capture(ex.InnerException ?? ex).Throw(); - return false; // unreachable + managed = Marshal.GetObjectForIUnknown(pUnk); } - - [MethodImpl(MethodImplOptions.NoInlining)] - static bool CallIsBlittable() => Methods.IsBlittable; } - public static void Free(T? managed, byte* unmanaged, int nativeSize, ref CleanupWorkListElement? cleanupWorkList) + public static unsafe void Free(byte* unmanaged) { - // For blittable types, FreeCore is a no-op and there are no native sub-structures to free. - // Calling NativeMemory.Clear on a potentially invalid pointer (e.g., in DestroyStructure tests) - // would cause a fault, so we skip cleanup entirely for blittable types. - if (unmanaged != null && !GetIsBlittable()) + IntPtr pUnk = *(IntPtr*)unmanaged; + if (pUnk != IntPtr.Zero) { - FreeCore(managed, unmanaged, ref cleanupWorkList); - NativeMemory.Clear(unmanaged, (nuint)nativeSize); + Marshal.Release(pUnk); } } + + static unsafe nuint IArrayElementMarshaler.UnmanagedSize => (nuint)sizeof(IntPtr); } - // Marshaller for layout classes and boxed structs. - internal static unsafe class BoxedLayoutTypeMarshaler where T : notnull + internal sealed class VariantArrayElementMarshaler : IArrayElementMarshaler> + where TNativeDataValid : IMarshalerOption { - public static void ConvertToUnmanaged(object managed, byte* unmanaged, int nativeSize, ref CleanupWorkListElement? cleanupWorkList) + public static unsafe void ConvertToUnmanaged(ref object? managed, byte* unmanaged) { - if (typeof(T).IsValueType) - { - StructureMarshaler.ConvertToUnmanaged(ref Unsafe.As(ref managed.GetRawData()), unmanaged, nativeSize, ref cleanupWorkList); - } - else + if (!TNativeDataValid.Enabled) { - LayoutClassMarshaler.ConvertToUnmanaged(Unsafe.As(ref managed), unmanaged, nativeSize, ref cleanupWorkList); + // Native buffer is uninitialized — zero it so ConvertToNative + // doesn't see garbage VT_BYREF bits. + *(ComVariant*)unmanaged = default; } + // When TNativeDataValid is enabled, the existing VARIANT may have + // VT_BYREF set. ConvertToNative checks vt & VT_BYREF and calls + // MarshalOleRefVariantForObject to write through the byref pointer. + ObjectMarshaler.ConvertToNative(managed!, (IntPtr)unmanaged); } - public static void ConvertToManaged(object managed, byte* unmanaged, ref CleanupWorkListElement? cleanupWorkList) + public static unsafe void ConvertToManaged(ref object? managed, byte* unmanaged) { - if (typeof(T).IsValueType) - { - StructureMarshaler.ConvertToManaged(ref Unsafe.As(ref managed.GetRawData()), unmanaged, ref cleanupWorkList); - } - else - { - LayoutClassMarshaler.ConvertToManaged(Unsafe.As(ref managed), unmanaged, ref cleanupWorkList); - } + managed = ObjectMarshaler.ConvertToManaged((IntPtr)unmanaged); } - public static void Free(object? managed, byte* unmanaged, int nativeSize, ref CleanupWorkListElement? cleanupWorkList) + public static unsafe void Free(byte* unmanaged) { - if (typeof(T).IsValueType) - { - ref byte managedRef = ref Unsafe.NullRef(); + ObjectMarshaler.ClearNative((IntPtr)unmanaged); + } - if (managed != null) - { - managedRef = ref managed.GetRawData(); - } + static unsafe nuint IArrayElementMarshaler>.UnmanagedSize => (nuint)sizeof(ComVariant); + } +#endif // FEATURE_COMINTEROP - StructureMarshaler.Free(ref Unsafe.As(ref managedRef), unmanaged, nativeSize, ref cleanupWorkList); - } - else - { - LayoutClassMarshaler.Free(Unsafe.As(ref managed), unmanaged, nativeSize, ref cleanupWorkList); - } + internal interface IMarshalerOption + { + static abstract bool Enabled { get; } + + public sealed class EnabledOption : IMarshalerOption + { + public static bool Enabled => true; + } + + public sealed class DisabledOption : IMarshalerOption + { + public static bool Enabled => false; } } @@ -2032,56 +2458,35 @@ internal static unsafe void LayoutTypeConvertToManaged(object* obj, byte* pNativ } } - private static readonly MemberInfo StructureMarshalerConvertToUnmanaged = typeof(StructureMarshaler<>).GetMethod(nameof(StructureMarshaler<>.ConvertToUnmanaged))!; - private static readonly MemberInfo StructureMarshalerConvertToManaged = typeof(StructureMarshaler<>).GetMethod(nameof(StructureMarshaler<>.ConvertToManaged))!; - private static readonly MemberInfo StructureMarshalerFree = typeof(StructureMarshaler<>).GetMethod(nameof(StructureMarshaler<>.Free))!; - - private sealed unsafe class StructureMarshalInfo + public static unsafe void ConvertArrayContentsToUnmanaged(Array managed, byte* pNative, int numElements) + where TMarshaler : IArrayMarshaler { - public delegate* ConvertToUnmanaged; - public delegate* ConvertToManaged; - public delegate* Free; - - public int ManagedSize; + // Assert that the array is actually an array of compatible type. + Debug.Assert(managed is not null); + Debug.Assert(managed.GetType().GetElementType()!.MakeArrayType().IsAssignableTo(typeof(T[])), $"Managed array type {managed.GetType()} is not compatible with expected element type {typeof(T)}"); + TMarshaler.ConvertContentsToUnmanaged(managed, pNative, numElements); } - private static readonly ConditionalWeakTable s_structureMarshalInfoCache = []; - - private static unsafe StructureMarshalInfo GetStructureMarshalMethods(Type structureType) + public static unsafe void ConvertArrayContentsToManaged(Array managed, byte* pNative, int numElements) + where TMarshaler : IArrayMarshaler { - return s_structureMarshalInfoCache.GetOrAdd(structureType, static structureType => - { - Type structureMarshalerType = typeof(StructureMarshaler<>).MakeGenericType(structureType); - var convertToUnmanagedMethodInfo = (MethodInfo)structureMarshalerType.GetMemberWithSameMetadataDefinitionAs(StructureMarshalerConvertToUnmanaged)!; - var convertToManagedMethodInfo = (MethodInfo)structureMarshalerType.GetMemberWithSameMetadataDefinitionAs(StructureMarshalerConvertToManaged)!; - var freeMethodInfo = (MethodInfo)structureMarshalerType.GetMemberWithSameMetadataDefinitionAs(StructureMarshalerFree)!; - - return new StructureMarshalInfo - { - ConvertToUnmanaged = (delegate*)convertToUnmanagedMethodInfo.MethodHandle.GetFunctionPointer(), - ConvertToManaged = (delegate*)convertToManagedMethodInfo.MethodHandle.GetFunctionPointer(), - Free = (delegate*)freeMethodInfo.MethodHandle.GetFunctionPointer(), - ManagedSize = RuntimeHelpers.SizeOf(structureType.TypeHandle) - }; - }); + // Assert that the array is actually an array of compatible type. + Debug.Assert(managed is not null); + Debug.Assert(managed.GetType().GetElementType()!.MakeArrayType().IsAssignableTo(typeof(T[])), $"Managed array type {managed.GetType()} is not compatible with expected element type {typeof(T)}"); + TMarshaler.ConvertContentsToManaged(managed, pNative, numElements); } [UnmanagedCallersOnly] - internal static unsafe void NonBlittableStructureArrayConvertToUnmanaged(Array* managedArray, byte* pNative, MethodTable* pInterfaceMT, int nativeSize, Exception* pException) + internal static unsafe void InvokeArrayContentsConverter( + Array* pManagedArray, + byte* pNative, + int numElements, + delegate* pConvertMethod, + Exception* pException) { try { - StructureMarshalInfo marshalInfo = GetStructureMarshalMethods(RuntimeTypeHandle.GetRuntimeTypeFromHandle((IntPtr)pInterfaceMT)); - - nint length = (nint)managedArray->Length * (nint)marshalInfo.ManagedSize; - - for (ref byte managedElement = ref MemoryMarshal.GetArrayDataReference(*managedArray), end = ref Unsafe.AddByteOffset(ref managedElement, length); - Unsafe.IsAddressLessThan(ref managedElement, ref end); - managedElement = ref Unsafe.AddByteOffset(ref managedElement, marshalInfo.ManagedSize)) - { - marshalInfo.ConvertToUnmanaged(ref managedElement, pNative, nativeSize, ref Unsafe.NullRef()); - pNative += nativeSize; - } + pConvertMethod(*pManagedArray, pNative, numElements); } catch (Exception ex) { @@ -2089,48 +2494,38 @@ internal static unsafe void NonBlittableStructureArrayConvertToUnmanaged(Array* } } - [UnmanagedCallersOnly] - internal static unsafe void NonBlittableStructureArrayConvertToManaged(Array* managedArray, byte* pNative, MethodTable* pInterfaceMT, int nativeSize, Exception* pException) + internal static unsafe void FreeArrayContents(byte* pNative, int length) where TMarshaler : IArrayMarshaler { - try - { - StructureMarshalInfo marshalInfo = GetStructureMarshalMethods(RuntimeTypeHandle.GetRuntimeTypeFromHandle((IntPtr)pInterfaceMT)); - - nint length = (nint)managedArray->Length * (nint)marshalInfo.ManagedSize; + TMarshaler.FreeContents(pNative, length); + } - for (ref byte managedElement = ref MemoryMarshal.GetArrayDataReference(*managedArray), end = ref Unsafe.AddByteOffset(ref managedElement, length); - Unsafe.IsAddressLessThan(ref managedElement, ref end); - managedElement = ref Unsafe.AddByteOffset(ref managedElement, marshalInfo.ManagedSize)) - { - marshalInfo.ConvertToManaged(ref managedElement, pNative, ref Unsafe.NullRef()); - pNative += nativeSize; - } - } - catch (Exception ex) - { - *pException = ex; - } + public static unsafe byte* ConvertArraySpaceToNative(Array? managed) + where TMarshaler : IArrayMarshaler + { + return TMarshaler.AllocateSpaceForUnmanaged(managed); } - [UnmanagedCallersOnly] - internal static unsafe void NonBlittableStructureArrayFree(byte* pArray, nuint numElements, MethodTable* pInterfaceMT, int nativeSize, Exception* pException) + internal static unsafe Array? ConvertArraySpaceToManaged(byte* pNativeHome, int cElements) + where TMarshaler : IArrayMarshaler { - try - { - StructureMarshalInfo marshalInfo = GetStructureMarshalMethods(RuntimeTypeHandle.GetRuntimeTypeFromHandle((IntPtr)pInterfaceMT)); + return TMarshaler.AllocateSpaceForManaged(pNativeHome, cElements); + } - for (nuint i = 0; i < numElements; i++) - { - marshalInfo.Free(ref Unsafe.NullRef(), pArray, nativeSize, ref Unsafe.NullRef()); - pArray += nativeSize; - } - } - catch (Exception ex) + internal static unsafe void ClearArrayNative(byte* pNativeHome, int cElements) + where TMarshaler : IArrayMarshaler + { + if (pNativeHome != null) { - *pException = ex; + FreeArrayContents(pNativeHome, cElements); + Marshal.FreeCoTaskMem((IntPtr)pNativeHome); } } + internal static void ThrowWrongSizeArrayInNativeStruct() + { + throw new ArgumentException(SR.Argument_WrongSizeArrayInNativeStruct); + } + [LibraryImport(RuntimeHelpers.QCall, EntryPoint="StubHelpers_MarshalToManagedVaList")] internal static partial void MarshalToManagedVaList(IntPtr va_list, IntPtr pArgIterator); diff --git a/src/coreclr/vm/CMakeLists.txt b/src/coreclr/vm/CMakeLists.txt index aca63f44539e65..dff4181bdcbeab 100644 --- a/src/coreclr/vm/CMakeLists.txt +++ b/src/coreclr/vm/CMakeLists.txt @@ -350,7 +350,6 @@ set(VM_SOURCES_WKS nativeeventsource.cpp nativelibrary.cpp nativelibrarynative.cpp - olevariant.cpp pendingload.cpp pinvokeoverride.cpp profdetach.cpp @@ -457,7 +456,6 @@ set(VM_HEADERS_WKS multicorejit.h multicorejitimpl.h nativeeventsource.h - olevariant.h pendingload.h profdetach.h profilingenumerators.h @@ -597,6 +595,7 @@ if(CLR_CMAKE_TARGET_WIN32) dispatchinfo.cpp dispparammarshaler.cpp olecontexthelpers.cpp + olevariant.cpp runtimecallablewrapper.cpp stdinterfaces.cpp stdinterfaces_wrapper.cpp @@ -613,6 +612,7 @@ if(CLR_CMAKE_TARGET_WIN32) dispatchinfo.h dispparammarshaler.h olecontexthelpers.h + olevariant.h runtimecallablewrapper.h stdinterfaces.h stdinterfaces_internal.h diff --git a/src/coreclr/vm/binder.cpp b/src/coreclr/vm/binder.cpp index 0a2326d1143def..877d0b1ec39f1b 100644 --- a/src/coreclr/vm/binder.cpp +++ b/src/coreclr/vm/binder.cpp @@ -19,7 +19,6 @@ #include "dllimport.h" #include "clrvarargs.h" #include "sigbuilder.h" -#include "olevariant.h" #include "configuration.h" #include "conditionalweaktable.h" #include "interoplibinterface_comwrappers.h" diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index 2d0680d41c6d19..30c5e748d594de 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -147,7 +147,6 @@ #include "eventtrace.h" #include "corhost.h" #include "binder.h" -#include "olevariant.h" #include "comcallablewrapper.h" #include "../dlls/mscorrc/resource.h" #include "util.hpp" diff --git a/src/coreclr/vm/comcallablewrapper.cpp b/src/coreclr/vm/comcallablewrapper.cpp index 3c3e8c35c3c124..3bd5be1351ff10 100644 --- a/src/coreclr/vm/comcallablewrapper.cpp +++ b/src/coreclr/vm/comcallablewrapper.cpp @@ -21,7 +21,6 @@ #include "method.hpp" #include "class.h" #include "runtimecallablewrapper.h" -#include "olevariant.h" #include "cachelinealloc.h" #include "threads.h" #include "ceemain.h" diff --git a/src/coreclr/vm/corelib.h b/src/coreclr/vm/corelib.h index 19182570116edd..512320b3ca99b2 100644 --- a/src/coreclr/vm/corelib.h +++ b/src/coreclr/vm/corelib.h @@ -1077,9 +1077,14 @@ DEFINE_METHOD(STUBHELPERS, CHECK_STRING_LENGTH, CheckStringLength, DEFINE_METHOD(STUBHELPERS, LAYOUT_TYPE_CONVERT_TO_UNMANAGED, LayoutTypeConvertToUnmanaged, SM_PtrObj_PtrByte_PtrException_RetVoid) DEFINE_METHOD(STUBHELPERS, LAYOUT_TYPE_CONVERT_TO_MANAGED, LayoutTypeConvertToManaged, SM_PtrObj_PtrByte_PtrException_RetVoid) -DEFINE_METHOD(STUBHELPERS, NONBLITTABLE_STRUCTURE_ARRAY_CONVERT_TO_UNMANAGED, NonBlittableStructureArrayConvertToUnmanaged, NoSig) -DEFINE_METHOD(STUBHELPERS, NONBLITTABLE_STRUCTURE_ARRAY_CONVERT_TO_MANAGED, NonBlittableStructureArrayConvertToManaged, NoSig) -DEFINE_METHOD(STUBHELPERS, NONBLITTABLE_STRUCTURE_ARRAY_FREE, NonBlittableStructureArrayFree, NoSig) +DEFINE_METHOD(STUBHELPERS, CONVERT_ARRAY_CONTENTS_TO_UNMANAGED, ConvertArrayContentsToUnmanaged, NoSig) +DEFINE_METHOD(STUBHELPERS, CONVERT_ARRAY_CONTENTS_TO_MANAGED, ConvertArrayContentsToManaged, NoSig) +DEFINE_METHOD(STUBHELPERS, FREE_ARRAY_CONTENTS, FreeArrayContents, NoSig) +DEFINE_METHOD(STUBHELPERS, CONVERT_ARRAY_SPACE_TO_NATIVE, ConvertArraySpaceToNative, NoSig) +DEFINE_METHOD(STUBHELPERS, CONVERT_ARRAY_SPACE_TO_MANAGED, ConvertArraySpaceToManaged, NoSig) +DEFINE_METHOD(STUBHELPERS, CLEAR_ARRAY_NATIVE, ClearArrayNative, NoSig) +DEFINE_METHOD(STUBHELPERS, INVOKE_ARRAY_CONTENTS_CONVERTER, InvokeArrayContentsConverter, NoSig) +DEFINE_METHOD(STUBHELPERS, THROW_WRONG_SIZE_ARRAY_IN_NSTRUCT, ThrowWrongSizeArrayInNativeStruct, SM_RetVoid) #ifdef FEATURE_COMINTEROP DEFINE_CLASS(IDISPATCHHELPERS, Interop, IDispatchHelpers) @@ -1166,7 +1171,7 @@ DEFINE_METHOD(INTERFACEMARSHALER, VALIDATE_COM_VISIBILITY_FOR_IUNKNOWN, Valida DEFINE_CLASS(MNGD_SAFE_ARRAY_MARSHALER, StubHelpers, MngdSafeArrayMarshaler) -DEFINE_METHOD(MNGD_SAFE_ARRAY_MARSHALER, CREATE_MARSHALER, CreateMarshaler, SM_IntPtr_IntPtr_Int_Int_RetVoid) +DEFINE_METHOD(MNGD_SAFE_ARRAY_MARSHALER, CREATE_MARSHALER, CreateMarshaler, SM_IntPtr_IntPtr_Int_Int_IntPtr_IntPtr_RetVoid) DEFINE_METHOD(MNGD_SAFE_ARRAY_MARSHALER, CONVERT_SPACE_TO_NATIVE, ConvertSpaceToNative, SM_IntPtr_RefObj_IntPtr_RetVoid) DEFINE_METHOD(MNGD_SAFE_ARRAY_MARSHALER, CONVERT_CONTENTS_TO_NATIVE, ConvertContentsToNative, SM_IntPtr_RefObj_IntPtr_Obj_RetVoid) DEFINE_METHOD(MNGD_SAFE_ARRAY_MARSHALER, CONVERT_SPACE_TO_MANAGED, ConvertSpaceToManaged, SM_IntPtr_RefObj_IntPtr_RetVoid) @@ -1196,23 +1201,6 @@ DEFINE_METHOD(VBBYVALSTRMARSHALER, CONVERT_TO_NATIVE, ConvertToNative, DEFINE_METHOD(VBBYVALSTRMARSHALER, CONVERT_TO_MANAGED, ConvertToManaged, SM_IntPtr_Int_RetStr) DEFINE_METHOD(VBBYVALSTRMARSHALER, CLEAR_NATIVE, ClearNative, SM_IntPtr_RetVoid) -DEFINE_CLASS(MNGD_NATIVE_ARRAY_MARSHALER, StubHelpers, MngdNativeArrayMarshaler) -DEFINE_METHOD(MNGD_NATIVE_ARRAY_MARSHALER, CREATE_MARSHALER, CreateMarshaler, SM_IntPtr_IntPtr_Int_Bool_RetVoid) -DEFINE_METHOD(MNGD_NATIVE_ARRAY_MARSHALER, CONVERT_SPACE_TO_NATIVE, ConvertSpaceToNative, SM_IntPtr_RefObj_IntPtr_RetVoid) -DEFINE_METHOD(MNGD_NATIVE_ARRAY_MARSHALER, CONVERT_CONTENTS_TO_NATIVE, ConvertContentsToNative, SM_IntPtr_RefObj_IntPtr_RetVoid) -DEFINE_METHOD(MNGD_NATIVE_ARRAY_MARSHALER, CONVERT_SPACE_TO_MANAGED, ConvertSpaceToManaged, SM_IntPtr_RefObj_IntPtr_Int_RetVoid) -DEFINE_METHOD(MNGD_NATIVE_ARRAY_MARSHALER, CONVERT_CONTENTS_TO_MANAGED, ConvertContentsToManaged, SM_IntPtr_RefObj_IntPtr_RetVoid) -DEFINE_METHOD(MNGD_NATIVE_ARRAY_MARSHALER, CLEAR_NATIVE, ClearNative, SM_IntPtr_IntPtr_Int_RetVoid) -DEFINE_METHOD(MNGD_NATIVE_ARRAY_MARSHALER, CLEAR_NATIVE_CONTENTS, ClearNativeContents, SM_IntPtr_IntPtr_Int_RetVoid) - -DEFINE_CLASS(MNGD_FIXED_ARRAY_MARSHALER, StubHelpers, MngdFixedArrayMarshaler) -DEFINE_METHOD(MNGD_FIXED_ARRAY_MARSHALER, CREATE_MARSHALER, CreateMarshaler, SM_IntPtr_IntPtr_Int_Int_RetVoid) -DEFINE_METHOD(MNGD_FIXED_ARRAY_MARSHALER, CONVERT_SPACE_TO_NATIVE, ConvertSpaceToNative, SM_IntPtr_RefObj_IntPtr_RetVoid) -DEFINE_METHOD(MNGD_FIXED_ARRAY_MARSHALER, CONVERT_CONTENTS_TO_NATIVE, ConvertContentsToNative, SM_IntPtr_RefObj_IntPtr_RetVoid) -DEFINE_METHOD(MNGD_FIXED_ARRAY_MARSHALER, CONVERT_SPACE_TO_MANAGED, ConvertSpaceToManaged, SM_IntPtr_RefObj_IntPtr_RetVoid) -DEFINE_METHOD(MNGD_FIXED_ARRAY_MARSHALER, CONVERT_CONTENTS_TO_MANAGED, ConvertContentsToManaged, SM_IntPtr_RefObj_IntPtr_RetVoid) -DEFINE_METHOD(MNGD_FIXED_ARRAY_MARSHALER, CLEAR_NATIVE_CONTENTS, ClearNativeContents, SM_IntPtr_RefObj_IntPtr_RetVoid) - DEFINE_CLASS(MNGD_REF_CUSTOM_MARSHALER, StubHelpers, MngdRefCustomMarshaler) DEFINE_METHOD(MNGD_REF_CUSTOM_MARSHALER, CONVERT_CONTENTS_TO_NATIVE, ConvertContentsToNative, SM_ICustomMarshaler_RefObj_PtrIntPtr_RetVoid) DEFINE_METHOD(MNGD_REF_CUSTOM_MARSHALER, CONVERT_CONTENTS_TO_MANAGED, ConvertContentsToManaged, SM_ICustomMarshaler_RefObj_PtrIntPtr_RetVoid) @@ -1224,7 +1212,7 @@ DEFINE_METHOD(MNGD_REF_CUSTOM_MARSHALER, CLEAR_MANAGED_UCO, ClearM DEFINE_METHOD(MNGD_REF_CUSTOM_MARSHALER, GET_CUSTOM_MARSHALER_INSTANCE, GetCustomMarshalerInstance, SM_PtrVoid_PtrByte_Int_PtrObj_PtrException_RetVoid) DEFINE_CLASS(ASANY_MARSHALER, StubHelpers, AsAnyMarshaler) -DEFINE_METHOD(ASANY_MARSHALER, CTOR, .ctor, IM_IntPtr_RetVoid) +DEFINE_METHOD(ASANY_MARSHALER, CTOR, .ctor, IM_Obj_Int_RetVoid) DEFINE_METHOD(ASANY_MARSHALER, CONVERT_TO_NATIVE, ConvertToNative, IM_Obj_Int_RetIntPtr) DEFINE_METHOD(ASANY_MARSHALER, CONVERT_TO_MANAGED, ConvertToManaged, IM_Obj_IntPtr_RetVoid) DEFINE_METHOD(ASANY_MARSHALER, CLEAR_NATIVE, ClearNative, IM_IntPtr_RetVoid) @@ -1235,6 +1223,7 @@ DEFINE_METHOD(HANDLE_MARSHALER, THROW_SAFEHANDLE_FIELD_CHANGED, ThrowSa DEFINE_METHOD(HANDLE_MARSHALER, THROW_CRITICALHANDLE_FIELD_CHANGED, ThrowCriticalHandleFieldChanged, SM_RetVoid) DEFINE_CLASS(STRUCTURE_MARSHALER, StubHelpers, StructureMarshaler`1) +DEFINE_CLASS(BLITTABLE_ARRAY_MARSHALER, StubHelpers, BlittableArrayMarshaler`1) DEFINE_METHOD(STRUCTURE_MARSHALER, CONVERT_TO_MANAGED, ConvertToManaged, NoSig) DEFINE_METHOD(STRUCTURE_MARSHALER, CONVERT_TO_UNMANAGED, ConvertToUnmanaged, NoSig) DEFINE_METHOD(STRUCTURE_MARSHALER, CONVERT_TO_UNMANAGED_CORE, ConvertToUnmanagedCore, NoSig) @@ -1255,6 +1244,22 @@ DEFINE_METHOD(BOXEDLAYOUTTYPE_MARSHALER, FREE, Free, DEFINE_CLASS(COMVARIANT, Marshalling, ComVariant) +DEFINE_CLASS(VARIANT_BOOL_MARSHALER, StubHelpers, VariantBoolMarshaler) +DEFINE_CLASS(BOOL_MARSHALER, StubHelpers, BoolMarshaler`1) +DEFINE_CLASS(LPWSTR_MARSHALER, StubHelpers, LPWSTRMarshaler) +DEFINE_CLASS(ANSICHAR_ARRAY_ELEMENT_MARSHALER, StubHelpers, AnsiCharArrayMarshaler`2) +DEFINE_CLASS(LPSTR_ARRAY_ELEMENT_MARSHALER, StubHelpers, LPSTRArrayElementMarshaler`2) +DEFINE_CLASS(BSTR_ARRAY_ELEMENT_MARSHALER, StubHelpers, BSTRArrayElementMarshaler) +#ifdef FEATURE_COMINTEROP +DEFINE_CLASS(CURRENCY_ARRAY_ELEMENT_MARSHALER, StubHelpers, CurrencyArrayElementMarshaler) +DEFINE_CLASS(INTERFACE_ARRAY_ELEMENT_MARSHALER, StubHelpers, InterfaceArrayElementMarshaler`1) +DEFINE_CLASS(TYPED_INTERFACE_ARRAY_ELEMENT_MARSHALER, StubHelpers, TypedInterfaceArrayElementMarshaler`1) +DEFINE_CLASS(HETEROGENEOUS_INTERFACE_ARRAY_ELEMENT_MARSHALER, StubHelpers, HeterogeneousInterfaceArrayElementMarshaler) +DEFINE_CLASS(VARIANT_ARRAY_ELEMENT_MARSHALER, StubHelpers, VariantArrayElementMarshaler`1) +#endif // FEATURE_COMINTEROP +DEFINE_CLASS(MARSHALER_OPTION_ENABLED, StubHelpers, IMarshalerOption+EnabledOption) +DEFINE_CLASS(MARSHALER_OPTION_DISABLED, StubHelpers, IMarshalerOption+DisabledOption) + DEFINE_CLASS(SZARRAYHELPER, System, SZArrayHelper) // Note: The order of methods here has to match order they are implemented on the interfaces in // IEnumerable`1 diff --git a/src/coreclr/vm/dispatchinfo.cpp b/src/coreclr/vm/dispatchinfo.cpp index 68aed360cd9b8a..f5f578cfd191f1 100644 --- a/src/coreclr/vm/dispatchinfo.cpp +++ b/src/coreclr/vm/dispatchinfo.cpp @@ -700,7 +700,7 @@ void DispatchMemberInfo::SetUpMethodMarshalerInfo(MethodDesc *pMD, BOOL bReturnV { THROWS; GC_TRIGGERS; - MODE_ANY; + MODE_COOPERATIVE; PRECONDITION(CheckPointer(pMD)); PRECONDITION(!pMD->IsAsyncMethod()); } @@ -842,10 +842,7 @@ void DispatchMemberInfo::SetUpDispParamMarshalerForMarshalInfo(int iParam, Marsh { CONTRACTL { - THROWS; - GC_TRIGGERS; - MODE_ANY; - INJECT_FAULT(COMPlusThrowOM()); + STANDARD_VM_CHECK; PRECONDITION(CheckPointer(pInfo)); } CONTRACTL_END; @@ -1636,7 +1633,7 @@ void DispatchInfo::InvokeMemberWorker(DispatchMemberInfo* pDispMemberInfo, { // VarArg scenario // Here we only unmarshal the object whose corresponding VARIANT is VarArg - OleVariant::MarshalVariantArrayComToOle((BASEARRAYREF*)&pObjs->TmpObj, (void *)(aByrefArgOleVariant[i]), NULL, TRUE, FALSE, TRUE, TRUE, -1); + OleVariant::MarshalVarArgVariantArrayToOle((PTRARRAYREF*)&pObjs->TmpObj, (aByrefArgOleVariant[i])); } else { @@ -2154,7 +2151,13 @@ void DispatchInfo::MarshalParamManagedToNativeRef(DispatchMemberInfo *pMemberInf MethodTable *pElementMT = (*(BASEARRAYREF *)pSrcObj)->GetArrayElementTypeHandle().GetMethodTable(); // Convert the contents of the managed array into the original SAFEARRAY. - OleVariant::MarshalSafeArrayForArrayRef((BASEARRAYREF *)pSrcObj, *V_ARRAYREF(pRefVar), ElementVt, pElementMT); + PCODE pConvertCode; + { + GCX_PREEMP(); + pConvertCode = GetInstantiatedSafeArrayMethod(METHOD__STUBHELPERS__CONVERT_ARRAY_CONTENTS_TO_UNMANAGED, ElementVt, pElementMT, TRUE)->GetMultiCallableAddrOfCode(); + } + + OleVariant::MarshalSafeArrayForArrayRef((BASEARRAYREF *)pSrcObj, *V_ARRAYREF(pRefVar), ElementVt, pElementMT, pConvertCode); } else { diff --git a/src/coreclr/vm/dispparammarshaler.cpp b/src/coreclr/vm/dispparammarshaler.cpp index 1c9c9a22d41a4a..792ffcf3ad2450 100644 --- a/src/coreclr/vm/dispparammarshaler.cpp +++ b/src/coreclr/vm/dispparammarshaler.cpp @@ -212,6 +212,21 @@ void DispParamInterfaceMarshaler::MarshalManagedToNative(OBJECTREF *pSrcObj, VAR V_VT(pDestVar) = static_cast(m_bDispatch ? VT_DISPATCH : VT_UNKNOWN); } +DispParamArrayMarshaler::DispParamArrayMarshaler(VARTYPE ElementVT, MethodTable *pElementMT) : + m_ElementVT(ElementVT), + m_pElementMT(pElementMT), + m_pConvertContentsToManagedCode(NULL), + m_pConvertContentsToUnmanagedCode(NULL) +{ + STANDARD_VM_CONTRACT; + + if (ElementVT != VT_EMPTY && pElementMT != NULL) + { + m_pConvertContentsToManagedCode = GetInstantiatedSafeArrayMethod(METHOD__STUBHELPERS__CONVERT_ARRAY_CONTENTS_TO_MANAGED, ElementVT, pElementMT, FALSE)->GetMultiCallableAddrOfCode(); + m_pConvertContentsToUnmanagedCode = GetInstantiatedSafeArrayMethod(METHOD__STUBHELPERS__CONVERT_ARRAY_CONTENTS_TO_UNMANAGED, ElementVT, pElementMT, FALSE)->GetMultiCallableAddrOfCode(); + } +} + void DispParamArrayMarshaler::MarshalNativeToManaged(VARIANT *pSrcVar, OBJECTREF *pDestObj) { CONTRACTL @@ -251,7 +266,13 @@ void DispParamArrayMarshaler::MarshalNativeToManaged(VARIANT *pSrcVar, OBJECTREF *(BASEARRAYREF*)pDestObj = OleVariant::CreateArrayRefForSafeArray(pSafeArray, vt, pElemMT); // Convert the contents of the SAFEARRAY. - OleVariant::MarshalArrayRefForSafeArray(pSafeArray, (BASEARRAYREF*)pDestObj, vt, pElemMT); + PCODE pConvertCode = m_pConvertContentsToManagedCode; + if (pConvertCode == NULL) + { + GCX_PREEMP(); + pConvertCode = GetInstantiatedSafeArrayMethod(METHOD__STUBHELPERS__CONVERT_ARRAY_CONTENTS_TO_MANAGED, vt, pElemMT, FALSE)->GetMultiCallableAddrOfCode(); + } + OleVariant::MarshalArrayRefForSafeArray(pSafeArray, (BASEARRAYREF*)pDestObj, vt, pElemMT, pConvertCode); } void DispParamArrayMarshaler::MarshalManagedToNative(OBJECTREF *pSrcObj, VARIANT *pDestVar) @@ -290,7 +311,13 @@ void DispParamArrayMarshaler::MarshalManagedToNative(OBJECTREF *pSrcObj, VARIANT _ASSERTE(pSafeArray); // Marshal the contents of the SAFEARRAY. - OleVariant::MarshalSafeArrayForArrayRef((BASEARRAYREF*)pSrcObj, pSafeArray, vt, pElemMT); + PCODE pConvertCode = m_pConvertContentsToUnmanagedCode; + if (pConvertCode == NULL) + { + GCX_PREEMP(); + pConvertCode = GetInstantiatedSafeArrayMethod(METHOD__STUBHELPERS__CONVERT_ARRAY_CONTENTS_TO_UNMANAGED, vt, pElemMT, FALSE)->GetMultiCallableAddrOfCode(); + } + OleVariant::MarshalSafeArrayForArrayRef((BASEARRAYREF*)pSrcObj, pSafeArray, vt, pElemMT, pConvertCode); } // Store the resulting SAFEARRAY in the destination VARIANT. diff --git a/src/coreclr/vm/dispparammarshaler.h b/src/coreclr/vm/dispparammarshaler.h index 51d4a359da420e..3249598f482622 100644 --- a/src/coreclr/vm/dispparammarshaler.h +++ b/src/coreclr/vm/dispparammarshaler.h @@ -128,12 +128,7 @@ class DispParamInterfaceMarshaler : public DispParamMarshaler class DispParamArrayMarshaler : public DispParamMarshaler { public: - DispParamArrayMarshaler(VARTYPE ElementVT, MethodTable *pElementMT) : - m_ElementVT(ElementVT), - m_pElementMT(pElementMT) - { - WRAPPER_NO_CONTRACT; - } + DispParamArrayMarshaler(VARTYPE ElementVT, MethodTable *pElementMT); virtual ~DispParamArrayMarshaler() { @@ -147,6 +142,8 @@ class DispParamArrayMarshaler : public DispParamMarshaler private: VARTYPE m_ElementVT; MethodTable* m_pElementMT; + PCODE m_pConvertContentsToManagedCode; + PCODE m_pConvertContentsToUnmanagedCode; }; diff --git a/src/coreclr/vm/dllimport.cpp b/src/coreclr/vm/dllimport.cpp index ccca4a0178983c..ed05479d9a9401 100644 --- a/src/coreclr/vm/dllimport.cpp +++ b/src/coreclr/vm/dllimport.cpp @@ -4112,10 +4112,10 @@ bool StructMarshalStubs::TryGenerateStructMarshallingMethod(MethodDesc* pMD, Dyn _ASSERTE(pStructMT->IsValueType()); - if (pStructMT->IsBlittable()) + if (pStructMT->IsBlittable() || pStructMT->IsEnum()) { - // No need to generate stubs for blittable types since they can be marshaled by value without any transformation. - // The default IL implementation is correct. + // No need to generate stubs for blittable types or enums since they can be marshaled + // by value without any transformation. The default IL implementation is correct. return false; } diff --git a/src/coreclr/vm/fieldmarshaler.cpp b/src/coreclr/vm/fieldmarshaler.cpp index 81b52188444982..4e8e7be87bc503 100644 --- a/src/coreclr/vm/fieldmarshaler.cpp +++ b/src/coreclr/vm/fieldmarshaler.cpp @@ -19,12 +19,222 @@ #include "comdelegate.h" #include "eeconfig.h" #include "comdatetime.h" -#include "olevariant.h" #include #include #include #include "sigformat.h" #include "marshalnative.h" +#ifdef FEATURE_COMINTEROP +#include "interoputil.h" +#endif // FEATURE_COMINTEROP + +static VARTYPE GetVarTypeForCorElementType(CorElementType type) +{ + CONTRACTL + { + THROWS; + GC_NOTRIGGER; + MODE_ANY; + } + CONTRACTL_END; + + static const BYTE map[] = + { + VT_EMPTY, // ELEMENT_TYPE_END + VT_VOID, // ELEMENT_TYPE_VOID + VT_BOOL, // ELEMENT_TYPE_BOOLEAN + VT_UI2, // ELEMENT_TYPE_CHAR + VT_I1, // ELEMENT_TYPE_I1 + VT_UI1, // ELEMENT_TYPE_U1 + VT_I2, // ELEMENT_TYPE_I2 + VT_UI2, // ELEMENT_TYPE_U2 + VT_I4, // ELEMENT_TYPE_I4 + VT_UI4, // ELEMENT_TYPE_U4 + VT_I8, // ELEMENT_TYPE_I8 + VT_UI8, // ELEMENT_TYPE_U8 + VT_R4, // ELEMENT_TYPE_R4 + VT_R8, // ELEMENT_TYPE_R8 + }; + + _ASSERTE(type < (CorElementType) (sizeof(map) / sizeof(map[0]))); + + VARTYPE vt = VARTYPE(map[type]); + + return vt; +} + +VARTYPE GetVarTypeForTypeHandle(TypeHandle type) +{ + CONTRACTL + { + THROWS; + GC_TRIGGERS; + MODE_ANY; + } + CONTRACTL_END; + + // Handle primitive types. + CorElementType elemType = type.GetSignatureCorElementType(); + if (elemType <= ELEMENT_TYPE_R8) + return GetVarTypeForCorElementType(elemType); + + // Types incompatible with interop. + if (type.IsTypeDesc()) + COMPlusThrow(kArgumentException, IDS_EE_COM_UNSUPPORTED_SIG); + + // Handle objects. + MethodTable * pMT = type.AsMethodTable(); + + if (pMT == g_pStringClass) + return VT_BSTR; + if (pMT == g_pObjectClass) + return VT_VARIANT; + + // We need to make sure the CVClasses table is populated. + if(CoreLibBinder::IsClass(pMT, CLASS__DATE_TIME)) + return VT_DATE; + if(CoreLibBinder::IsClass(pMT, CLASS__DECIMAL)) + return VT_DECIMAL; + +#ifdef HOST_64BIT + if (CoreLibBinder::IsClass(pMT, CLASS__INTPTR)) + return VT_I8; + if (CoreLibBinder::IsClass(pMT, CLASS__UINTPTR)) + return VT_UI8; +#else + if (CoreLibBinder::IsClass(pMT, CLASS__INTPTR)) + return VT_INT; + if (CoreLibBinder::IsClass(pMT, CLASS__UINTPTR)) + return VT_UINT; +#endif + +#ifdef FEATURE_COMINTEROP + // The wrapper types are only available when built-in COM is supported. + if (g_pConfig->IsBuiltInCOMSupported()) + { + if (CoreLibBinder::IsClass(pMT, CLASS__DISPATCH_WRAPPER)) + return VT_DISPATCH; + if (CoreLibBinder::IsClass(pMT, CLASS__UNKNOWN_WRAPPER)) + return VT_UNKNOWN; + if (CoreLibBinder::IsClass(pMT, CLASS__ERROR_WRAPPER)) + return VT_ERROR; + if (CoreLibBinder::IsClass(pMT, CLASS__CURRENCY_WRAPPER)) + return VT_CY; + if (CoreLibBinder::IsClass(pMT, CLASS__BSTR_WRAPPER)) + return VT_BSTR; + + // VariantWrappers cannot be stored in VARIANT's. + if (CoreLibBinder::IsClass(pMT, CLASS__VARIANT_WRAPPER)) + COMPlusThrow(kArgumentException, IDS_EE_COM_UNSUPPORTED_SIG); + } +#endif // FEATURE_COMINTEROP + + if (pMT->IsEnum()) + return GetVarTypeForCorElementType(type.GetInternalCorElementType()); + + if (pMT->IsValueType()) + return VT_RECORD; + + if (pMT->IsArray()) + return VT_ARRAY; + +#ifdef FEATURE_COMINTEROP + // There is no VT corresponding to SafeHandles as they cannot be stored in + // VARIANTs or Arrays. The same applies to CriticalHandle. + if (type.CanCastTo(TypeHandle(CoreLibBinder::GetClass(CLASS__SAFE_HANDLE)))) + COMPlusThrow(kArgumentException, IDS_EE_COM_UNSUPPORTED_SIG); + if (type.CanCastTo(TypeHandle(CoreLibBinder::GetClass(CLASS__CRITICAL_HANDLE)))) + COMPlusThrow(kArgumentException, IDS_EE_COM_UNSUPPORTED_SIG); + + if (pMT->IsInterface()) + { + CorIfaceAttr ifaceType = pMT->GetComInterfaceType(); + return static_cast(IsDispatchBasedItf(ifaceType) ? VT_DISPATCH : VT_UNKNOWN); + } + + TypeHandle hndDefItfClass; + DefaultInterfaceType DefItfType = GetDefaultInterfaceForClassWrapper(type, &hndDefItfClass); + switch (DefItfType) + { + case DefaultInterfaceType_Explicit: + { + CorIfaceAttr ifaceType = hndDefItfClass.GetMethodTable()->GetComInterfaceType(); + return static_cast(IsDispatchBasedItf(ifaceType) ? VT_DISPATCH : VT_UNKNOWN); + } + + case DefaultInterfaceType_AutoDual: + { + return VT_DISPATCH; + } + + case DefaultInterfaceType_IUnknown: + case DefaultInterfaceType_BaseComClass: + { + return VT_UNKNOWN; + } + + case DefaultInterfaceType_AutoDispatch: + { + return VT_DISPATCH; + } + + default: + { + _ASSERTE(!"Invalid default interface type!"); + } + } +#endif // FEATURE_COMINTEROP + + return VT_UNKNOWN; +} + +MethodTable* GetNativeMethodTableForVarType(VARTYPE vt, MethodTable* pManagedMT) +{ + CONTRACTL + { + THROWS; + GC_TRIGGERS; + MODE_ANY; + } + CONTRACTL_END; + + if (vt & VT_ARRAY) + { + return CoreLibBinder::GetClass(CLASS__INTPTR); + } + + switch (vt) + { + case VT_DATE: + return CoreLibBinder::GetClass(CLASS__DOUBLE); + case VT_CY: + return CoreLibBinder::GetClass(CLASS__CURRENCY); + case VT_BOOL: + return CoreLibBinder::GetClass(CLASS__INT16); + case VT_DISPATCH: + case VT_UNKNOWN: + case VT_LPSTR: + case VT_LPWSTR: + case VT_BSTR: + case VT_USERDEFINED: + case VT_SAFEARRAY: + case VT_CARRAY: + return CoreLibBinder::GetClass(CLASS__INTPTR); + case VT_VARIANT: + return CoreLibBinder::GetClass(CLASS__COMVARIANT); + case VT_UI2: + // When CharSet = CharSet.Unicode, System.Char arrays are marshaled as VT_UI2. + // However, since System.Char itself is CharSet.Ansi, the native size of + // System.Char is 1 byte instead of 2. So here we explicitly return System.UInt16's + // MethodTable to ensure the correct size. + return CoreLibBinder::GetClass(CLASS__UINT16); + case VT_DECIMAL: + return CoreLibBinder::GetClass(CLASS__DECIMAL); + default: + _ASSERTE(pManagedMT != NULL); + return pManagedMT; + } +} VOID ParseNativeType(Module* pModule, SigPointer sig, @@ -180,7 +390,22 @@ VOID ParseNativeType(Module* pModule, pMT = CoreLibBinder::GetElementType(pMT->GetInternalCorElementType()); } - *pNFD = NativeFieldDescriptor(pFD, OleVariant::GetNativeMethodTableForVarType(mops.elementType, pMT), mops.additive); + MethodTable *pNativeMT; + switch (mops.elementNativeType) + { + case NATIVE_TYPE_BOOLEAN: + pNativeMT = CoreLibBinder::GetClass(CLASS__INT32); + break; + case NATIVE_TYPE_I1: + case NATIVE_TYPE_U1: + pNativeMT = CoreLibBinder::GetClass(CLASS__BYTE); + break; + default: + pNativeMT = GetNativeMethodTableForVarType(mops.elementType, pMT); + break; + } + + *pNFD = NativeFieldDescriptor(pFD, pNativeMT, mops.additive); break; } case MarshalInfo::MARSHAL_TYPE_FIXED_CSTR: diff --git a/src/coreclr/vm/fieldmarshaler.h b/src/coreclr/vm/fieldmarshaler.h index 568322cab21106..93d548c61e13e9 100644 --- a/src/coreclr/vm/fieldmarshaler.h +++ b/src/coreclr/vm/fieldmarshaler.h @@ -11,7 +11,9 @@ #include "util.hpp" #include "mlinfo.h" #include "eeconfig.h" -#include "olevariant.h" + +VARTYPE GetVarTypeForTypeHandle(TypeHandle typeHnd); +MethodTable* GetNativeMethodTableForVarType(VARTYPE vt, MethodTable* pManagedMT); // Forward references class EEClassLayoutInfo; diff --git a/src/coreclr/vm/ilmarshalers.cpp b/src/coreclr/vm/ilmarshalers.cpp index 3676e78abd041d..110245fecc59a9 100644 --- a/src/coreclr/vm/ilmarshalers.cpp +++ b/src/coreclr/vm/ilmarshalers.cpp @@ -11,7 +11,9 @@ #include "dllimport.h" #include "mlinfo.h" #include "ilmarshalers.h" +#ifdef FEATURE_COMINTEROP #include "olevariant.h" +#endif // FEATURE_COMINTEROP #include "comdatetime.h" #include "fieldmarshaler.h" @@ -1113,11 +1115,9 @@ void ILValueClassMarshaler::EmitClearNative(ILCodeStream * pslILEmit) EmitLoadManagedHomeAddr(pslILEmit); EmitLoadNativeHomeAddr(pslILEmit); - pslILEmit->EmitLDC(m_pargs->m_pMT->GetNativeLayoutInfo()->GetSize()); EmitLoadCleanupWorkList(pslILEmit); - - pslILEmit->EmitCALL(pslILEmit->GetToken(GetStructMarshalingMethod(METHOD__STRUCTURE_MARSHALER__FREE, m_pargs->m_pMT)), 4, 0); + pslILEmit->EmitCALL(pslILEmit->GetToken(GetStructMarshalingMethod(METHOD__STRUCTURE_MARSHALER__FREE, m_pargs->m_pMT)), 3, 0); } @@ -1127,10 +1127,9 @@ void ILValueClassMarshaler::EmitConvertContentsCLRToNative(ILCodeStream* pslILEm EmitLoadManagedHomeAddr(pslILEmit); EmitLoadNativeHomeAddr(pslILEmit); - pslILEmit->EmitLDC(m_pargs->m_pMT->GetNativeLayoutInfo()->GetSize()); EmitLoadCleanupWorkList(pslILEmit); - pslILEmit->EmitCALL(pslILEmit->GetToken(GetStructMarshalingMethod(METHOD__STRUCTURE_MARSHALER__CONVERT_TO_UNMANAGED, m_pargs->m_pMT)), 4, 0); + pslILEmit->EmitCALL(pslILEmit->GetToken(GetStructMarshalingMethod(METHOD__STRUCTURE_MARSHALER__CONVERT_TO_UNMANAGED, m_pargs->m_pMT)), 3, 0); } void ILValueClassMarshaler::EmitConvertContentsNativeToCLR(ILCodeStream* pslILEmit) @@ -2327,10 +2326,9 @@ void ILLayoutClassPtrMarshaler::EmitConvertContentsCLRToNative(ILCodeStream* psl EmitLoadManagedValue(pslILEmit); EmitLoadNativeValue(pslILEmit); - pslILEmit->EmitLDC(m_pargs->m_pMT->GetNativeLayoutInfo()->GetSize()); EmitLoadCleanupWorkList(pslILEmit); - pslILEmit->EmitCALL(pslILEmit->GetToken(GetStructMarshalingMethod(METHOD__LAYOUTCLASS_MARSHALER__CONVERT_TO_UNMANAGED, m_pargs->m_pMT)), 4, 0); + pslILEmit->EmitCALL(pslILEmit->GetToken(GetStructMarshalingMethod(METHOD__LAYOUTCLASS_MARSHALER__CONVERT_TO_UNMANAGED, m_pargs->m_pMT)), 3, 0); if (emittedTypeCheck) { @@ -2386,10 +2384,9 @@ void ILLayoutClassPtrMarshaler::EmitClearNativeContents(ILCodeStream * pslILEmit EmitLoadManagedValue(pslILEmit); EmitLoadNativeValue(pslILEmit); - pslILEmit->EmitLDC(m_pargs->m_pMT->GetNativeLayoutInfo()->GetSize()); EmitLoadCleanupWorkList(pslILEmit); - pslILEmit->EmitCALL(pslILEmit->GetToken(GetStructMarshalingMethod(METHOD__LAYOUTCLASS_MARSHALER__FREE, m_pargs->m_pMT)), 4, 0); + pslILEmit->EmitCALL(pslILEmit->GetToken(GetStructMarshalingMethod(METHOD__LAYOUTCLASS_MARSHALER__FREE, m_pargs->m_pMT)), 3, 0); if (emittedTypeCheck) { @@ -2533,10 +2530,9 @@ void ILLayoutClassMarshaler::EmitConvertContentsCLRToNative(ILCodeStream* pslILE EmitLoadManagedValue(pslILEmit); EmitLoadNativeHomeAddr(pslILEmit); - pslILEmit->EmitLDC(m_pargs->m_pMT->GetNativeLayoutInfo()->GetSize()); EmitLoadCleanupWorkList(pslILEmit); - pslILEmit->EmitCALL(pslILEmit->GetToken(GetStructMarshalingMethod(METHOD__LAYOUTCLASS_MARSHALER__CONVERT_TO_UNMANAGED, m_pargs->m_pMT)), 4, 0); + pslILEmit->EmitCALL(pslILEmit->GetToken(GetStructMarshalingMethod(METHOD__LAYOUTCLASS_MARSHALER__CONVERT_TO_UNMANAGED, m_pargs->m_pMT)), 3, 0); pslILEmit->EmitLabel(pNullRefLabel); } @@ -2566,10 +2562,9 @@ void ILLayoutClassMarshaler::EmitClearNativeContents(ILCodeStream* pslILEmit) EmitLoadManagedValue(pslILEmit); EmitLoadNativeHomeAddr(pslILEmit); - pslILEmit->EmitLDC(m_pargs->m_pMT->GetNativeLayoutInfo()->GetSize()); EmitLoadCleanupWorkList(pslILEmit); - pslILEmit->EmitCALL(pslILEmit->GetToken(GetStructMarshalingMethod(METHOD__LAYOUTCLASS_MARSHALER__FREE, m_pargs->m_pMT)), 4, 0); + pslILEmit->EmitCALL(pslILEmit->GetToken(GetStructMarshalingMethod(METHOD__LAYOUTCLASS_MARSHALER__FREE, m_pargs->m_pMT)), 3, 0); } @@ -3759,23 +3754,19 @@ void ILAsAnyMarshalerBase::EmitCreateMngdMarshaler(ILCodeStream* pslILEmit) LocalDesc marshalerType(CoreLibBinder::GetClass(CLASS__ASANY_MARSHALER)); m_dwMngdMarshalerLocalNum = pslILEmit->NewLocal(marshalerType); - DWORD dwTmpLocalNum = pslILEmit->NewLocal(ELEMENT_TYPE_I); - pslILEmit->EmitLDC(sizeof(MngdNativeArrayMarshaler)); - pslILEmit->EmitLOCALLOC(); - pslILEmit->EmitSTLOC(dwTmpLocalNum); - - // marshaler = new AsAnyMarshaler(local_buffer) pslILEmit->EmitLDLOCA(m_dwMngdMarshalerLocalNum); pslILEmit->EmitINITOBJ(pslILEmit->GetToken(marshalerType.InternalToken)); - - pslILEmit->EmitLDLOCA(m_dwMngdMarshalerLocalNum); - pslILEmit->EmitLDLOC(dwTmpLocalNum); - pslILEmit->EmitCALL(METHOD__ASANY_MARSHALER__CTOR, 2, 0); } void ILAsAnyMarshalerBase::EmitConvertContentsCLRToNative(ILCodeStream* pslILEmit) { + // marshaler = new AsAnyMarshaler(managedValue, flags); + EmitLoadMngdMarshalerAddr(pslILEmit); + EmitLoadManagedValue(pslILEmit); + pslILEmit->EmitLDC(GetAsAnyFlags()); + pslILEmit->EmitCALL(METHOD__ASANY_MARSHALER__CTOR, 3, 0); + // nativeValue = marshaler.ConvertToNative(managedValue, flags); EmitLoadMngdMarshalerAddr(pslILEmit); EmitLoadManagedValue(pslILEmit); @@ -3852,47 +3843,34 @@ void ILMngdMarshaler::EmitCallMngdMarshalerMethod(ILCodeStream* pslILEmit, Metho } } -void ILNativeArrayMarshaler::EmitCreateMngdMarshaler(ILCodeStream* pslILEmit) +bool ILNativeArrayMarshaler::CanMarshalViaPinning() { - STANDARD_VM_CONTRACT; + // We can't pin an array if we have a non-default element native type (e.g. ANSICHAR, WINBOOL, CBOOL), + // if we have a marshaler for the var type, or if we can't get a method-table representing the array. - m_dwMngdMarshalerLocalNum = pslILEmit->NewLocal(ELEMENT_TYPE_I); - - pslILEmit->EmitLDC(sizeof(MngdNativeArrayMarshaler)); - pslILEmit->EmitLOCALLOC(); - pslILEmit->EmitSTLOC(m_dwMngdMarshalerLocalNum); + if (!IsCLRToNative(m_dwMarshalFlags) || IsByref(m_dwMarshalFlags)) + { + return false; + } CREATE_MARSHALER_CARRAY_OPERANDS mops; m_pargs->m_pMarshalInfo->GetMops(&mops); - - pslILEmit->EmitLDLOC(m_dwMngdMarshalerLocalNum); - - pslILEmit->EmitLDTOKEN(pslILEmit->GetToken(mops.methodTable)); - pslILEmit->EmitCALL(METHOD__RT_TYPE_HANDLE__TO_INTPTR, 1, 1); - - DWORD dwFlags = mops.elementType; - dwFlags |= (((DWORD)mops.bestfitmapping) << 16); - dwFlags |= (((DWORD)mops.throwonunmappablechar) << 24); - - pslILEmit->EmitLDC(dwFlags); - - if (!IsCLRToNative(m_dwMarshalFlags) && IsOut(m_dwMarshalFlags) && IsIn(m_dwMarshalFlags)) + if (mops.elementNativeType != NATIVE_TYPE_DEFAULT) { - pslILEmit->EmitLDC(1); // true + // This means that we have some sort of custom marshaling logic. + return false; } - else + + if (NULL == m_pargs->na.m_pArrayMT) { - pslILEmit->EmitLDC(0); // false + return false; } - pslILEmit->EmitCALL(METHOD__MNGD_NATIVE_ARRAY_MARSHALER__CREATE_MARSHALER, 4, 0); -} + TypeHandle elementTypeHandle = m_pargs->na.m_pArrayMT->GetArrayElementTypeHandle(); -bool ILNativeArrayMarshaler::CanMarshalViaPinning() -{ - // We can't pin an array if we have a marshaler for the var type - // or if we can't get a method-table representing the array (how we determine the offset to pin). - return IsCLRToNative(m_dwMarshalFlags) && !IsByref(m_dwMarshalFlags) && (NULL != m_pargs->na.m_pArrayMT) && (NULL == OleVariant::GetMarshalerForVarType(m_pargs->na.m_vt, TRUE)); + return elementTypeHandle.IsBlittable() + && (elementTypeHandle.GetMethodTable()->IsValueType() + || elementTypeHandle.GetMethodTable()->IsTruePrimitive()); } void ILNativeArrayMarshaler::EmitMarshalViaPinning(ILCodeStream* pslILEmit) @@ -4146,40 +4124,271 @@ void ILNativeArrayMarshaler::EmitLoadElementCount(ILCodeStream* pslILEmit) } } -void ILNativeArrayMarshaler::EmitConvertSpaceNativeToCLR(ILCodeStream* pslILEmit) + +namespace +{ + // Resolve the managed marshaler MethodTable and the element type it marshals. + // Both are returned together to guarantee they are consistent. + void GetMarshalerAndElementTypes(MarshalInfo* pMarshalInfo, MethodTable** ppMarshalerMT, TypeHandle* pElementType) { STANDARD_VM_CONTRACT; - EmitLoadMngdMarshaler(pslILEmit); - EmitLoadManagedHomeAddr(pslILEmit); - EmitLoadNativeHomeAddr(pslILEmit); + CREATE_MARSHALER_CARRAY_OPERANDS mops; + pMarshalInfo->GetMops(&mops); + VARTYPE vt = mops.elementType; + bool bestFit = mops.bestfitmapping != 0; + bool throwOnUnmappable = mops.throwonunmappablechar != 0; + + // Start from the managed element type - this is the authoritative source. + MethodTable* pElementMT = mops.methodTable; + + TypeHandle thElement(pElementMT); + + MethodTable* pEnabledMT = CoreLibBinder::GetClass(CLASS__MARSHALER_OPTION_ENABLED); + MethodTable* pDisabledMT = CoreLibBinder::GetClass(CLASS__MARSHALER_OPTION_DISABLED); + MethodTable* pBestFitMT = bestFit ? pEnabledMT : pDisabledMT; + MethodTable* pThrowOnUnmappableMT = throwOnUnmappable ? pEnabledMT : pDisabledMT; + + // Handle explicit CorNativeType overrides first. These override the default + // marshaling for the element type (e.g. bool as NATIVE_TYPE_BOOLEAN vs VT_BOOL). + CorNativeType nt = mops.elementNativeType; + if (nt != NATIVE_TYPE_DEFAULT) + { + switch (nt) + { + case NATIVE_TYPE_BOOLEAN: + { + _ASSERTE(thElement == TypeHandle(CoreLibBinder::GetClass(CLASS__BOOLEAN))); + TypeHandle thInt32 = CoreLibBinder::GetClass(CLASS__INT32); + *pElementType = thElement; + *ppMarshalerMT = TypeHandle(CoreLibBinder::GetClass(CLASS__BOOL_MARSHALER)).Instantiate(Instantiation(&thInt32, 1)).AsMethodTable(); + return; + } + + case NATIVE_TYPE_I1: + { + _ASSERTE(thElement == TypeHandle(CoreLibBinder::GetClass(CLASS__BOOLEAN))); + TypeHandle thByte = CoreLibBinder::GetClass(CLASS__BYTE); + *pElementType = thElement; + *ppMarshalerMT = TypeHandle(CoreLibBinder::GetClass(CLASS__BOOL_MARSHALER)).Instantiate(Instantiation(&thByte, 1)).AsMethodTable(); + return; + } + + case NATIVE_TYPE_U1: + { + _ASSERTE(thElement == TypeHandle(CoreLibBinder::GetClass(CLASS__CHAR))); + TypeHandle thArgs[2] = { TypeHandle(pBestFitMT), TypeHandle(pThrowOnUnmappableMT) }; + *pElementType = thElement; + *ppMarshalerMT = TypeHandle(CoreLibBinder::GetClass(CLASS__ANSICHAR_ARRAY_ELEMENT_MARSHALER)).Instantiate(Instantiation(thArgs, 2)).AsMethodTable(); + return; + } + + default: + _ASSERTE(!"Unsupported CorNativeType for GetMarshalerAndElementTypes"); + COMPlusThrow(kArgumentException, IDS_EE_COM_UNSUPPORTED_SIG); + return; + } + } + + // Use the VT only to select the marshaler pattern; element type comes from the + // managed side so enums, char, etc. are handled naturally. + switch (vt) + { + case VT_I1: + case VT_UI1: + case VT_I2: + case VT_I4: + case VT_INT: + case VT_UI4: + case VT_UINT: + case VT_ERROR: + case VT_I8: + case VT_UI8: + case VT_R4: + case VT_R8: + case VT_DECIMAL: + { + *pElementType = thElement; + *ppMarshalerMT = TypeHandle(CoreLibBinder::GetClass(CLASS__BLITTABLE_ARRAY_MARSHALER)).Instantiate(Instantiation(&thElement, 1)).AsMethodTable(); + return; + } + + case VT_UI2: + { + *pElementType = thElement; + *ppMarshalerMT = TypeHandle(CoreLibBinder::GetClass(CLASS__BLITTABLE_ARRAY_MARSHALER)).Instantiate(Instantiation(&thElement, 1)).AsMethodTable(); + return; + } + + case VT_BOOL: + { + _ASSERTE(thElement == TypeHandle(CoreLibBinder::GetClass(CLASS__BOOLEAN))); + *pElementType = thElement; + *ppMarshalerMT = CoreLibBinder::GetClass(CLASS__VARIANT_BOOL_MARSHALER); + return; + } + + case VT_DATE: + { + *pElementType = thElement; + *ppMarshalerMT = CoreLibBinder::GetClass(CLASS__DATEMARSHALER); + return; + } + + case VT_LPWSTR: + { + *pElementType = thElement; + *ppMarshalerMT = CoreLibBinder::GetClass(CLASS__LPWSTR_MARSHALER); + return; + } + + case VT_LPSTR: + { + TypeHandle thArgs[2] = { TypeHandle(pBestFitMT), TypeHandle(pThrowOnUnmappableMT) }; + *pElementType = thElement; + *ppMarshalerMT = TypeHandle(CoreLibBinder::GetClass(CLASS__LPSTR_ARRAY_ELEMENT_MARSHALER)).Instantiate(Instantiation(thArgs, 2)).AsMethodTable(); + return; + } + + case VT_BSTR: + { + *pElementType = thElement; + *ppMarshalerMT = CoreLibBinder::GetClass(CLASS__BSTR_ARRAY_ELEMENT_MARSHALER); + return; + } + +#ifdef FEATURE_COMINTEROP + case VT_CY: + { + *pElementType = thElement; + *ppMarshalerMT = CoreLibBinder::GetClass(CLASS__CURRENCY_ARRAY_ELEMENT_MARSHALER); + return; + } + + case VT_UNKNOWN: + case VT_DISPATCH: + { + TypeHandle arrayElementTypeHandle = pMarshalInfo->GetArrayElementTypeHandle(); + if (arrayElementTypeHandle == TypeHandle(g_pObjectClass)) + { + TypeHandle thDispatch(vt == VT_DISPATCH ? pEnabledMT : pDisabledMT); + *pElementType = TypeHandle(g_pObjectClass); + *ppMarshalerMT = TypeHandle(CoreLibBinder::GetClass(CLASS__INTERFACE_ARRAY_ELEMENT_MARSHALER)).Instantiate(Instantiation(&thDispatch, 1)).AsMethodTable(); + } + else if (!arrayElementTypeHandle.IsInterface()) + { + // For class types, resolve the default COM interface. + BOOL bDispatch = FALSE; + MethodTable* pDefaultItfMT = GetDefaultInterfaceMTForClass(arrayElementTypeHandle.AsMethodTable(), &bDispatch); + if (pDefaultItfMT != NULL) + { + TypeHandle thItf(pDefaultItfMT); + *pElementType = thItf; + *ppMarshalerMT = TypeHandle(CoreLibBinder::GetClass(CLASS__TYPED_INTERFACE_ARRAY_ELEMENT_MARSHALER)).Instantiate(Instantiation(&thItf, 1)).AsMethodTable(); + } + else + { + TypeHandle thDispatch(bDispatch ? pEnabledMT : pDisabledMT); + *pElementType = TypeHandle(g_pObjectClass); + *ppMarshalerMT = TypeHandle(CoreLibBinder::GetClass(CLASS__INTERFACE_ARRAY_ELEMENT_MARSHALER)).Instantiate(Instantiation(&thDispatch, 1)).AsMethodTable(); + } + } + else + { + *pElementType = arrayElementTypeHandle; + *ppMarshalerMT = TypeHandle(CoreLibBinder::GetClass(CLASS__TYPED_INTERFACE_ARRAY_ELEMENT_MARSHALER)).Instantiate(Instantiation(&arrayElementTypeHandle, 1)).AsMethodTable(); + } + return; + } + + case VT_VARIANT: + { + *pElementType = TypeHandle(g_pObjectClass); + TypeHandle thDisabled(pDisabledMT); + *ppMarshalerMT = TypeHandle(CoreLibBinder::GetClass(CLASS__VARIANT_ARRAY_ELEMENT_MARSHALER)).Instantiate(Instantiation(&thDisabled, 1)).AsMethodTable(); + return; + } +#endif // FEATURE_COMINTEROP + + case VT_RECORD: + { + *pElementType = thElement; + + if (thElement.IsBlittable()) + { + *ppMarshalerMT = TypeHandle(CoreLibBinder::GetClass(CLASS__BLITTABLE_ARRAY_MARSHALER)).Instantiate(Instantiation(&thElement, 1)).AsMethodTable(); + } + else + { + *ppMarshalerMT = TypeHandle(CoreLibBinder::GetClass(CLASS__STRUCTURE_MARSHALER)).Instantiate(Instantiation(&thElement, 1)).AsMethodTable(); + } + return; + } + + default: + _ASSERTE(!"Unsupported VT for GetMarshalerAndElementTypes"); + COMPlusThrow(kArgumentException, IDS_EE_COM_UNSUPPORTED_SIG); + return; + } +} + + + // Instantiate one of the generic StubHelpers array methods with the element type and marshaler type. + MethodDesc* GetInstantiatedArrayMethod(MarshalInfo* pMarshalInfo, BinderMethodID methodId) +{ + STANDARD_VM_CONTRACT; + + MethodDesc* pGenericMD = CoreLibBinder::GetMethod(methodId); + + // Get both the element type and marshaler type from a single source + // to guarantee they are consistent. + TypeHandle thElementType; + MethodTable* pMarshalerMT; + GetMarshalerAndElementTypes(pMarshalInfo, &pMarshalerMT, &thElementType); + + TypeHandle thMarshalerType(pMarshalerMT); + TypeHandle thArgs[2] = { thElementType, thMarshalerType }; + + MethodDesc* pInstMD = MethodDesc::FindOrCreateAssociatedMethodDesc( + pGenericMD, + pGenericMD->GetMethodTable(), + FALSE, + Instantiation(thArgs, 2), + FALSE); + + return pInstMD; +} +} // anonymous namespace + +void ILNativeArrayMarshaler::EmitConvertSpaceNativeToCLR(ILCodeStream* pslILEmit) +{ + STANDARD_VM_CONTRACT; if (IsByref(m_dwMarshalFlags)) { - // - // Reset the element count just in case there is an exception thrown in the code emitted by - // EmitLoadElementCount. The best thing we can do here is to avoid a crash. - // + // Reset the element count in case EmitLoadElementCount throws. _ASSERTE(m_dwSavedSizeArg != LOCAL_NUM_UNUSED); pslILEmit->EmitLDC(0); pslILEmit->EmitSTLOC(m_dwSavedSizeArg); } + MethodDesc* pMD = GetInstantiatedArrayMethod(m_pargs->m_pMarshalInfo, METHOD__STUBHELPERS__CONVERT_ARRAY_SPACE_TO_MANAGED); + + EmitLoadNativeValue(pslILEmit); + // Dynamically calculate element count using SizeParamIndex argument EmitLoadElementCount(pslILEmit); if (IsByref(m_dwMarshalFlags)) { - // - // Save the native array size before converting it to managed and load it again - // + // Save the native array size and reload it _ASSERTE(m_dwSavedSizeArg != LOCAL_NUM_UNUSED); pslILEmit->EmitSTLOC(m_dwSavedSizeArg); pslILEmit->EmitLDLOC(m_dwSavedSizeArg); } - // MngdNativeArrayMarshaler::ConvertSpaceToManaged - pslILEmit->EmitCALL(pslILEmit->GetToken(GetConvertSpaceToManagedMethod()), 4, 0); + pslILEmit->EmitCALL(pslILEmit->GetToken(pMD), 2, 1); + EmitStoreManagedValue(pslILEmit); } void ILNativeArrayMarshaler::EmitConvertSpaceCLRToNative(ILCodeStream* pslILEmit) @@ -4190,31 +4399,34 @@ void ILNativeArrayMarshaler::EmitConvertSpaceCLRToNative(ILCodeStream* pslILEmit { _ASSERTE(m_dwSavedSizeArg != LOCAL_NUM_UNUSED); - // // Save the array size before converting it to native - // EmitLoadManagedValue(pslILEmit); ILCodeLabel *pManagedHomeIsNull = pslILEmit->NewCodeLabel(); pslILEmit->EmitBRFALSE(pManagedHomeIsNull); EmitLoadManagedValue(pslILEmit); pslILEmit->EmitLDLEN(); + pslILEmit->EmitCONV_OVF_I4(); pslILEmit->EmitSTLOC(m_dwSavedSizeArg); + pslILEmit->EmitLabel(pManagedHomeIsNull); } + MethodDesc* pMD = GetInstantiatedArrayMethod(m_pargs->m_pMarshalInfo, METHOD__STUBHELPERS__CONVERT_ARRAY_SPACE_TO_NATIVE); - ILMngdMarshaler::EmitConvertSpaceCLRToNative(pslILEmit); + EmitLoadManagedValue(pslILEmit); + pslILEmit->EmitCALL(pslILEmit->GetToken(pMD), 1, 1); + EmitStoreNativeValue(pslILEmit); } void ILNativeArrayMarshaler::EmitClearNative(ILCodeStream* pslILEmit) { STANDARD_VM_CONTRACT; - EmitLoadMngdMarshaler(pslILEmit); - EmitLoadNativeHomeAddr(pslILEmit); - EmitLoadNativeSize(pslILEmit); + MethodDesc* pMD = GetInstantiatedArrayMethod(m_pargs->m_pMarshalInfo, METHOD__STUBHELPERS__CLEAR_ARRAY_NATIVE); - pslILEmit->EmitCALL(pslILEmit->GetToken(GetClearNativeMethod()), 3, 0); + EmitLoadNativeValue(pslILEmit); + EmitLoadNativeSize(pslILEmit); + pslILEmit->EmitCALL(pslILEmit->GetToken(pMD), 2, 0); } void ILNativeArrayMarshaler::EmitLoadNativeSize(ILCodeStream* pslILEmit) @@ -4240,15 +4452,55 @@ void ILNativeArrayMarshaler::EmitLoadNativeSize(ILCodeStream* pslILEmit) } } +void ILNativeArrayMarshaler::EmitConvertContentsCLRToNative(ILCodeStream* pslILEmit) +{ + STANDARD_VM_CONTRACT; + + ILCodeLabel* pSkipLabel = pslILEmit->NewCodeLabel(); + EmitLoadManagedValue(pslILEmit); + pslILEmit->EmitBRFALSE(pSkipLabel); + + MethodDesc* pMD = GetInstantiatedArrayMethod(m_pargs->m_pMarshalInfo, METHOD__STUBHELPERS__CONVERT_ARRAY_CONTENTS_TO_UNMANAGED); + + EmitLoadManagedValue(pslILEmit); + EmitLoadNativeValue(pslILEmit); + EmitLoadManagedValue(pslILEmit); + pslILEmit->EmitLDLEN(); + pslILEmit->EmitCONV_I4(); + pslILEmit->EmitCALL(pslILEmit->GetToken(pMD), 3, 0); + + pslILEmit->EmitLabel(pSkipLabel); +} + +void ILNativeArrayMarshaler::EmitConvertContentsNativeToCLR(ILCodeStream* pslILEmit) +{ + STANDARD_VM_CONTRACT; + + ILCodeLabel* pSkipLabel = pslILEmit->NewCodeLabel(); + EmitLoadManagedValue(pslILEmit); + pslILEmit->EmitBRFALSE(pSkipLabel); + + MethodDesc* pMD = GetInstantiatedArrayMethod(m_pargs->m_pMarshalInfo, METHOD__STUBHELPERS__CONVERT_ARRAY_CONTENTS_TO_MANAGED); + + EmitLoadManagedValue(pslILEmit); + EmitLoadNativeValue(pslILEmit); + EmitLoadManagedValue(pslILEmit); + pslILEmit->EmitLDLEN(); + pslILEmit->EmitCONV_I4(); + pslILEmit->EmitCALL(pslILEmit->GetToken(pMD), 3, 0); + + pslILEmit->EmitLabel(pSkipLabel); +} + void ILNativeArrayMarshaler::EmitClearNativeContents(ILCodeStream* pslILEmit) { STANDARD_VM_CONTRACT; - EmitLoadMngdMarshaler(pslILEmit); - EmitLoadNativeHomeAddr(pslILEmit); - EmitLoadNativeSize(pslILEmit); + MethodDesc* pMD = GetInstantiatedArrayMethod(m_pargs->m_pMarshalInfo, METHOD__STUBHELPERS__FREE_ARRAY_CONTENTS); - pslILEmit->EmitCALL(pslILEmit->GetToken(GetClearNativeContentsMethod()), 3, 0); + EmitLoadNativeValue(pslILEmit); + EmitLoadNativeSize(pslILEmit); + pslILEmit->EmitCALL(pslILEmit->GetToken(pMD), 2, 0); } void ILNativeArrayMarshaler::EmitSetupArgumentForMarshalling(ILCodeStream* pslILEmit) @@ -4269,341 +4521,145 @@ void ILNativeArrayMarshaler::EmitNewSavedSizeArgLocal(ILCodeStream* pslILEmit) pslILEmit->EmitSTLOC(m_dwSavedSizeArg); } -extern "C" void QCALLTYPE MngdNativeArrayMarshaler_ConvertSpaceToNative(MngdNativeArrayMarshaler* pThis, QCall::ObjectHandleOnStack pManagedHome, void** pNativeHome) -{ - QCALL_CONTRACT; - - BEGIN_QCALL; - GCX_COOP(); +// ==================== ILFixedArrayMarshaler ==================== - if (pManagedHome.Get() == NULL) - { - *pNativeHome = NULL; - } - else - { - SIZE_T cElements = ((BASEARRAYREF)pManagedHome.Get())->GetNumComponents(); - SIZE_T cbElement = OleVariant::GetElementSizeForVarType(pThis->m_vt, pThis->m_pElementMT); - - if (cbElement == 0) - COMPlusThrow(kArgumentException, IDS_EE_COM_UNSUPPORTED_SIG); +void ILFixedArrayMarshaler::EmitConvertSpaceCLRToNative(ILCodeStream* pslILEmit) +{ + STANDARD_VM_CONTRACT; - GCX_PREEMP(); + // For fixed arrays, the native space is inline in the struct. + // Validate that the managed array (if non-null) has enough elements. + CREATE_MARSHALER_CARRAY_OPERANDS mops; + m_pargs->m_pMarshalInfo->GetMops(&mops); - SIZE_T cbArray; - if ( (!ClrSafeInt::multiply(cElements, cbElement, cbArray)) || cbArray > MAX_SIZE_FOR_INTEROP) - COMPlusThrow(kArgumentException, IDS_EE_STRUCTARRAYTOOLARGE); + ILCodeLabel* pDoneLabel = pslILEmit->NewCodeLabel(); - *pNativeHome = CoTaskMemAlloc(cbArray); + EmitLoadManagedValue(pslILEmit); + pslILEmit->EmitBRFALSE(pDoneLabel); - if (*pNativeHome == NULL) - ThrowOutOfMemory(); + EmitLoadManagedValue(pslILEmit); + pslILEmit->EmitLDLEN(); + pslILEmit->EmitLDC(mops.additive); + pslILEmit->EmitBGE_UN(pDoneLabel); - // initialize the array - FillMemory(*pNativeHome, cbArray, 0); - } + // Array too small for the fixed-size native layout - throw + pslILEmit->EmitCALL(METHOD__STUBHELPERS__THROW_WRONG_SIZE_ARRAY_IN_NSTRUCT, 0, 0); - END_QCALL; + pslILEmit->EmitLabel(pDoneLabel); } -extern "C" void QCALLTYPE MngdNativeArrayMarshaler_ConvertContentsToNative(MngdNativeArrayMarshaler* pThis, QCall::ObjectHandleOnStack pManagedHome, void** pNativeHome) +void ILFixedArrayMarshaler::EmitConvertSpaceNativeToCLR(ILCodeStream* pslILEmit) { - QCALL_CONTRACT; - - BEGIN_QCALL; - - GCX_COOP(); - - BASEARRAYREF arrayRef = NULL; - GCPROTECT_BEGIN(arrayRef); - arrayRef = (BASEARRAYREF)pManagedHome.Get(); - - if (arrayRef != NULL) - { - const OleVariant::Marshaler* pMarshaler = OleVariant::GetMarshalerForVarType(pThis->m_vt, TRUE); - SIZE_T cElements = arrayRef->GetNumComponents(); - if (pMarshaler == NULL || pMarshaler->ComToOleArray == NULL) - { - if ( (!ClrSafeInt::multiply(cElements, OleVariant::GetElementSizeForVarType(pThis->m_vt, pThis->m_pElementMT), cElements)) || cElements > MAX_SIZE_FOR_INTEROP) - COMPlusThrow(kArgumentException, IDS_EE_STRUCTARRAYTOOLARGE); - - _ASSERTE(!OleVariant::GetTypeHandleForVarType(pThis->m_vt).GetMethodTable()->ContainsGCPointers()); - memcpyNoGCRefs(*pNativeHome, arrayRef->GetDataPtr(), cElements); - } - else - { - pMarshaler->ComToOleArray(&arrayRef, *pNativeHome, pThis->m_pElementMT, pThis->m_BestFitMap, - pThis->m_ThrowOnUnmappableChar, pThis->m_NativeDataValid, cElements); - } - } + STANDARD_VM_CONTRACT; - GCPROTECT_END(); + // Allocate the managed array with the fixed element count. + CREATE_MARSHALER_CARRAY_OPERANDS mops; + m_pargs->m_pMarshalInfo->GetMops(&mops); - END_QCALL; + // new T[cElements] + pslILEmit->EmitLDC(mops.additive); + pslILEmit->EmitNEWARR(pslILEmit->GetToken(mops.methodTable)); + EmitStoreManagedValue(pslILEmit); } -extern "C" void QCALLTYPE MngdNativeArrayMarshaler_ConvertSpaceToManaged(MngdNativeArrayMarshaler* pThis, - QCall::ObjectHandleOnStack managedHome, void** pNativeHome, INT32 cElements) +void ILFixedArrayMarshaler::EmitConvertContentsCLRToNative(ILCodeStream* pslILEmit) { - QCALL_CONTRACT; + STANDARD_VM_CONTRACT; - BEGIN_QCALL; - GCX_COOP(); + CREATE_MARSHALER_CARRAY_OPERANDS mops; + m_pargs->m_pMarshalInfo->GetMops(&mops); - if (*pNativeHome == NULL) + // Compute the total native byte size of the inline array so we can + // zero it when the managed array is null. + MethodTable* pElementMT = mops.methodTable; + if (pElementMT->IsEnum()) { - managedHome.Set(NULL); + pElementMT = CoreLibBinder::GetElementType(pElementMT->GetInternalCorElementType()); } - else - { - // @todo: lookup this class before marshal time - if (pThis->m_Array.IsNull()) - { - // Get proper array class name & type - pThis->m_Array = OleVariant::GetArrayForVarType(pThis->m_vt, TypeHandle(pThis->m_pElementMT)); - if (pThis->m_Array.IsNull()) - COMPlusThrow(kTypeLoadException); - } - // - // Allocate array - // - managedHome.Set(AllocateSzArray(pThis->m_Array, cElements)); - } - - END_QCALL; -} - -extern "C" void QCALLTYPE MngdNativeArrayMarshaler_ConvertContentsToManaged(MngdNativeArrayMarshaler* pThis, QCall::ObjectHandleOnStack pManagedHome, void** pNativeHome) -{ - QCALL_CONTRACT; - - BEGIN_QCALL; - GCX_COOP(); - - BASEARRAYREF arrayRef = NULL; - GCPROTECT_BEGIN(arrayRef); - arrayRef = (BASEARRAYREF)pManagedHome.Get(); - - if (*pNativeHome != NULL) + MethodTable* pNativeElementMT; + switch (mops.elementNativeType) { - const OleVariant::Marshaler *pMarshaler = OleVariant::GetMarshalerForVarType(pThis->m_vt, TRUE); + case NATIVE_TYPE_BOOLEAN: + pNativeElementMT = CoreLibBinder::GetClass(CLASS__INT32); + break; + case NATIVE_TYPE_I1: + case NATIVE_TYPE_U1: + pNativeElementMT = CoreLibBinder::GetClass(CLASS__BYTE); + break; + default: + pNativeElementMT = GetNativeMethodTableForVarType(mops.elementType, pElementMT); + break; + } - if (pMarshaler == NULL || pMarshaler->OleToComArray == NULL) - { - SIZE_T cElements; - if ( (!ClrSafeInt::multiply(arrayRef->GetNumComponents(), OleVariant::GetElementSizeForVarType(pThis->m_vt, pThis->m_pElementMT), cElements)) || cElements > MAX_SIZE_FOR_INTEROP) - COMPlusThrow(kArgumentException, IDS_EE_STRUCTARRAYTOOLARGE); + UINT uNativeSize = pNativeElementMT->GetNativeSize() * mops.additive; - // If we are copying variants, strings, etc, we need to use write barrier - _ASSERTE(!OleVariant::GetTypeHandleForVarType(pThis->m_vt).GetMethodTable()->ContainsGCPointers()); - memcpyNoGCRefs(arrayRef->GetDataPtr(), *pNativeHome, cElements); - } - else - { - pMarshaler->OleToComArray(*pNativeHome, &arrayRef, pThis->m_pElementMT); - } - } + ILCodeLabel* pNullLabel = pslILEmit->NewCodeLabel(); + ILCodeLabel* pDoneLabel = pslILEmit->NewCodeLabel(); + EmitLoadManagedValue(pslILEmit); + pslILEmit->EmitBRFALSE(pNullLabel); - GCPROTECT_END(); - END_QCALL; -} + MethodDesc* pMD = GetInstantiatedArrayMethod(m_pargs->m_pMarshalInfo, METHOD__STUBHELPERS__CONVERT_ARRAY_CONTENTS_TO_UNMANAGED); -extern "C" void QCALLTYPE MngdNativeArrayMarshaler_ClearNativeContents(MngdNativeArrayMarshaler* pThis, void** pNativeHome, INT32 cElements) -{ - QCALL_CONTRACT; - BEGIN_QCALL; - GCX_COOP(); + EmitLoadManagedValue(pslILEmit); + EmitLoadNativeHomeAddr(pslILEmit); + pslILEmit->EmitLDC(mops.additive); + pslILEmit->EmitCALL(pslILEmit->GetToken(pMD), 3, 0); - if (*pNativeHome != NULL) - { - const OleVariant::Marshaler *pMarshaler = OleVariant::GetMarshalerForVarType(pThis->m_vt, FALSE); + pslILEmit->EmitBR(pDoneLabel); - if (pMarshaler != NULL && pMarshaler->ClearOleArray != NULL) - { - pMarshaler->ClearOleArray(*pNativeHome, cElements, pThis->m_pElementMT); - } - } + // When the managed array is null, zero the native inline buffer. + pslILEmit->EmitLabel(pNullLabel); + EmitLoadNativeHomeAddr(pslILEmit); + pslILEmit->EmitLDC(0); + pslILEmit->EmitLDC(uNativeSize); + pslILEmit->EmitINITBLK(); - END_QCALL; + pslILEmit->EmitLabel(pDoneLabel); } -void ILFixedArrayMarshaler::EmitCreateMngdMarshaler(ILCodeStream* pslILEmit) +void ILFixedArrayMarshaler::EmitConvertContentsNativeToCLR(ILCodeStream* pslILEmit) { STANDARD_VM_CONTRACT; - m_dwMngdMarshalerLocalNum = pslILEmit->NewLocal(ELEMENT_TYPE_I); - - pslILEmit->EmitLDC(sizeof(MngdFixedArrayMarshaler)); - pslILEmit->EmitLOCALLOC(); - pslILEmit->EmitSTLOC(m_dwMngdMarshalerLocalNum); - CREATE_MARSHALER_CARRAY_OPERANDS mops; m_pargs->m_pMarshalInfo->GetMops(&mops); - pslILEmit->EmitLDLOC(m_dwMngdMarshalerLocalNum); - - pslILEmit->EmitLDTOKEN(pslILEmit->GetToken(mops.methodTable)); - pslILEmit->EmitCALL(METHOD__RT_TYPE_HANDLE__TO_INTPTR, 1, 1); - - DWORD dwFlags = mops.elementType; - dwFlags |= (((DWORD)mops.bestfitmapping) << 16); - dwFlags |= (((DWORD)mops.throwonunmappablechar) << 24); - - pslILEmit->EmitLDC(dwFlags); + MethodDesc* pMD = GetInstantiatedArrayMethod(m_pargs->m_pMarshalInfo, METHOD__STUBHELPERS__CONVERT_ARRAY_CONTENTS_TO_MANAGED); + EmitLoadManagedValue(pslILEmit); + EmitLoadNativeHomeAddr(pslILEmit); pslILEmit->EmitLDC(mops.additive); - - pslILEmit->EmitCALL(METHOD__MNGD_FIXED_ARRAY_MARSHALER__CREATE_MARSHALER, 4, 0); -} - -extern "C" void QCALLTYPE MngdFixedArrayMarshaler_ConvertContentsToNative(MngdFixedArrayMarshaler* pThis, QCall::ObjectHandleOnStack pManagedHome, void* pNativeHome) -{ - QCALL_CONTRACT; - BEGIN_QCALL; - - GCX_COOP(); - - BASEARRAYREF arrayRef = NULL; - GCPROTECT_BEGIN(arrayRef); - arrayRef = (BASEARRAYREF)pManagedHome.Get(); - - if (pThis->m_vt == VTHACK_ANSICHAR) - { - SIZE_T nativeSize = sizeof(CHAR) * pThis->m_cElements; - - if (arrayRef == NULL) - { - FillMemory(pNativeHome, nativeSize, 0); - } - else - { - InternalWideToAnsi((const WCHAR*)arrayRef->GetDataPtr(), - pThis->m_cElements, - (CHAR*)pNativeHome, - (int)nativeSize, - pThis->m_BestFitMap, - pThis->m_ThrowOnUnmappableChar); - } - } - else - { - SIZE_T cbElement = OleVariant::GetElementSizeForVarType(pThis->m_vt, pThis->m_pElementMT); - SIZE_T nativeSize = cbElement * pThis->m_cElements; - - if (arrayRef == NULL) - { - FillMemory(pNativeHome, nativeSize, 0); - } - else - { - - const OleVariant::Marshaler* pMarshaler = OleVariant::GetMarshalerForVarType(pThis->m_vt, TRUE); - SIZE_T cElements = arrayRef->GetNumComponents(); - if (pMarshaler == NULL || pMarshaler->ComToOleArray == NULL) - { - _ASSERTE(!OleVariant::GetTypeHandleForVarType(pThis->m_vt).GetMethodTable()->ContainsGCPointers()); - memcpyNoGCRefs(pNativeHome, arrayRef->GetDataPtr(), nativeSize); - } - else - { - pMarshaler->ComToOleArray(&arrayRef, pNativeHome, pThis->m_pElementMT, pThis->m_BestFitMap, - pThis->m_ThrowOnUnmappableChar, FALSE, pThis->m_cElements); - } - } - } - - GCPROTECT_END(); - END_QCALL; + pslILEmit->EmitCALL(pslILEmit->GetToken(pMD), 3, 0); } -extern "C" void QCALLTYPE MngdFixedArrayMarshaler_ConvertSpaceToManaged(MngdFixedArrayMarshaler* pThis, - QCall::ObjectHandleOnStack pManagedHome, void* pNativeHome) +void ILFixedArrayMarshaler::EmitLoadNativeSize(ILCodeStream* pslILEmit) { - QCALL_CONTRACT; - - BEGIN_QCALL; - - GCX_COOP(); - - // @todo: lookup this class before marshal time - if (pThis->m_Array.IsNull()) - { - // Get proper array class name & type - pThis->m_Array = OleVariant::GetArrayForVarType(pThis->m_vt, TypeHandle(pThis->m_pElementMT)); - if (pThis->m_Array.IsNull()) - COMPlusThrow(kTypeLoadException); - } - // - // Allocate array - // - - OBJECTREF arrayRef = AllocateSzArray(pThis->m_Array, pThis->m_cElements); - pManagedHome.Set(arrayRef); + STANDARD_VM_CONTRACT; - END_QCALL; + CREATE_MARSHALER_CARRAY_OPERANDS mops; + m_pargs->m_pMarshalInfo->GetMops(&mops); + pslILEmit->EmitLDC(mops.additive); } -extern "C" void QCALLTYPE MngdFixedArrayMarshaler_ConvertContentsToManaged(MngdFixedArrayMarshaler* pThis, QCall::ObjectHandleOnStack pManagedHome, void* pNativeHome) +void ILFixedArrayMarshaler::EmitClearNativeContents(ILCodeStream* pslILEmit) { - QCALL_CONTRACT; - BEGIN_QCALL; - - GCX_COOP(); - - BASEARRAYREF arrayRef = NULL; - - GCPROTECT_BEGIN(arrayRef); - arrayRef = (BASEARRAYREF)pManagedHome.Get(); - - if (pThis->m_vt == VTHACK_ANSICHAR) - { - MultiByteToWideChar(CP_ACP, - MB_PRECOMPOSED, - (const CHAR*)pNativeHome, - pThis->m_cElements * sizeof(CHAR), // size, in bytes, of in buffer - (WCHAR*)(arrayRef->GetDataPtr()), - pThis->m_cElements); // size, in WCHAR's of outbuffer - } - else - { - const OleVariant::Marshaler* pMarshaler = OleVariant::GetMarshalerForVarType(pThis->m_vt, TRUE); - - SIZE_T cbElement = OleVariant::GetElementSizeForVarType(pThis->m_vt, pThis->m_pElementMT); - SIZE_T nativeSize = cbElement * pThis->m_cElements; + STANDARD_VM_CONTRACT; + MethodDesc* pMD = GetInstantiatedArrayMethod(m_pargs->m_pMarshalInfo, METHOD__STUBHELPERS__FREE_ARRAY_CONTENTS); - if (pMarshaler == NULL || pMarshaler->OleToComArray == NULL) - { - // If we are copying variants, strings, etc, we need to use write barrier - _ASSERTE(!OleVariant::GetTypeHandleForVarType(pThis->m_vt).GetMethodTable()->ContainsGCPointers()); - memcpyNoGCRefs(arrayRef->GetDataPtr(), pNativeHome, nativeSize); - } - else - { - pMarshaler->OleToComArray(pNativeHome, &arrayRef, pThis->m_pElementMT); - } - } - - GCPROTECT_END(); - END_QCALL; + EmitLoadNativeHomeAddr(pslILEmit); + EmitLoadNativeSize(pslILEmit); + pslILEmit->EmitCALL(pslILEmit->GetToken(pMD), 2, 0); } -extern "C" void QCALLTYPE MngdFixedArrayMarshaler_ClearNativeContents(MngdFixedArrayMarshaler* pThis, void* pNativeHome) +void ILFixedArrayMarshaler::EmitClearNative(ILCodeStream* pslILEmit) { - QCALL_CONTRACT; - BEGIN_QCALL; - GCX_COOP(); - - const OleVariant::Marshaler* pMarshaler = OleVariant::GetMarshalerForVarType(pThis->m_vt, FALSE); - - if (pMarshaler != NULL && pMarshaler->ClearOleArray != NULL) - { - pMarshaler->ClearOleArray(pNativeHome, pThis->m_cElements, pThis->m_pElementMT); - } + STANDARD_VM_CONTRACT; - END_QCALL; + // For fixed arrays, native space is inline - just clear element contents, don't free a buffer. + EmitClearNativeContents(pslILEmit); } #ifdef FEATURE_COMINTEROP @@ -4643,7 +4699,20 @@ void ILSafeArrayMarshaler::EmitCreateMngdMarshaler(ILCodeStream* pslILEmit) pslILEmit->EmitLDC(m_pargs->m_pMarshalInfo->GetArrayRank()); pslILEmit->EmitLDC(dwFlags); - pslILEmit->EmitCALL(METHOD__MNGD_SAFE_ARRAY_MARSHALER__CREATE_MARSHALER, 4, 0); + // Resolve the instantiated content conversion methods at stub generation time + // and emit ldftn to pass their entry points to CreateMarshaler. + BOOL bNativeDataValid = !!(fStatic & MngdSafeArrayMarshaler::SCSF_NativeDataValid); + MethodDesc* pConvertToNativeMD = GetInstantiatedSafeArrayMethod( + METHOD__STUBHELPERS__CONVERT_ARRAY_CONTENTS_TO_UNMANAGED, + mops.elementType, mops.methodTable, FALSE, bNativeDataValid); + pslILEmit->EmitLDFTN(pslILEmit->GetToken(pConvertToNativeMD)); + + MethodDesc* pConvertToManagedMD = GetInstantiatedSafeArrayMethod( + METHOD__STUBHELPERS__CONVERT_ARRAY_CONTENTS_TO_MANAGED, + mops.elementType, mops.methodTable, FALSE); + pslILEmit->EmitLDFTN(pslILEmit->GetToken(pConvertToManagedMD)); + + pslILEmit->EmitCALL(METHOD__MNGD_SAFE_ARRAY_MARSHALER__CREATE_MARSHALER, 6, 0); } void ILSafeArrayMarshaler::EmitConvertContentsNativeToCLR(ILCodeStream* pslILEmit) @@ -4680,7 +4749,7 @@ void ILSafeArrayMarshaler::EmitConvertContentsCLRToNative(ILCodeStream* pslILEmi pslILEmit->EmitCALL(METHOD__MNGD_SAFE_ARRAY_MARSHALER__CONVERT_CONTENTS_TO_NATIVE, 4, 0); } -extern "C" void QCALLTYPE MngdSafeArrayMarshaler_CreateMarshaler(MngdSafeArrayMarshaler* pThis, MethodTable* pMT, UINT32 iRank, UINT32 dwFlags) +extern "C" void QCALLTYPE MngdSafeArrayMarshaler_CreateMarshaler(MngdSafeArrayMarshaler* pThis, MethodTable* pMT, UINT32 iRank, UINT32 dwFlags, PCODE pConvertToNative, PCODE pConvertToManaged) { QCALL_CONTRACT_NO_GC_TRANSITION; @@ -4689,6 +4758,8 @@ extern "C" void QCALLTYPE MngdSafeArrayMarshaler_CreateMarshaler(MngdSafeArrayMa pThis->m_vt = (VARTYPE)dwFlags; pThis->m_fStatic = (BYTE)(dwFlags >> 16); pThis->m_nolowerbounds = (BYTE)(dwFlags >> 24); + pThis->m_pConvertContentsToNativeCode = pConvertToNative; + pThis->m_pConvertContentsToManagedCode = pConvertToManaged; } extern "C" void QCALLTYPE MngdSafeArrayMarshaler_ConvertSpaceToNative(MngdSafeArrayMarshaler* pThis, QCall::ObjectHandleOnStack pManagedHome, void** pNativeHome) @@ -4762,7 +4833,7 @@ extern "C" void QCALLTYPE MngdSafeArrayMarshaler_ConvertContentsToNative(MngdSaf (SAFEARRAY*)*pNativeHome, pThis->m_vt, pThis->m_pElementMT, - (pThis->m_fStatic & MngdSafeArrayMarshaler::SCSF_NativeDataValid)); + pThis->m_pConvertContentsToNativeCode); } GCPROTECT_END(); @@ -4855,7 +4926,8 @@ extern "C" void QCALLTYPE MngdSafeArrayMarshaler_ConvertContentsToManaged(MngdSa OleVariant::MarshalArrayRefForSafeArray(pNative, &arrayRef, pThis->m_vt, - pThis->m_pElementMT); + pThis->m_pElementMT, + pThis->m_pConvertContentsToManagedCode); } GCPROTECT_END(); @@ -4912,3 +4984,4 @@ void ILReferenceCustomMarshaler::EmitCreateMngdMarshaler(ILCodeStream* pslILEmit pslILEmit->EmitSTLOC(m_dwMngdMarshalerLocalNum); // Store the ICustomMarshaler as our marshaler state } + diff --git a/src/coreclr/vm/ilmarshalers.h b/src/coreclr/vm/ilmarshalers.h index e4f5d3cb32c56c..e8e0fe0e2896a7 100644 --- a/src/coreclr/vm/ilmarshalers.h +++ b/src/coreclr/vm/ilmarshalers.h @@ -3268,24 +3268,16 @@ class ILMngdMarshaler : public ILMarshaler const BinderMethodID m_idClearManaged; }; -class ILNativeArrayMarshaler : public ILMngdMarshaler +class ILNativeArrayMarshaler : public ILMarshaler { public: enum { c_fInOnly = FALSE, + c_nativeSize = TARGET_POINTER_SIZE, }; - ILNativeArrayMarshaler() : - ILMngdMarshaler( - METHOD__MNGD_NATIVE_ARRAY_MARSHALER__CONVERT_SPACE_TO_MANAGED, - METHOD__MNGD_NATIVE_ARRAY_MARSHALER__CONVERT_CONTENTS_TO_MANAGED, - METHOD__MNGD_NATIVE_ARRAY_MARSHALER__CONVERT_SPACE_TO_NATIVE, - METHOD__MNGD_NATIVE_ARRAY_MARSHALER__CONVERT_CONTENTS_TO_NATIVE, - METHOD__MNGD_NATIVE_ARRAY_MARSHALER__CLEAR_NATIVE, - METHOD__MNGD_NATIVE_ARRAY_MARSHALER__CLEAR_NATIVE_CONTENTS, - METHOD__NIL - ) + ILNativeArrayMarshaler() { LIMITED_METHOD_CONTRACT; m_dwSavedSizeArg = LOCAL_NUM_UNUSED; @@ -3297,6 +3289,8 @@ class ILNativeArrayMarshaler : public ILMngdMarshaler void EmitSetupArgumentForMarshalling(ILCodeStream* pslILEmit) override; void EmitConvertSpaceNativeToCLR(ILCodeStream* pslILEmit) override; void EmitConvertSpaceCLRToNative(ILCodeStream* pslILEmit) override; + void EmitConvertContentsCLRToNative(ILCodeStream* pslILEmit) override; + void EmitConvertContentsNativeToCLR(ILCodeStream* pslILEmit) override; void EmitClearNative(ILCodeStream* pslILEmit) override; void EmitClearNativeContents(ILCodeStream* pslILEmit) override; @@ -3305,39 +3299,39 @@ class ILNativeArrayMarshaler : public ILMngdMarshaler LIMITED_METHOD_CONTRACT; return false; } + protected: + LocalDesc GetNativeType() override + { + LIMITED_METHOD_CONTRACT; + return LocalDesc(ELEMENT_TYPE_I); + } + + LocalDesc GetManagedType() override + { + LIMITED_METHOD_CONTRACT; + return LocalDesc(ELEMENT_TYPE_OBJECT); + } + + bool NeedsClearNative() override + { + LIMITED_METHOD_CONTRACT; + return true; + } + BOOL CheckSizeParamIndexArg(const CREATE_MARSHALER_CARRAY_OPERANDS &mops, CorElementType *pElementType); // Calculate element count and load it on evaluation stack void EmitLoadElementCount(ILCodeStream* pslILEmit); - void EmitCreateMngdMarshaler(ILCodeStream* pslILEmit) override; - void EmitLoadNativeSize(ILCodeStream* pslILEmit); void EmitNewSavedSizeArgLocal(ILCodeStream* pslILEmit); -private : DWORD m_dwSavedSizeArg; }; -struct MngdNativeArrayMarshaler -{ - MethodTable* m_pElementMT; - TypeHandle m_Array; - BOOL m_NativeDataValid; - BOOL m_BestFitMap; - BOOL m_ThrowOnUnmappableChar; - VARTYPE m_vt; -}; - -extern "C" void QCALLTYPE MngdNativeArrayMarshaler_ConvertSpaceToNative(MngdNativeArrayMarshaler* pThis, QCall::ObjectHandleOnStack pManagedHome, void** pNativeHome); -extern "C" void QCALLTYPE MngdNativeArrayMarshaler_ConvertContentsToNative(MngdNativeArrayMarshaler* pThis, QCall::ObjectHandleOnStack pManagedHome, void** pNativeHome); -extern "C" void QCALLTYPE MngdNativeArrayMarshaler_ConvertSpaceToManaged(MngdNativeArrayMarshaler* pThis, QCall::ObjectHandleOnStack pManagedHome, void** pNativeHome, INT32 cElements); -extern "C" void QCALLTYPE MngdNativeArrayMarshaler_ConvertContentsToManaged(MngdNativeArrayMarshaler* pThis, QCall::ObjectHandleOnStack pManagedHome, void** pNativeHome); -extern "C" void QCALLTYPE MngdNativeArrayMarshaler_ClearNativeContents(MngdNativeArrayMarshaler* pThis, void** pNativeHome, INT32 cElements); - -class ILFixedArrayMarshaler : public ILMngdMarshaler +class ILFixedArrayMarshaler : public ILMarshaler { public: enum @@ -3346,20 +3340,6 @@ class ILFixedArrayMarshaler : public ILMngdMarshaler c_fInOnly = FALSE }; - ILFixedArrayMarshaler() : - ILMngdMarshaler( - METHOD__MNGD_FIXED_ARRAY_MARSHALER__CONVERT_SPACE_TO_MANAGED, - METHOD__MNGD_FIXED_ARRAY_MARSHALER__CONVERT_CONTENTS_TO_MANAGED, - METHOD__MNGD_FIXED_ARRAY_MARSHALER__CONVERT_SPACE_TO_NATIVE, - METHOD__MNGD_FIXED_ARRAY_MARSHALER__CONVERT_CONTENTS_TO_NATIVE, - METHOD__MNGD_FIXED_ARRAY_MARSHALER__CLEAR_NATIVE_CONTENTS, - METHOD__MNGD_FIXED_ARRAY_MARSHALER__CLEAR_NATIVE_CONTENTS, - METHOD__NIL - ) - { - LIMITED_METHOD_CONTRACT; - } - bool SupportsArgumentMarshal(DWORD dwMarshalFlags, UINT* pErrorResID) override { LIMITED_METHOD_CONTRACT; @@ -3374,23 +3354,32 @@ class ILFixedArrayMarshaler : public ILMngdMarshaler protected: - void EmitCreateMngdMarshaler(ILCodeStream* pslILEmit) override; -}; + LocalDesc GetNativeType() override + { + LIMITED_METHOD_CONTRACT; + return LocalDesc(ELEMENT_TYPE_I); + } -struct MngdFixedArrayMarshaler -{ - MethodTable* m_pElementMT; - TypeHandle m_Array; - BOOL m_BestFitMap; - BOOL m_ThrowOnUnmappableChar; - VARTYPE m_vt; - UINT32 m_cElements; -}; + LocalDesc GetManagedType() override + { + LIMITED_METHOD_CONTRACT; + return LocalDesc(ELEMENT_TYPE_OBJECT); + } + + bool NeedsClearNative() override + { + LIMITED_METHOD_CONTRACT; + return true; + } -extern "C" void QCALLTYPE MngdFixedArrayMarshaler_ConvertContentsToNative(MngdFixedArrayMarshaler* pThis, QCall::ObjectHandleOnStack pManagedHome, void* pNativeHome); -extern "C" void QCALLTYPE MngdFixedArrayMarshaler_ConvertSpaceToManaged(MngdFixedArrayMarshaler* pThis, QCall::ObjectHandleOnStack pManagedHome, void* pNativeHome); -extern "C" void QCALLTYPE MngdFixedArrayMarshaler_ConvertContentsToManaged(MngdFixedArrayMarshaler* pThis, QCall::ObjectHandleOnStack pManagedHome, void* pNativeHome); -extern "C" void QCALLTYPE MngdFixedArrayMarshaler_ClearNativeContents(MngdFixedArrayMarshaler* pThis, void* pNativeHome); + void EmitConvertSpaceCLRToNative(ILCodeStream* pslILEmit) override; + void EmitConvertSpaceNativeToCLR(ILCodeStream* pslILEmit) override; + void EmitConvertContentsCLRToNative(ILCodeStream* pslILEmit) override; + void EmitConvertContentsNativeToCLR(ILCodeStream* pslILEmit) override; + void EmitLoadNativeSize(ILCodeStream* pslILEmit); + void EmitClearNativeContents(ILCodeStream* pslILEmit) override; + void EmitClearNative(ILCodeStream* pslILEmit) override; +}; #ifdef FEATURE_COMINTEROP class ILSafeArrayMarshaler : public ILMngdMarshaler @@ -3469,9 +3458,11 @@ class MngdSafeArrayMarshaler VARTYPE m_vt; BYTE m_fStatic; // StaticCheckStateFlags BYTE m_nolowerbounds; + PCODE m_pConvertContentsToNativeCode; + PCODE m_pConvertContentsToManagedCode; }; -extern "C" void QCALLTYPE MngdSafeArrayMarshaler_CreateMarshaler(MngdSafeArrayMarshaler* pThis, MethodTable* pMT, UINT32 iRank, UINT32 dwFlags); +extern "C" void QCALLTYPE MngdSafeArrayMarshaler_CreateMarshaler(MngdSafeArrayMarshaler* pThis, MethodTable* pMT, UINT32 iRank, UINT32 dwFlags, PCODE pConvertToNative, PCODE pConvertToManaged); extern "C" void QCALLTYPE MngdSafeArrayMarshaler_ConvertSpaceToNative(MngdSafeArrayMarshaler* pThis, QCall::ObjectHandleOnStack pManagedHome, void** pNativeHome); extern "C" void QCALLTYPE MngdSafeArrayMarshaler_ConvertContentsToNative(MngdSafeArrayMarshaler* pThis, QCall::ObjectHandleOnStack pManagedHome, void** pNativeHome, QCall::ObjectHandleOnStack pOriginalManaged); extern "C" void QCALLTYPE MngdSafeArrayMarshaler_ConvertSpaceToManaged(MngdSafeArrayMarshaler* pThis, QCall::ObjectHandleOnStack pManagedHome, void** pNativeHome); diff --git a/src/coreclr/vm/interopconverter.cpp b/src/coreclr/vm/interopconverter.cpp index 4f951c97634037..f9bc94daf257a1 100644 --- a/src/coreclr/vm/interopconverter.cpp +++ b/src/coreclr/vm/interopconverter.cpp @@ -7,7 +7,6 @@ #include "excep.h" #include "interoputil.h" #include "interopconverter.h" -#include "olevariant.h" #include "comcallablewrapper.h" #ifdef FEATURE_COMINTEROP diff --git a/src/coreclr/vm/marshalnative.cpp b/src/coreclr/vm/marshalnative.cpp index db6bfa48942440..b9163b700bad00 100644 --- a/src/coreclr/vm/marshalnative.cpp +++ b/src/coreclr/vm/marshalnative.cpp @@ -105,6 +105,14 @@ extern "C" BOOL QCALLTYPE MarshalNative_HasLayout(QCall::TypeHandle t, BOOL* pIs BEGIN_QCALL; TypeHandle th = t.AsTypeHandle(); + + if (th.IsEnum()) + { + // Enums don't have native layout info, but they marshal identically + // to their underlying primitive type. + th = CoreLibBinder::GetElementType(th.GetVerifierCorElementType()); + } + if (th.HasLayout()) { *pIsBlittable = th.IsBlittable(); diff --git a/src/coreclr/vm/metasig.h b/src/coreclr/vm/metasig.h index d304d63ff9d908..88c986a9c6e4b7 100644 --- a/src/coreclr/vm/metasig.h +++ b/src/coreclr/vm/metasig.h @@ -224,6 +224,7 @@ DEFINE_METASIG_T(SM(PtrPropertyInfo_PtrException_RetInt, P(C(PROPERTY_INFO)) P(C DEFINE_METASIG(SM(IntPtr_RefObj_IntPtr_RetVoid, I r(j) I, v)) DEFINE_METASIG(SM(IntPtr_RefObj_IntPtr_Int_RetVoid, I r(j) I i,v)) DEFINE_METASIG(SM(IntPtr_IntPtr_Int_Int_RetVoid, I I i i, v)) +DEFINE_METASIG(SM(IntPtr_IntPtr_Int_Int_IntPtr_IntPtr_RetVoid, I I i i I I, v)) DEFINE_METASIG(SM(IntPtr_RefObj_IntPtr_Obj_RetVoid, I r(j) I j, v)) DEFINE_METASIG(SM(Obj_Int_RetVoid, j i,v)) DEFINE_METASIG(SM(PtrVoid_Obj_RetObj, P(v) j, j)) @@ -335,6 +336,7 @@ DEFINE_METASIG(SM(PtrSByt_RetStr, P(B), s)) DEFINE_METASIG(SM(PtrSByt_Int_Int_RetStr, P(B) i i, s)) DEFINE_METASIG_T(SM(PtrSByt_Int_Int_Encoding_RetStr, P(B) i i C(ENCODING), s)) DEFINE_METASIG(IM(Obj_Int_RetIntPtr, j i, I)) +DEFINE_METASIG(IM(Obj_Int_RetVoid, j i, v)) DEFINE_METASIG(IM(Char_Int_RetVoid, u i, v)) diff --git a/src/coreclr/vm/mlinfo.cpp b/src/coreclr/vm/mlinfo.cpp index db04397c72dd49..dfba06e9a1eba2 100644 --- a/src/coreclr/vm/mlinfo.cpp +++ b/src/coreclr/vm/mlinfo.cpp @@ -15,7 +15,7 @@ #include "../dlls/mscorrc/resource.h" #include "typeparse.h" #include "comdelegate.h" -#include "olevariant.h" +#include "fieldmarshaler.h" #include "ilmarshalers.h" #include "interoputil.h" #include "mdfileformat.h" // For CPackedLen @@ -2190,6 +2190,7 @@ HRESULT MarshalInfo::HandleArrayElemType(NativeTypeParamInfo *pParamInfo, TypeHa // Set the array type handle and VARTYPE to use for marshalling. m_hndArrayElemType = arrayMarshalInfo.GetElementTypeHandle(); m_arrayElementType = arrayMarshalInfo.GetElementVT(); + m_arrayElementNativeType = arrayMarshalInfo.GetElementNativeType(); if (m_type == MARSHAL_TYPE_NATIVEARRAY || m_type == MARSHAL_TYPE_FIXED_ARRAY) { @@ -3071,7 +3072,7 @@ DispParamMarshaler *MarshalInfo::GenerateDispParamMarshaler() { THROWS; GC_TRIGGERS; - MODE_ANY; + MODE_PREEMPTIVE; INJECT_FAULT(COMPlusThrowOM()); POSTCONDITION(CheckPointer(RETVAL, NULL_OK)); } @@ -3321,7 +3322,7 @@ void ArrayMarshalInfo::InitElementInfo(CorNativeType arrayNativeType, MarshalInf { case NATIVE_TYPE_I1: //fallthru case NATIVE_TYPE_U1: - m_vtElement = VTHACK_ANSICHAR; + m_ntElement = NATIVE_TYPE_U1; break; case NATIVE_TYPE_I2: //fallthru @@ -3337,7 +3338,10 @@ void ArrayMarshalInfo::InitElementInfo(CorNativeType arrayNativeType, MarshalInf m_vtElement = VT_UI2; else #endif // FEATURE_COMINTEROP - m_vtElement = isAnsi ? VTHACK_ANSICHAR : VT_UI2; + if (isAnsi) + m_ntElement = NATIVE_TYPE_U1; + else + m_vtElement = VT_UI2; } } else if (etElement == ELEMENT_TYPE_BOOLEAN) @@ -3345,7 +3349,7 @@ void ArrayMarshalInfo::InitElementInfo(CorNativeType arrayNativeType, MarshalInf switch (ntElement) { case NATIVE_TYPE_BOOLEAN: - m_vtElement = VTHACK_WINBOOL; + m_ntElement = NATIVE_TYPE_BOOLEAN; break; #ifdef FEATURE_COMINTEROP @@ -3356,7 +3360,7 @@ void ArrayMarshalInfo::InitElementInfo(CorNativeType arrayNativeType, MarshalInf case NATIVE_TYPE_I1 : case NATIVE_TYPE_U1 : - m_vtElement = VTHACK_CBOOL; + m_ntElement = NATIVE_TYPE_I1; break; // Compat: if the native type doesn't make sense, we need to ignore it and not report an error. @@ -3371,7 +3375,7 @@ void ArrayMarshalInfo::InitElementInfo(CorNativeType arrayNativeType, MarshalInf else #endif // FEATURE_COMINTEROP { - m_vtElement = VTHACK_WINBOOL; + m_ntElement = NATIVE_TYPE_BOOLEAN; } break; } @@ -3529,7 +3533,7 @@ void ArrayMarshalInfo::InitElementInfo(CorNativeType arrayNativeType, MarshalInf } else { - m_vtElement = OleVariant::GetVarTypeForTypeHandle(m_thElement); + m_vtElement = GetVarTypeForTypeHandle(m_thElement); } } #ifdef FEATURE_COMINTEROP diff --git a/src/coreclr/vm/mlinfo.h b/src/coreclr/vm/mlinfo.h index c56380d58cb789..496856ec49a798 100644 --- a/src/coreclr/vm/mlinfo.h +++ b/src/coreclr/vm/mlinfo.h @@ -65,6 +65,7 @@ struct CREATE_MARSHALER_CARRAY_OPERANDS UINT32 multiplier; UINT32 additive; VARTYPE elementType; + CorNativeType elementNativeType; UINT16 countParamIdx; BYTE bestfitmapping; BYTE throwonunmappablechar; @@ -398,6 +399,7 @@ class MarshalInfo WRAPPER_NO_CONTRACT; pMopsOut->methodTable = m_hndArrayElemType.AsMethodTable(); pMopsOut->elementType = m_arrayElementType; + pMopsOut->elementNativeType = m_arrayElementNativeType; pMopsOut->countParamIdx = m_countParamIdx; pMopsOut->multiplier = m_multiplier; pMopsOut->additive = m_additive; @@ -480,6 +482,7 @@ class MarshalInfo MethodDesc* m_pMD; // Save MethodDesc for later inspection so that we can pass SizeParamIndex by ref TypeHandle m_hndArrayElemType; VARTYPE m_arrayElementType; + CorNativeType m_arrayElementNativeType; int m_iArrayRank; BOOL m_nolowerbounds; // if managed type is SZARRAY, don't allow lower bounds @@ -545,6 +548,7 @@ class ArrayMarshalInfo public: ArrayMarshalInfo(ArrayMarshalInfoFlags flags) : m_vtElement(VT_EMPTY) + , m_ntElement(NATIVE_TYPE_DEFAULT) , m_errorResourceId(0) , m_flags(flags) #ifdef FEATURE_COMINTEROP @@ -592,6 +596,12 @@ class ArrayMarshalInfo } } + CorNativeType GetElementNativeType() + { + LIMITED_METHOD_CONTRACT; + return m_ntElement; + } + BOOL IsValid() { CONTRACTL @@ -602,7 +612,7 @@ class ArrayMarshalInfo } CONTRACTL_END; - return m_vtElement != VT_EMPTY; + return m_vtElement != VT_EMPTY || m_ntElement != NATIVE_TYPE_DEFAULT; } BOOL IsSafeArraySubTypeExplicitlySpecified() @@ -654,6 +664,7 @@ class ArrayMarshalInfo TypeHandle m_thElement; TypeHandle m_thInterfaceArrayElementClass; VARTYPE m_vtElement; + CorNativeType m_ntElement; DWORD m_errorResourceId; ArrayMarshalInfoFlags m_flags; diff --git a/src/coreclr/vm/olevariant.cpp b/src/coreclr/vm/olevariant.cpp index 846b69b3e4a4c0..2f8d856c030c54 100644 --- a/src/coreclr/vm/olevariant.cpp +++ b/src/coreclr/vm/olevariant.cpp @@ -22,249 +22,6 @@ * Local constants * ------------------------------------------------------------------------- */ -#define NO_MAPPING ((BYTE) -1) - - -/* ------------------------------------------------------------------------- * - * Mapping routines - * ------------------------------------------------------------------------- */ - -VARTYPE GetVarTypeForCorElementType(CorElementType type) -{ - CONTRACTL - { - THROWS; - GC_NOTRIGGER; - MODE_ANY; - } - CONTRACTL_END; - - static const BYTE map[] = - { - VT_EMPTY, // ELEMENT_TYPE_END - VT_VOID, // ELEMENT_TYPE_VOID - VT_BOOL, // ELEMENT_TYPE_BOOLEAN - VT_UI2, // ELEMENT_TYPE_CHAR - VT_I1, // ELEMENT_TYPE_I1 - VT_UI1, // ELEMENT_TYPE_U1 - VT_I2, // ELEMENT_TYPE_I2 - VT_UI2, // ELEMENT_TYPE_U2 - VT_I4, // ELEMENT_TYPE_I4 - VT_UI4, // ELEMENT_TYPE_U4 - VT_I8, // ELEMENT_TYPE_I8 - VT_UI8, // ELEMENT_TYPE_U8 - VT_R4, // ELEMENT_TYPE_R4 - VT_R8, // ELEMENT_TYPE_R8 - VT_BSTR, // ELEMENT_TYPE_STRING - }; - - _ASSERTE(type < (CorElementType) (sizeof(map) / sizeof(map[0]))); - - VARTYPE vt = VARTYPE(map[type]); - - if (vt == NO_MAPPING) - COMPlusThrow(kArgumentException, IDS_EE_COM_UNSUPPORTED_SIG); - - return vt; -} - -// -// GetTypeHandleForVarType returns the TypeHandle for a given -// VARTYPE. This is called by the marshaller in the context of -// a function call. -// - -TypeHandle OleVariant::GetTypeHandleForVarType(VARTYPE vt) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_ANY; - } - CONTRACTL_END; - - static const BYTE map[] = - { - CLASS__EMPTY, // VT_EMPTY - CLASS__NULL, // VT_NULL - CLASS__INT16, // VT_I2 - CLASS__INT32, // VT_I4 - CLASS__SINGLE, // VT_R4 - CLASS__DOUBLE, // VT_R8 - CLASS__DECIMAL, // VT_CY - CLASS__DATE_TIME, // VT_DATE - CLASS__STRING, // VT_BSTR - CLASS__OBJECT, // VT_DISPATCH - CLASS__INT32, // VT_ERROR - CLASS__BOOLEAN, // VT_BOOL - NO_MAPPING, // VT_VARIANT - CLASS__OBJECT, // VT_UNKNOWN - CLASS__DECIMAL, // VT_DECIMAL - NO_MAPPING, // unused - CLASS__SBYTE, // VT_I1 - CLASS__BYTE, // VT_UI1 - CLASS__UINT16, // VT_UI2 - CLASS__UINT32, // VT_UI4 - CLASS__INT64, // VT_I8 - CLASS__UINT64, // VT_UI8 - CLASS__INT32, // VT_INT - CLASS__UINT32, // VT_UINT - CLASS__VOID, // VT_VOID - NO_MAPPING, // VT_HRESULT - NO_MAPPING, // VT_PTR - NO_MAPPING, // VT_SAFEARRAY - NO_MAPPING, // VT_CARRAY - NO_MAPPING, // VT_USERDEFINED - NO_MAPPING, // VT_LPSTR - NO_MAPPING, // VT_LPWSTR - NO_MAPPING, // unused - NO_MAPPING, // unused - NO_MAPPING, // unused - NO_MAPPING, // unused - CLASS__OBJECT, // VT_RECORD - }; - - BinderClassID type = CLASS__NIL; - - // Validate the arguments. - _ASSERTE((vt & VT_BYREF) == 0); - - // Array's map to object. - if (vt & VT_ARRAY) - return TypeHandle(CoreLibBinder::GetClass(CLASS__OBJECT)); - - // This is prety much a workaround because you cannot cast a CorElementType into a CVTYPE - if (vt > VT_RECORD || (type = (BinderClassID) map[vt]) == NO_MAPPING) - COMPlusThrow(kArgumentException, IDS_EE_COM_UNSUPPORTED_TYPE); - - return TypeHandle(CoreLibBinder::GetClass(type)); -} // CVTypes OleVariant::GetCVTypeForVarType() - -VARTYPE OleVariant::GetVarTypeForTypeHandle(TypeHandle type) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_ANY; - } - CONTRACTL_END; - - // Handle primitive types. - CorElementType elemType = type.GetSignatureCorElementType(); - if (elemType <= ELEMENT_TYPE_R8) - return GetVarTypeForCorElementType(elemType); - - // Types incompatible with interop. - if (type.IsTypeDesc()) - COMPlusThrow(kArgumentException, IDS_EE_COM_UNSUPPORTED_SIG); - - // Handle objects. - MethodTable * pMT = type.AsMethodTable(); - - if (pMT == g_pStringClass) - return VT_BSTR; - if (pMT == g_pObjectClass) - return VT_VARIANT; - - // We need to make sure the CVClasses table is populated. - if(CoreLibBinder::IsClass(pMT, CLASS__DATE_TIME)) - return VT_DATE; - if(CoreLibBinder::IsClass(pMT, CLASS__DECIMAL)) - return VT_DECIMAL; - -#ifdef HOST_64BIT - if (CoreLibBinder::IsClass(pMT, CLASS__INTPTR)) - return VT_I8; - if (CoreLibBinder::IsClass(pMT, CLASS__UINTPTR)) - return VT_UI8; -#else - if (CoreLibBinder::IsClass(pMT, CLASS__INTPTR)) - return VT_INT; - if (CoreLibBinder::IsClass(pMT, CLASS__UINTPTR)) - return VT_UINT; -#endif - -#ifdef FEATURE_COMINTEROP - // The wrapper types are only available when built-in COM is supported. - if (g_pConfig->IsBuiltInCOMSupported()) - { - if (CoreLibBinder::IsClass(pMT, CLASS__DISPATCH_WRAPPER)) - return VT_DISPATCH; - if (CoreLibBinder::IsClass(pMT, CLASS__UNKNOWN_WRAPPER)) - return VT_UNKNOWN; - if (CoreLibBinder::IsClass(pMT, CLASS__ERROR_WRAPPER)) - return VT_ERROR; - if (CoreLibBinder::IsClass(pMT, CLASS__CURRENCY_WRAPPER)) - return VT_CY; - if (CoreLibBinder::IsClass(pMT, CLASS__BSTR_WRAPPER)) - return VT_BSTR; - - // VariantWrappers cannot be stored in VARIANT's. - if (CoreLibBinder::IsClass(pMT, CLASS__VARIANT_WRAPPER)) - COMPlusThrow(kArgumentException, IDS_EE_COM_UNSUPPORTED_SIG); - } -#endif // FEATURE_COMINTEROP - - if (pMT->IsEnum()) - return GetVarTypeForCorElementType(type.GetInternalCorElementType()); - - if (pMT->IsValueType()) - return VT_RECORD; - - if (pMT->IsArray()) - return VT_ARRAY; - -#ifdef FEATURE_COMINTEROP - // There is no VT corresponding to SafeHandles as they cannot be stored in - // VARIANTs or Arrays. The same applies to CriticalHandle. - if (type.CanCastTo(TypeHandle(CoreLibBinder::GetClass(CLASS__SAFE_HANDLE)))) - COMPlusThrow(kArgumentException, IDS_EE_COM_UNSUPPORTED_SIG); - if (type.CanCastTo(TypeHandle(CoreLibBinder::GetClass(CLASS__CRITICAL_HANDLE)))) - COMPlusThrow(kArgumentException, IDS_EE_COM_UNSUPPORTED_SIG); - - if (pMT->IsInterface()) - { - CorIfaceAttr ifaceType = pMT->GetComInterfaceType(); - return static_cast(IsDispatchBasedItf(ifaceType) ? VT_DISPATCH : VT_UNKNOWN); - } - - TypeHandle hndDefItfClass; - DefaultInterfaceType DefItfType = GetDefaultInterfaceForClassWrapper(type, &hndDefItfClass); - switch (DefItfType) - { - case DefaultInterfaceType_Explicit: - { - CorIfaceAttr ifaceType = hndDefItfClass.GetMethodTable()->GetComInterfaceType(); - return static_cast(IsDispatchBasedItf(ifaceType) ? VT_DISPATCH : VT_UNKNOWN); - } - - case DefaultInterfaceType_AutoDual: - { - return VT_DISPATCH; - } - - case DefaultInterfaceType_IUnknown: - case DefaultInterfaceType_BaseComClass: - { - return VT_UNKNOWN; - } - - case DefaultInterfaceType_AutoDispatch: - { - return VT_DISPATCH; - } - - default: - { - _ASSERTE(!"Invalid default interface type!"); - } - } -#endif // FEATURE_COMINTEROP - - return VT_UNKNOWN; -} // // GetElementVarTypeForArrayRef returns the safearray variant type for the @@ -282,11 +39,9 @@ VARTYPE OleVariant::GetElementVarTypeForArrayRef(BASEARRAYREF pArrayRef) CONTRACTL_END; TypeHandle elemTypeHnd = pArrayRef->GetArrayElementTypeHandle(); - return(GetVarTypeForTypeHandle(elemTypeHnd)); + return(::GetVarTypeForTypeHandle(elemTypeHnd)); } -#ifdef FEATURE_COMINTEROP - BOOL OleVariant::IsValidArrayForSafeArrayElementType(BASEARRAYREF *pArrayRef, VARTYPE vtExpected) { CONTRACTL @@ -337,8 +92,6 @@ BOOL OleVariant::IsValidArrayForSafeArrayElementType(BASEARRAYREF *pArrayRef, VA } } -#endif // FEATURE_COMINTEROP - // // GetArrayClassForVarType returns the element class name and underlying method table // to use to represent an array with the given variant type. @@ -366,15 +119,9 @@ TypeHandle OleVariant::GetArrayForVarType(VARTYPE vt, TypeHandle elemType, unsig switch (vt) { case VT_BOOL: - case VTHACK_WINBOOL: - case VTHACK_CBOOL: baseElement = ELEMENT_TYPE_BOOLEAN; break; - case VTHACK_ANSICHAR: - baseElement = ELEMENT_TYPE_CHAR; - break; - case VT_UI1: baseElement = ELEMENT_TYPE_U1; break; @@ -621,26 +368,12 @@ UINT OleVariant::GetElementSizeForVarType(VARTYPE vt, MethodTable *pInterfaceMT) sizeof(LPWSTR), // VT_LPWSTR }; - // Special cases - switch (vt) - { - case VTHACK_WINBOOL: - return sizeof(BOOL); - break; - case VTHACK_ANSICHAR: - return GetMaxDBCSCharByteSize(); // Multi byte characters. - break; - case VTHACK_CBOOL: - return sizeof(BYTE); - default: - break; - } // VT_ARRAY indicates a safe array which is always sizeof(SAFEARRAY *). if (vt & VT_ARRAY) return sizeof(SAFEARRAY*); - if (vt == VTHACK_NONBLITTABLERECORD || vt == VTHACK_BLITTABLERECORD || vt == VT_RECORD) + if (vt == VT_RECORD) { _ASSERTE(pInterfaceMT != NULL); return pInterfaceMT->GetNativeSize(); @@ -652,231 +385,6 @@ UINT OleVariant::GetElementSizeForVarType(VARTYPE vt, MethodTable *pInterfaceMT) } // -// GetElementSizeForVarType returns the a MethodTable* to a type that it blittable to the native -// element representation, or pManagedMT if vt represents a record (user-defined type). -// - -MethodTable* OleVariant::GetNativeMethodTableForVarType(VARTYPE vt, MethodTable* pManagedMT) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_ANY; - } - CONTRACTL_END; - - if (vt & VT_ARRAY) - { - return CoreLibBinder::GetClass(CLASS__INTPTR); - } - - switch (vt) - { - case VT_DATE: - return CoreLibBinder::GetClass(CLASS__DOUBLE); - case VT_CY: - return CoreLibBinder::GetClass(CLASS__CURRENCY); - case VTHACK_WINBOOL: - return CoreLibBinder::GetClass(CLASS__INT32); - case VT_BOOL: - return CoreLibBinder::GetClass(CLASS__INT16); - case VTHACK_CBOOL: - return CoreLibBinder::GetClass(CLASS__BYTE); - case VT_DISPATCH: - case VT_UNKNOWN: - case VT_LPSTR: - case VT_LPWSTR: - case VT_BSTR: - case VT_USERDEFINED: - case VT_SAFEARRAY: - case VT_CARRAY: - return CoreLibBinder::GetClass(CLASS__INTPTR); - case VT_VARIANT: - return CoreLibBinder::GetClass(CLASS__COMVARIANT); - case VTHACK_ANSICHAR: - return CoreLibBinder::GetClass(CLASS__BYTE); - case VT_UI2: - // When CharSet = CharSet.Unicode, System.Char arrays are marshaled as VT_UI2. - // However, since System.Char itself is CharSet.Ansi, the native size of - // System.Char is 1 byte instead of 2. So here we explicitly return System.UInt16's - // MethodTable to ensure the correct size. - return CoreLibBinder::GetClass(CLASS__UINT16); - case VT_DECIMAL: - return CoreLibBinder::GetClass(CLASS__DECIMAL); - default: - _ASSERTE(pManagedMT != NULL); - return pManagedMT; - } -} - -// -// GetMarshalerForVarType returns the marshaler for the -// given VARTYPE. -// - -const OleVariant::Marshaler *OleVariant::GetMarshalerForVarType(VARTYPE vt, BOOL fThrow) -{ - CONTRACT (const OleVariant::Marshaler*) - { - if (fThrow) THROWS; else NOTHROW; - GC_NOTRIGGER; - MODE_ANY; - POSTCONDITION(CheckPointer(RETVAL, NULL_OK)); - } - CONTRACT_END; - -#define RETURN_MARSHALER(ArrayOleToCom, ArrayComToOle, ClearArray) \ - { static const Marshaler marshaler = { ArrayOleToCom, ArrayComToOle, ClearArray }; RETURN &marshaler; } - -#ifdef FEATURE_COMINTEROP - if (vt & VT_ARRAY) - { -VariantArray: - RETURN_MARSHALER( - NULL, - NULL, - ClearVariantArray - ); - } -#endif // FEATURE_COMINTEROP - - switch (vt) - { - case VT_BOOL: - RETURN_MARSHALER( - MarshalBoolArrayOleToCom, - MarshalBoolArrayComToOle, - NULL - ); - - case VT_DATE: - RETURN_MARSHALER( - MarshalDateArrayOleToCom, - MarshalDateArrayComToOle, - NULL - ); - -#ifdef FEATURE_COMINTEROP - case VT_CY: - RETURN_MARSHALER( - MarshalCurrencyArrayOleToCom, - MarshalCurrencyArrayComToOle, - NULL - ); - - case VT_BSTR: - RETURN_MARSHALER( - MarshalBSTRArrayOleToCom, - MarshalBSTRArrayComToOle, - ClearBSTRArray - ); - - case VT_UNKNOWN: - RETURN_MARSHALER( - MarshalInterfaceArrayOleToCom, - MarshalIUnknownArrayComToOle, - ClearInterfaceArray - ); - - case VT_DISPATCH: - RETURN_MARSHALER( - MarshalInterfaceArrayOleToCom, - MarshalIDispatchArrayComToOle, - ClearInterfaceArray - ); - - case VT_SAFEARRAY: - goto VariantArray; - - case VT_VARIANT: - RETURN_MARSHALER( - MarshalVariantArrayOleToCom, - MarshalVariantArrayComToOle, - ClearVariantArray - ); - -#endif // FEATURE_COMINTEROP - - case VTHACK_NONBLITTABLERECORD: - RETURN_MARSHALER( - MarshalNonBlittableRecordArrayOleToCom, - MarshalNonBlittableRecordArrayComToOle, - ClearNonBlittableRecordArray - ); - - case VTHACK_BLITTABLERECORD: - RETURN NULL; // Requires no marshaling - - case VTHACK_WINBOOL: - RETURN_MARSHALER( - MarshalWinBoolArrayOleToCom, - MarshalWinBoolArrayComToOle, - NULL - ); - - case VTHACK_CBOOL: - RETURN_MARSHALER( - MarshalCBoolArrayOleToCom, - MarshalCBoolArrayComToOle, - NULL - ); - - case VTHACK_ANSICHAR: - RETURN_MARSHALER( - MarshalAnsiCharArrayOleToCom, - MarshalAnsiCharArrayComToOle, - NULL - ); - - case VT_LPSTR: - RETURN_MARSHALER( - MarshalLPSTRArrayOleToCom, - MarshalLPSTRRArrayComToOle, - ClearLPSTRArray - ); - - case VT_LPWSTR: - RETURN_MARSHALER( - MarshalLPWSTRArrayOleToCom, - MarshalLPWSTRRArrayComToOle, - ClearLPWSTRArray - ); - - case VT_RECORD: -#ifdef FEATURE_COMINTEROP - RETURN_MARSHALER( - MarshalRecordArrayOleToCom, - MarshalRecordArrayComToOle, - ClearRecordArray - ); -#else - RETURN_MARSHALER( - MarshalRecordArrayOleToCom, - MarshalRecordArrayComToOle, - ClearRecordArray - ); -#endif // FEATURE_COMINTEROP - - case VT_CARRAY: - case VT_USERDEFINED: - if (fThrow) - { - COMPlusThrow(kArgumentException, IDS_EE_COM_UNSUPPORTED_SIG); - } - else - { - RETURN NULL; - } - - default: - RETURN NULL; - } -} // OleVariant::Marshaler *OleVariant::GetMarshalerForVarType() - - -#ifdef FEATURE_COMINTEROP - void SafeVariantClear(VARIANT* pVar) { CONTRACTL @@ -903,928 +411,38 @@ struct VariantEmptyHolderTraits final using Type = VARIANT*; static constexpr Type Default() { return NULL; } static void Free(Type value) - { - WRAPPER_NO_CONTRACT; - SafeVariantClear(value); - } -}; - -using VariantEmptyHolder = LifetimeHolder; - -struct RecordVariantHolderTraits final -{ - using Type = VARIANT*; - static constexpr Type Default() { return NULL; } - static void Free(Type value) - { - LIMITED_METHOD_CONTRACT; - - if (value != NULL) - { - if (V_RECORD(value)) - V_RECORDINFO(value)->RecordDestroy(V_RECORD(value)); - if (V_RECORDINFO(value)) - V_RECORDINFO(value)->Release(); - } - } -}; - -using RecordVariantHolder = LifetimeHolder; -#endif // FEATURE_COMINTEROP - -/* ------------------------------------------------------------------------- * - * Boolean marshaling routines - * ------------------------------------------------------------------------- */ - -void OleVariant::MarshalBoolArrayOleToCom(void *oleArray, BASEARRAYREF *pComArray, - MethodTable *pInterfaceMT) -{ - CONTRACTL - { - NOTHROW; - GC_NOTRIGGER; - MODE_COOPERATIVE; - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - } - CONTRACTL_END; - - ASSERT_PROTECTED(pComArray); - - SIZE_T elementCount = (*pComArray)->GetNumComponents(); - - VARIANT_BOOL *pOle = (VARIANT_BOOL *) oleArray; - VARIANT_BOOL *pOleEnd = pOle + elementCount; - - UCHAR *pCom = (UCHAR *) (*pComArray)->GetDataPtr(); - - while (pOle < pOleEnd) - { - static_assert(sizeof(VARIANT_BOOL) == sizeof(UINT16)); - (*(pCom++)) = MAYBE_UNALIGNED_READ(pOle, 16) ? 1 : 0; - pOle++; - } -} - -void OleVariant::MarshalBoolArrayComToOle(BASEARRAYREF *pComArray, void *oleArray, - MethodTable *pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, - BOOL fOleArrayIsValid, SIZE_T cElements) -{ - CONTRACTL - { - NOTHROW; - GC_NOTRIGGER; - MODE_COOPERATIVE; - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - } - CONTRACTL_END; - - ASSERT_PROTECTED(pComArray); - - VARIANT_BOOL *pOle = (VARIANT_BOOL *) oleArray; - VARIANT_BOOL *pOleEnd = pOle + cElements; - - UCHAR *pCom = (UCHAR *) (*pComArray)->GetDataPtr(); - - while (pOle < pOleEnd) - { - static_assert(sizeof(VARIANT_BOOL) == sizeof(UINT16)); - MAYBE_UNALIGNED_WRITE(pOle, 16, *pCom ? VARIANT_TRUE : VARIANT_FALSE); - pOle++; pCom++; - } -} - -/* ------------------------------------------------------------------------- * - * WinBoolean marshaling routines - * ------------------------------------------------------------------------- */ - -void OleVariant::MarshalWinBoolArrayOleToCom(void *oleArray, BASEARRAYREF *pComArray, - MethodTable *pInterfaceMT) -{ - CONTRACTL - { - NOTHROW; - GC_NOTRIGGER; - MODE_COOPERATIVE; - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - } - CONTRACTL_END; - - ASSERT_PROTECTED(pComArray); - - SIZE_T elementCount = (*pComArray)->GetNumComponents(); - - BOOL *pOle = (BOOL *) oleArray; - BOOL *pOleEnd = pOle + elementCount; - - UCHAR *pCom = (UCHAR *) (*pComArray)->GetDataPtr(); - - while (pOle < pOleEnd) - { - static_assert(sizeof(BOOL) == sizeof(UINT32)); - (*(pCom++)) = MAYBE_UNALIGNED_READ(pOle, 32) ? 1 : 0; - pOle++; - } -} - -void OleVariant::MarshalWinBoolArrayComToOle(BASEARRAYREF *pComArray, void *oleArray, - MethodTable *pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, - BOOL fOleArrayIsValid, SIZE_T cElements) -{ - CONTRACTL - { - NOTHROW; - GC_NOTRIGGER; - MODE_COOPERATIVE; - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - } - CONTRACTL_END; - - ASSERT_PROTECTED(pComArray); - - BOOL *pOle = (BOOL *) oleArray; - BOOL *pOleEnd = pOle + cElements; - - UCHAR *pCom = (UCHAR *) (*pComArray)->GetDataPtr(); - - while (pOle < pOleEnd) - { - static_assert(sizeof(BOOL) == sizeof(UINT32)); - MAYBE_UNALIGNED_WRITE(pOle, 32, *pCom ? 1 : 0); - pOle++; pCom++; - } -} - -/* ------------------------------------------------------------------------- * - * CBool marshaling routines - * ------------------------------------------------------------------------- */ - -void OleVariant::MarshalCBoolArrayOleToCom(void* oleArray, BASEARRAYREF* pComArray, - MethodTable* pInterfaceMT) -{ - LIMITED_METHOD_CONTRACT; - - CONTRACTL - { - NOTHROW; - GC_NOTRIGGER; - MODE_COOPERATIVE; - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - } - CONTRACTL_END; - - ASSERT_PROTECTED(pComArray); - - _ASSERTE((*pComArray)->GetArrayElementType() == ELEMENT_TYPE_BOOLEAN); - - SIZE_T cbArray = (*pComArray)->GetNumComponents(); - - BYTE *pOle = (BYTE *) oleArray; - BYTE *pOleEnd = pOle + cbArray; - - UCHAR *pCom = (UCHAR *) (*pComArray)->GetDataPtr(); - - while (pOle < pOleEnd) - { - (*pCom) = (*pOle ? 1 : 0); - pOle++; pCom++; - } -} - -void OleVariant::MarshalCBoolArrayComToOle(BASEARRAYREF* pComArray, void* oleArray, - MethodTable* pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, BOOL fOleArrayIsValid, - SIZE_T cElements) -{ - LIMITED_METHOD_CONTRACT; - - CONTRACTL - { - NOTHROW; - GC_NOTRIGGER; - MODE_COOPERATIVE; - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - } - CONTRACTL_END; - - ASSERT_PROTECTED(pComArray); - - _ASSERTE((*pComArray)->GetArrayElementType() == ELEMENT_TYPE_BOOLEAN); - - BYTE *pOle = (BYTE *) oleArray; - BYTE *pOleEnd = pOle + cElements; - - UCHAR *pCom = (UCHAR *) (*pComArray)->GetDataPtr(); - - while (pOle < pOleEnd) - { - *pOle = (*pCom ? 1 : 0); - pOle++; pCom++; - } -} - -/* ------------------------------------------------------------------------- * - * Ansi char marshaling routines - * ------------------------------------------------------------------------- */ - -void OleVariant::MarshalAnsiCharArrayOleToCom(void *oleArray, BASEARRAYREF *pComArray, - MethodTable *pInterfaceMT) -{ - CONTRACTL - { - THROWS; - GC_NOTRIGGER; - MODE_COOPERATIVE; - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - } - CONTRACTL_END; - - ASSERT_PROTECTED(pComArray); - - SIZE_T elementCount = (*pComArray)->GetNumComponents(); - - WCHAR *pCom = (WCHAR *) (*pComArray)->GetDataPtr(); - - if (0 == elementCount) - { - *pCom = '\0'; - return; - } - - if (0 == MultiByteToWideChar(CP_ACP, - MB_PRECOMPOSED, - (const CHAR *)oleArray, - (int)elementCount, - pCom, - (int)elementCount)) - { - COMPlusThrowWin32(); - } -} - -void OleVariant::MarshalAnsiCharArrayComToOle(BASEARRAYREF *pComArray, void *oleArray, - MethodTable *pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, BOOL fOleArrayIsValid, - SIZE_T cElements) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_COOPERATIVE; - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - } - CONTRACTL_END; - - const WCHAR *pCom = (const WCHAR *) (*pComArray)->GetDataPtr(); - - if (!FitsIn(cElements)) - COMPlusThrowHR(COR_E_OVERFLOW); - - int cchCount = (int)cElements; - int cbBuffer; - - if (!ClrSafeInt::multiply(cchCount, GetMaxDBCSCharByteSize(), cbBuffer)) - COMPlusThrowHR(COR_E_OVERFLOW); - - InternalWideToAnsi((WCHAR*)pCom, cchCount, (CHAR*)oleArray, cbBuffer, - fBestFitMapping, fThrowOnUnmappableChar); -} - -/* ------------------------------------------------------------------------- * - * Interface marshaling routines - * ------------------------------------------------------------------------- */ - -#ifdef FEATURE_COMINTEROP -void OleVariant::MarshalInterfaceArrayOleToCom(void *oleArray, BASEARRAYREF *pComArray, - MethodTable *pElementMT) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_COOPERATIVE; - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - } - CONTRACTL_END; - - ASSERT_PROTECTED(pComArray); - - SIZE_T elementCount = (*pComArray)->GetNumComponents(); - - IUnknown **pOle = (IUnknown **) oleArray; - IUnknown **pOleEnd = pOle + elementCount; - - OBJECTREF *pCom = (OBJECTREF *) (*pComArray)->GetDataPtr(); - - OBJECTREF obj = NULL; - GCPROTECT_BEGIN(obj) - { - GCPROTECT_BEGININTERIOR(pCom) - { - while (pOle < pOleEnd) - { - IUnknown *unk = *pOle++; - - if (unk == NULL) - obj = NULL; - else - GetObjectRefFromComIP(&obj, unk); - - // - // Make sure the object can be cast to the destination type. - // - - if (pElementMT != NULL && !CanCastComObject(obj, pElementMT)) - { - StackSString ssObjClsName; - StackSString ssDestClsName; - obj->GetMethodTable()->_GetFullyQualifiedNameForClass(ssObjClsName); - pElementMT->_GetFullyQualifiedNameForClass(ssDestClsName); - COMPlusThrow(kInvalidCastException, IDS_EE_CANNOTCAST, - ssObjClsName.GetUnicode(), ssDestClsName.GetUnicode()); - } - - SetObjectReference(pCom++, obj); - } - } - GCPROTECT_END(); - } - GCPROTECT_END(); -} - -void OleVariant::MarshalIUnknownArrayComToOle(BASEARRAYREF *pComArray, void *oleArray, - MethodTable *pElementMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, - BOOL fOleArrayIsValid, SIZE_T cElements) -{ - WRAPPER_NO_CONTRACT; - - MarshalInterfaceArrayComToOleHelper(pComArray, oleArray, pElementMT, FALSE, cElements); -} - -void OleVariant::ClearInterfaceArray(void *oleArray, SIZE_T cElements, MethodTable *pInterfaceMT) -{ - CONTRACTL - { - NOTHROW; - GC_TRIGGERS; - MODE_ANY; - PRECONDITION(CheckPointer(oleArray)); - } - CONTRACTL_END; - - IUnknown **pOle = (IUnknown **) oleArray; - IUnknown **pOleEnd = pOle + cElements; - - GCX_PREEMP(); - while (pOle < pOleEnd) - { - IUnknown *pUnk = *pOle++; - - if (pUnk != NULL) - { - ULONG cbRef = SafeReleasePreemp(pUnk); - LogInteropRelease(pUnk, cbRef, "VariantClearInterfacArray"); - } - } -} - - -/* ------------------------------------------------------------------------- * - * BSTR marshaling routines - * ------------------------------------------------------------------------- */ - -void OleVariant::MarshalBSTRArrayOleToCom(void *oleArray, BASEARRAYREF *pComArray, - MethodTable *pInterfaceMT) -{ - CONTRACTL - { - WRAPPER(THROWS); - WRAPPER(GC_TRIGGERS); - MODE_COOPERATIVE; - INJECT_FAULT(COMPlusThrowOM()); - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - } - CONTRACTL_END; - - STRINGREF stringObj = NULL; - GCPROTECT_BEGIN(stringObj) - { - ASSERT_PROTECTED(pComArray); - SIZE_T elementCount = (*pComArray)->GetNumComponents(); - - BSTR *pOle = (BSTR *) oleArray; - BSTR *pOleEnd = pOle + elementCount; - - STRINGREF *pCom = (STRINGREF *) (*pComArray)->GetDataPtr(); - GCPROTECT_BEGININTERIOR(pCom) - { - while (pOle < pOleEnd) - { - BSTR bstr = *pOle++; - - ConvertBSTRToString(bstr, &stringObj); - - SetObjectReference((OBJECTREF*) pCom++, (OBJECTREF) stringObj); - } - } - GCPROTECT_END(); - } - GCPROTECT_END(); -} - -void OleVariant::MarshalBSTRArrayComToOle(BASEARRAYREF *pComArray, void *oleArray, - MethodTable *pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, - BOOL fOleArrayIsValid, SIZE_T cElements) -{ - CONTRACTL - { - THROWS; - WRAPPER(GC_TRIGGERS); - MODE_COOPERATIVE; - INJECT_FAULT(COMPlusThrowOM()); - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - } - CONTRACTL_END; - - STRINGREF stringObj = NULL; - GCPROTECT_BEGIN(stringObj) - { - ASSERT_PROTECTED(pComArray); - - BSTR *pOle = (BSTR *) oleArray; - BSTR *pOleEnd = pOle + cElements; - - STRINGREF *pCom = (STRINGREF *) (*pComArray)->GetDataPtr(); - GCPROTECT_BEGININTERIOR(pCom) - { - while (pOle < pOleEnd) - { - stringObj = *pCom++; - BSTR bstr = ConvertStringToBSTR(&stringObj); - *pOle++ = bstr; - } - } - GCPROTECT_END(); - } - GCPROTECT_END(); -} - -void OleVariant::ClearBSTRArray(void *oleArray, SIZE_T cElements, MethodTable *pInterfaceMT) -{ - CONTRACTL - { - NOTHROW; - GC_TRIGGERS; - MODE_ANY; - PRECONDITION(CheckPointer(oleArray)); - } - CONTRACTL_END; - - BSTR *pOle = (BSTR *) oleArray; - BSTR *pOleEnd = pOle + cElements; - - while (pOle < pOleEnd) - { - BSTR bstr = *pOle++; - - if (bstr != NULL) - SysFreeString(bstr); - } -} -#endif // FEATURE_COMINTEROP - - - -/* ------------------------------------------------------------------------- * - * Structure marshaling routines - * ------------------------------------------------------------------------- */ -void OleVariant::MarshalNonBlittableRecordArrayOleToCom(void *oleArray, BASEARRAYREF *pComArray, - MethodTable *pInterfaceMT) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_COOPERATIVE; - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - PRECONDITION(CheckPointer(pInterfaceMT)); - } - CONTRACTL_END; - - ASSERT_PROTECTED(pComArray); - - UnmanagedCallersOnlyCaller convertToManaged(METHOD__STUBHELPERS__NONBLITTABLE_STRUCTURE_ARRAY_CONVERT_TO_MANAGED); - convertToManaged.InvokeThrowing(pComArray, oleArray, pInterfaceMT, pInterfaceMT->GetNativeSize()); -} - -void OleVariant::MarshalNonBlittableRecordArrayComToOle(BASEARRAYREF *pComArray, void *oleArray, - MethodTable *pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, - BOOL fOleArrayIsValid, SIZE_T cElements) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_COOPERATIVE; - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - PRECONDITION(CheckPointer(pInterfaceMT)); - } - CONTRACTL_END; - - ASSERT_PROTECTED(pComArray); - - SIZE_T elemSize = pInterfaceMT->GetNativeSize(); - - BYTE *pOle = (BYTE *) oleArray; - BYTE *pOleEnd = pOle + elemSize * cElements; - - if (!fOleArrayIsValid) - { - // field marshalers assume that the native structure is valid - FillMemory(pOle, pOleEnd - pOle, 0); - } - - UnmanagedCallersOnlyCaller convertToUnmanaged(METHOD__STUBHELPERS__NONBLITTABLE_STRUCTURE_ARRAY_CONVERT_TO_UNMANAGED); - convertToUnmanaged.InvokeThrowing(pComArray, oleArray, pInterfaceMT, pInterfaceMT->GetNativeSize()); -} - -void OleVariant::ClearNonBlittableRecordArray(void *oleArray, SIZE_T cElements, MethodTable *pInterfaceMT) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_ANY; - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pInterfaceMT)); - } - CONTRACTL_END; - - UnmanagedCallersOnlyCaller free(METHOD__STUBHELPERS__NONBLITTABLE_STRUCTURE_ARRAY_FREE); - free.InvokeThrowing(oleArray, cElements, pInterfaceMT, pInterfaceMT->GetNativeSize()); -} - - -/* ------------------------------------------------------------------------- * - * LPWSTR marshaling routines - * ------------------------------------------------------------------------- */ - -void OleVariant::MarshalLPWSTRArrayOleToCom(void *oleArray, BASEARRAYREF *pComArray, - MethodTable *pInterfaceMT) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_COOPERATIVE; - INJECT_FAULT(COMPlusThrowOM()); - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - } - CONTRACTL_END; - - ASSERT_PROTECTED(pComArray); - SIZE_T elementCount = (*pComArray)->GetNumComponents(); - - LPWSTR *pOle = (LPWSTR *) oleArray; - LPWSTR *pOleEnd = pOle + elementCount; - - STRINGREF *pCom = (STRINGREF *) (*pComArray)->GetDataPtr(); - GCPROTECT_BEGININTERIOR(pCom) - { - while (pOle < pOleEnd) - { - LPWSTR lpwstr = *pOle++; - - STRINGREF string; - if (lpwstr == NULL) - string = NULL; - else - string = StringObject::NewString(lpwstr); - - SetObjectReference((OBJECTREF*) pCom++, (OBJECTREF) string); - } - } - GCPROTECT_END(); -} - -void OleVariant::MarshalLPWSTRRArrayComToOle(BASEARRAYREF *pComArray, void *oleArray, - MethodTable *pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, - BOOL fOleArrayIsValid, SIZE_T cElements) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_COOPERATIVE; - INJECT_FAULT(COMPlusThrowOM()); - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - } - CONTRACTL_END; - - LPWSTR *pOle = (LPWSTR *) oleArray; - LPWSTR *pOleEnd = pOle + cElements; - - struct - { - BASEARRAYREF pCom; - STRINGREF stringRef; - } gc; - gc.pCom = *pComArray; - gc.stringRef = NULL; - GCPROTECT_BEGIN(gc) - { - - int i = 0; - while (pOle < pOleEnd) - { - gc.stringRef = *((STRINGREF*)gc.pCom->GetDataPtr() + i); - - LPWSTR lpwstr; - if (gc.stringRef == NULL) - { - lpwstr = NULL; - } - else - { - // Retrieve the length of the string. - int Length = gc.stringRef->GetStringLength(); - int allocLength = (Length + 1) * sizeof(WCHAR); - if (allocLength < Length) - ThrowOutOfMemory(); - - // Allocate the string using CoTaskMemAlloc. - { - GCX_PREEMP(); - lpwstr = (LPWSTR)CoTaskMemAlloc(allocLength); - } - if (lpwstr == NULL) - ThrowOutOfMemory(); - - // Copy the COM+ string into the newly allocated LPWSTR. - memcpyNoGCRefs(lpwstr, gc.stringRef->GetBuffer(), allocLength); - lpwstr[Length] = W('\0'); - } - - *pOle++ = lpwstr; - i++; - } - } - GCPROTECT_END(); -} - -void OleVariant::ClearLPWSTRArray(void *oleArray, SIZE_T cElements, MethodTable *pInterfaceMT) -{ - CONTRACTL - { - NOTHROW; - GC_TRIGGERS; - MODE_ANY; - PRECONDITION(CheckPointer(oleArray)); - } - CONTRACTL_END; - - GCX_PREEMP(); - LPWSTR *pOle = (LPWSTR *) oleArray; - LPWSTR *pOleEnd = pOle + cElements; - - PERMANENT_CONTRACT_VIOLATION(ThrowsViolation, ReasonRuntimeReentrancy); // IMallocSpy in managed - while (pOle < pOleEnd) - { - LPWSTR lpwstr = *pOle++; - - if (lpwstr != NULL) - CoTaskMemFree(lpwstr); - } -} - -/* ------------------------------------------------------------------------- * - * LPWSTR marshaling routines - * ------------------------------------------------------------------------- */ - -void OleVariant::MarshalLPSTRArrayOleToCom(void *oleArray, BASEARRAYREF *pComArray, - MethodTable *pInterfaceMT) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_COOPERATIVE; - INJECT_FAULT(COMPlusThrowOM()); - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - } - CONTRACTL_END; - - ASSERT_PROTECTED(pComArray); - SIZE_T elementCount = (*pComArray)->GetNumComponents(); - - LPSTR *pOle = (LPSTR *) oleArray; - LPSTR *pOleEnd = pOle + elementCount; - - STRINGREF *pCom = (STRINGREF *) (*pComArray)->GetDataPtr(); - GCPROTECT_BEGININTERIOR(pCom) - { - while (pOle < pOleEnd) - { - LPSTR lpstr = *pOle++; - - STRINGREF string; - if (lpstr == NULL) - string = NULL; - else - string = StringObject::NewString(lpstr); - - SetObjectReference((OBJECTREF*) pCom++, (OBJECTREF) string); - } - } - GCPROTECT_END(); -} - -void OleVariant::MarshalLPSTRRArrayComToOle(BASEARRAYREF *pComArray, void *oleArray, - MethodTable *pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, - BOOL fOleArrayIsValid, SIZE_T cElements) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_COOPERATIVE; - INJECT_FAULT(COMPlusThrowOM()); - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - } - CONTRACTL_END; - - LPSTR *pOle = (LPSTR *) oleArray; - LPSTR *pOleEnd = pOle + cElements; - - struct - { - BASEARRAYREF pCom; - STRINGREF stringRef; - } gc; - gc.pCom = *pComArray; - gc.stringRef = NULL; - GCPROTECT_BEGIN(gc) - { - int i = 0; - while (pOle < pOleEnd) - { - gc.stringRef = *((STRINGREF*)gc.pCom->GetDataPtr() + i); - - CoTaskMemHolder lpstr(NULL); - if (gc.stringRef == NULL) - { - lpstr = NULL; - } - else - { - // Retrieve the length of the string. - int Length = gc.stringRef->GetStringLength(); - int allocLength = Length * GetMaxDBCSCharByteSize() + 1; - if (allocLength < Length) - ThrowOutOfMemory(); - - // Allocate the string using CoTaskMemAlloc. - { - GCX_PREEMP(); - lpstr = (LPSTR)CoTaskMemAlloc(allocLength); - } - if (lpstr == NULL) - ThrowOutOfMemory(); - - // Convert the unicode string to an ansi string. - int bytesWritten = InternalWideToAnsi(gc.stringRef->GetBuffer(), Length, lpstr, allocLength, fBestFitMapping, fThrowOnUnmappableChar); - _ASSERTE(bytesWritten >= 0 && bytesWritten < allocLength); - lpstr[bytesWritten] = '\0'; - } - - *pOle++ = lpstr; - i++; - lpstr.SuppressRelease(); - } - } - GCPROTECT_END(); -} - -void OleVariant::ClearLPSTRArray(void *oleArray, SIZE_T cElements, MethodTable *pInterfaceMT) -{ - CONTRACTL - { - NOTHROW; - GC_TRIGGERS; - MODE_ANY; - PRECONDITION(CheckPointer(oleArray)); - } - CONTRACTL_END; - - GCX_PREEMP(); - LPSTR *pOle = (LPSTR *) oleArray; - LPSTR *pOleEnd = pOle + cElements; - - PERMANENT_CONTRACT_VIOLATION(ThrowsViolation, ReasonRuntimeReentrancy); // IMallocSpy in managed - while (pOle < pOleEnd) - { - LPSTR lpstr = *pOle++; - - if (lpstr != NULL) - CoTaskMemFree(lpstr); - } -} - -/* ------------------------------------------------------------------------- * - * Date marshaling routines - * ------------------------------------------------------------------------- */ - -void OleVariant::MarshalDateArrayOleToCom(void *oleArray, BASEARRAYREF *pComArray, - MethodTable *pInterfaceMT) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_COOPERATIVE; - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - } - CONTRACTL_END; - - ASSERT_PROTECTED(pComArray); - - SIZE_T elementCount = (*pComArray)->GetNumComponents(); - - DATE *pOle = (DATE *) oleArray; - DATE *pOleEnd = pOle + elementCount; - - INT64 *pCom = (INT64 *) (*pComArray)->GetDataPtr(); - - // - // We aren't calling anything which might cause a GC, so don't worry about - // the array moving here. - // - - while (pOle < pOleEnd) - *pCom++ = COMDateTime::DoubleDateToTicks(*pOle++); -} - -void OleVariant::MarshalDateArrayComToOle(BASEARRAYREF *pComArray, void *oleArray, - MethodTable *pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, - BOOL fOleArrayIsValid, SIZE_T cElements) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_COOPERATIVE; - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); + { + WRAPPER_NO_CONTRACT; + SafeVariantClear(value); } - CONTRACTL_END; - - ASSERT_PROTECTED(pComArray); +}; - DATE *pOle = (DATE *) oleArray; - DATE *pOleEnd = pOle + cElements; +using VariantEmptyHolder = LifetimeHolder; - INT64 *pCom = (INT64 *) (*pComArray)->GetDataPtr(); +struct RecordVariantHolderTraits final +{ + using Type = VARIANT*; + static constexpr Type Default() { return NULL; } + static void Free(Type value) + { + LIMITED_METHOD_CONTRACT; - // - // We aren't calling anything which might cause a GC, so don't worry about - // the array moving here. - // + if (value != NULL) + { + if (V_RECORD(value)) + V_RECORDINFO(value)->RecordDestroy(V_RECORD(value)); + if (V_RECORDINFO(value)) + V_RECORDINFO(value)->Release(); + } + } +}; - while (pOle < pOleEnd) - *pOle++ = COMDateTime::TicksToDoubleDate(*pCom++); -} +using RecordVariantHolder = LifetimeHolder; /* ------------------------------------------------------------------------- * * Record marshaling routines * ------------------------------------------------------------------------- */ -#ifdef FEATURE_COMINTEROP void OleVariant::MarshalRecordVariantOleToObject(const VARIANT *pOleVariant, OBJECTREF * const & pObj) { @@ -1877,90 +495,6 @@ void OleVariant::MarshalRecordVariantOleToObject(const VARIANT *pOleVariant, } GCPROTECT_END(); } -#endif // FEATURE_COMINTEROP - -void OleVariant::MarshalRecordArrayOleToCom(void *oleArray, BASEARRAYREF *pComArray, - MethodTable *pElementMT) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_COOPERATIVE; - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - PRECONDITION(CheckPointer(pElementMT)); - } - CONTRACTL_END; - - if (pElementMT->IsBlittable()) - { - // The array is blittable so we can simply copy it. - _ASSERTE(pComArray); - SIZE_T elementCount = (*pComArray)->GetNumComponents(); - SIZE_T elemSize = pElementMT->GetNativeSize(); - memcpyNoGCRefs((*pComArray)->GetDataPtr(), oleArray, elementCount * elemSize); - } - else - { - // The array is non blittable so we need to marshal the elements. - _ASSERTE(pElementMT->HasLayout()); - MarshalNonBlittableRecordArrayOleToCom(oleArray, pComArray, pElementMT); - } -} - -void OleVariant::MarshalRecordArrayComToOle(BASEARRAYREF *pComArray, void *oleArray, - MethodTable *pElementMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, - BOOL fOleArrayIsValid, SIZE_T cElements) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_COOPERATIVE; - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - PRECONDITION(CheckPointer(pElementMT)); - } - CONTRACTL_END; - - if (pElementMT->IsBlittable()) - { - // The array is blittable so we can simply copy it. - _ASSERTE(pComArray); - SIZE_T elemSize = pElementMT->GetNativeSize(); - memcpyNoGCRefs(oleArray, (*pComArray)->GetDataPtr(), cElements * elemSize); - } - else - { - // The array is non blittable so we need to marshal the elements. - _ASSERTE(pElementMT->HasLayout()); - MarshalNonBlittableRecordArrayComToOle(pComArray, oleArray, pElementMT, fBestFitMapping, fThrowOnUnmappableChar, fOleArrayIsValid, cElements); - } -} - - -void OleVariant::ClearRecordArray(void *oleArray, SIZE_T cElements, MethodTable *pElementMT) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_ANY; - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pElementMT)); - } - CONTRACTL_END; - - if (!pElementMT->IsBlittable()) - { - _ASSERTE(pElementMT->HasLayout()); - ClearNonBlittableRecordArray(oleArray, cElements, pElementMT); - } -} - -#ifdef FEATURE_COMINTEROP // Warning! VariantClear's previous contents of pVarOut. void OleVariant::MarshalOleVariantForObject(OBJECTREF * const & pObj, VARIANT *pOle) @@ -2754,90 +1288,6 @@ void OleVariant::MarshalOleVariantForObjectUncommon(OBJECTREF * const & pObj, VA veh.Detach(); } -void OleVariant::MarshalInterfaceArrayComToOleHelper(BASEARRAYREF *pComArray, void *oleArray, - MethodTable *pElementMT, BOOL bDefaultIsDispatch, - SIZE_T cElements) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_COOPERATIVE; - PRECONDITION(CheckPointer(pComArray)); - PRECONDITION(CheckPointer(oleArray)); - } - CONTRACTL_END; - - ASSERT_PROTECTED(pComArray); - - - BOOL bDispatch = bDefaultIsDispatch; - BOOL bHeterogenous = (pElementMT == NULL); - - // If the method table is for Object then don't consider it. - if (pElementMT == g_pObjectClass) - pElementMT = NULL; - - // If the element MT represents a class, then we need to determine the default - // interface to use to expose the object out to COM. - if (pElementMT && !pElementMT->IsInterface()) - { - pElementMT = GetDefaultInterfaceMTForClass(pElementMT, &bDispatch); - } - - // Determine the start and the end of the data in the OLE array. - IUnknown **pOle = (IUnknown **) oleArray; - IUnknown **pOleEnd = pOle + cElements; - - // Retrieve the start of the data in the managed array. - OBJECTREF *pCom = (OBJECTREF *) (*pComArray)->GetDataPtr(); - - OBJECTREF TmpObj = NULL; - GCPROTECT_BEGIN(TmpObj) - { - GCPROTECT_BEGININTERIOR(pCom) - { - MethodTable *pLastElementMT = NULL; - - while (pOle < pOleEnd) - { - TmpObj = *pCom++; - - IUnknown *unk; - if (TmpObj == NULL) - unk = NULL; - else - { - if (bHeterogenous) - { - // Inspect the type of each element separately (cache the last type for perf). - if (TmpObj->GetMethodTable() != pLastElementMT) - { - pLastElementMT = TmpObj->GetMethodTable(); - pElementMT = GetDefaultInterfaceMTForClass(pLastElementMT, &bDispatch); - } - } - - if (pElementMT) - { - // Convert to COM IP based on an interface MT (a specific interface will be exposed). - unk = GetComIPFromObjectRef(&TmpObj, pElementMT); - } - else - { - // Convert to COM IP exposing either IDispatch or IUnknown. - unk = GetComIPFromObjectRef(&TmpObj, (bDispatch ? ComIpType_Dispatch : ComIpType_Unknown), NULL); - } - } - - *pOle++ = unk; - } - } - GCPROTECT_END(); - } - GCPROTECT_END(); -} - // Used by customer checked build to test validity of VARIANT BOOL OleVariant::CheckVariant(VARIANT* pOle) @@ -2869,284 +1319,115 @@ BOOL OleVariant::CheckVariant(VARIANT* pOle) EX_CATCH { } - EX_END_CATCH - - return bValidVariant; -} - -HRESULT OleVariant::ClearAndInsertContentsIntoByrefRecordVariant(VARIANT* pOle, OBJECTREF* pObj) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_ANY; - } - CONTRACTL_END; - - if (V_VT(pOle) != (VT_BYREF | VT_RECORD)) - return DISP_E_BADVARTYPE; - - // Clear the current contents of the record. - { - GCX_PREEMP(); - V_RECORDINFO(pOle)->RecordClear(V_RECORD(pOle)); - } - - // Ok - let's marshal the returning object into a VT_RECORD. - if ((*pObj) != NULL) - { - VARIANT vtmp; - SafeVariantInit(&vtmp); - - MarshalOleVariantForObject(pObj, &vtmp); - - { - GCX_PREEMP(); - - // Verify that we have a VT_RECORD. - if (V_VT(&vtmp) != VT_RECORD) - { - SafeVariantClear(&vtmp); - return DISP_E_TYPEMISMATCH; - } - - // Verify that we have the same type of record. - if (! V_RECORDINFO(pOle)->IsMatchingType(V_RECORDINFO(&vtmp))) - { - SafeVariantClear(&vtmp); - return DISP_E_TYPEMISMATCH; - } - - // Now copy the contents of the new variant back into the old variant. - HRESULT hr = V_RECORDINFO(pOle)->RecordCopy(V_RECORD(&vtmp), V_RECORD(pOle)); - if (hr != S_OK) - { - SafeVariantClear(&vtmp); - return DISP_E_TYPEMISMATCH; - } - } - } - return S_OK; -} - -void OleVariant::MarshalIDispatchArrayComToOle(BASEARRAYREF *pComArray, void *oleArray, - MethodTable *pElementMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, BOOL fOleArrayIsValid, - SIZE_T cElements) -{ - WRAPPER_NO_CONTRACT; - - MarshalInterfaceArrayComToOleHelper(pComArray, oleArray, pElementMT, TRUE, cElements); -} - - -/* ------------------------------------------------------------------------- * - * Currency marshaling routines - * ------------------------------------------------------------------------- */ - -void OleVariant::MarshalCurrencyArrayOleToCom(void *oleArray, BASEARRAYREF *pComArray, - MethodTable *pInterfaceMT) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_COOPERATIVE; - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - } - CONTRACTL_END; - - ASSERT_PROTECTED(pComArray); - SIZE_T elementCount = (*pComArray)->GetNumComponents(); - - CURRENCY *pOle = (CURRENCY *) oleArray; - CURRENCY *pOleEnd = pOle + elementCount; - - DECIMAL *pCom = (DECIMAL *) (*pComArray)->GetDataPtr(); - - while (pOle < pOleEnd) - { - VarDecFromCyCanonicalize(*pOle++, pCom++); - } -} - -void OleVariant::MarshalCurrencyArrayComToOle(BASEARRAYREF *pComArray, void *oleArray, - MethodTable *pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, - BOOL fOleArrayIsValid, SIZE_T cElements) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_COOPERATIVE; - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - } - CONTRACTL_END; - - ASSERT_PROTECTED(pComArray); - - CURRENCY *pOle = (CURRENCY *) oleArray; - CURRENCY *pOleEnd = pOle + cElements; - - DECIMAL *pCom = (DECIMAL *) (*pComArray)->GetDataPtr(); - - while (pOle < pOleEnd) - IfFailThrow(VarCyFromDec(pCom++, pOle++)); -} - - -/* ------------------------------------------------------------------------- * - * Variant marshaling routines - * ------------------------------------------------------------------------- */ - -void OleVariant::MarshalVariantArrayOleToCom(void *oleArray, BASEARRAYREF *pComArray, - MethodTable *pInterfaceMT) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_COOPERATIVE; - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - } - CONTRACTL_END; - - ASSERT_PROTECTED(pComArray); - - SIZE_T elementCount = (*pComArray)->GetNumComponents(); - - VARIANT *pOle = (VARIANT *) oleArray; - VARIANT *pOleEnd = pOle + elementCount; - - OBJECTREF *pCom = (OBJECTREF *) (*pComArray)->GetDataPtr(); - - OBJECTREF TmpObj = NULL; - GCPROTECT_BEGIN(TmpObj) - { - GCPROTECT_BEGININTERIOR(pCom) - { - while (pOle < pOleEnd) - { - // Marshal the OLE variant into a temp managed variant. - MarshalObjectForOleVariant(pOle++, &TmpObj); - - SetObjectReference(pCom++, TmpObj); - } - } - GCPROTECT_END(); - } - GCPROTECT_END(); -} - -void OleVariant::MarshalVariantArrayComToOle(BASEARRAYREF *pComArray, void *oleArray, - MethodTable *pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, - BOOL fOleArrayIsValid, SIZE_T cElements) -{ - CONTRACTL - { - THROWS; - GC_TRIGGERS; - MODE_COOPERATIVE; - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); - } - CONTRACTL_END; - + EX_END_CATCH - MarshalVariantArrayComToOle(pComArray, oleArray, pInterfaceMT, fBestFitMapping, fThrowOnUnmappableChar, FALSE, fOleArrayIsValid); + return bValidVariant; } - -void OleVariant::MarshalVariantArrayComToOle(BASEARRAYREF *pComArray, void *oleArray, - MethodTable *pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, BOOL fMarshalByrefArgOnly, - BOOL fOleArrayIsValid, int nOleArrayStepLength) +HRESULT OleVariant::ClearAndInsertContentsIntoByrefRecordVariant(VARIANT* pOle, OBJECTREF* pObj) { CONTRACTL { THROWS; GC_TRIGGERS; - MODE_COOPERATIVE; - PRECONDITION(CheckPointer(oleArray)); - PRECONDITION(CheckPointer(pComArray)); + MODE_ANY; } CONTRACTL_END; - ASSERT_PROTECTED(pComArray); + if (V_VT(pOle) != (VT_BYREF | VT_RECORD)) + return DISP_E_BADVARTYPE; - SIZE_T elementCount = (*pComArray)->GetNumComponents(); + // Clear the current contents of the record. + { + GCX_PREEMP(); + V_RECORDINFO(pOle)->RecordClear(V_RECORD(pOle)); + } - VARIANT *pOle = (VARIANT *) oleArray; - VARIANT *pOleEnd = pOle + elementCount * nOleArrayStepLength; + // Ok - let's marshal the returning object into a VT_RECORD. + if ((*pObj) != NULL) + { + VARIANT vtmp; + SafeVariantInit(&vtmp); - OBJECTREF *pCom = (OBJECTREF *) (*pComArray)->GetDataPtr(); + MarshalOleVariantForObject(pObj, &vtmp); - OBJECTREF TmpObj = NULL; - GCPROTECT_BEGIN(TmpObj) - { - GCPROTECT_BEGININTERIOR(pCom) { - while (pOle != pOleEnd) + GCX_PREEMP(); + + // Verify that we have a VT_RECORD. + if (V_VT(&vtmp) != VT_RECORD) { - TmpObj = *pCom++; + SafeVariantClear(&vtmp); + return DISP_E_TYPEMISMATCH; + } - // Marshal the temp managed variant into the OLE variant. - if (fOleArrayIsValid) - { - // We firstly try MarshalCommonOleRefVariantForObject for VT_BYREF variant because - // MarshalOleVariantForObject() VariantClear the variant and does not keep the VT_BYREF. - // For back compating the old behavior(we used MarshalOleVariantForObject in the previous - // version) that casts the managed object to Variant based on the object's MethodTable, - // MarshalCommonOleRefVariantForObject is used instead of MarshalOleRefVariantForObject so - // that cast will not be done based on the VT of the variant. - if (!((pOle->vt & VT_BYREF) && - SUCCEEDED(MarshalCommonOleRefVariantForObject(&TmpObj, pOle)))) - if (pOle->vt & VT_BYREF || !fMarshalByrefArgOnly) - MarshalOleVariantForObject(&TmpObj, pOle); - } - else - { - // The contents of pOle is undefined, don't try to handle byrefs. - MarshalOleVariantForObject(&TmpObj, pOle); - } + // Verify that we have the same type of record. + if (! V_RECORDINFO(pOle)->IsMatchingType(V_RECORDINFO(&vtmp))) + { + SafeVariantClear(&vtmp); + return DISP_E_TYPEMISMATCH; + } - pOle += nOleArrayStepLength; + // Now copy the contents of the new variant back into the old variant. + HRESULT hr = V_RECORDINFO(pOle)->RecordCopy(V_RECORD(&vtmp), V_RECORD(pOle)); + if (hr != S_OK) + { + SafeVariantClear(&vtmp); + return DISP_E_TYPEMISMATCH; } } - GCPROTECT_END(); } - GCPROTECT_END(); + return S_OK; } -void OleVariant::ClearVariantArray(void *oleArray, SIZE_T cElements, MethodTable *pInterfaceMT) +void OleVariant::MarshalVarArgVariantArrayToOle(PTRARRAYREF *pClrArray, VARIANT *oleArray) { CONTRACTL { - NOTHROW; + THROWS; GC_TRIGGERS; - MODE_ANY; + MODE_COOPERATIVE; PRECONDITION(CheckPointer(oleArray)); + PRECONDITION(CheckPointer(pClrArray)); } CONTRACTL_END; + ASSERT_PROTECTED(pClrArray); + + SIZE_T elementCount = (*pClrArray)->GetNumComponents(); + VARIANT *pOle = (VARIANT *) oleArray; - VARIANT *pOleEnd = pOle + cElements; - while (pOle < pOleEnd) - SafeVariantClear(pOle++); + OBJECTREF *pClr = (*pClrArray)->GetDataPtr(); + + OBJECTREF TmpObj = NULL; + GCPROTECT_BEGIN(TmpObj) + GCPROTECT_BEGININTERIOR(pClr) + for (SIZE_T i = 0; i < elementCount; i++) + { + TmpObj = pClr[i]; + VARIANT *pCurrent = pOle - ((SSIZE_T)i); + + // Marshal the temp managed variant into the OLE variant. + // We firstly try MarshalCommonOleRefVariantForObject for VT_BYREF variant because + // MarshalOleVariantForObject() VariantClear the variant and does not keep the VT_BYREF. + // MarshalCommonOleRefVariantForObject is used instead of MarshalOleRefVariantForObject so + // that cast will not be done based on the VT of the variant. + if (!((pCurrent->vt & VT_BYREF) && + SUCCEEDED(MarshalCommonOleRefVariantForObject(&TmpObj, pCurrent)))) + { + if (pCurrent->vt & VT_BYREF) + MarshalOleVariantForObject(&TmpObj, pCurrent); + } + } + GCPROTECT_END(); + GCPROTECT_END(); } /* ------------------------------------------------------------------------- * * Array marshaling routines * ------------------------------------------------------------------------- */ -#ifdef FEATURE_COMINTEROP void OleVariant::MarshalArrayVariantOleToObject(const VARIANT* pOleVariant, OBJECTREF * const & pObj) @@ -3175,9 +1456,15 @@ void OleVariant::MarshalArrayVariantOleToObject(const VARIANT* pOleVariant, if (vt == VT_RECORD) pElemMT = GetElementTypeForRecordSafeArray(pSafeArray).GetMethodTable(); + PCODE pConvertCode; + { + GCX_PREEMP(); + pConvertCode = GetInstantiatedSafeArrayMethod(METHOD__STUBHELPERS__CONVERT_ARRAY_CONTENTS_TO_MANAGED, vt, pElemMT, FALSE)->GetMultiCallableAddrOfCode(); + } + BASEARRAYREF pArrayRef = CreateArrayRefForSafeArray(pSafeArray, vt, pElemMT); SetObjectReference(pObj, pArrayRef); - MarshalArrayRefForSafeArray(pSafeArray, (BASEARRAYREF *) pObj, vt, pElemMT); + MarshalArrayRefForSafeArray(pSafeArray, (BASEARRAYREF *) pObj, vt, pElemMT, pConvertCode); } else { @@ -3214,7 +1501,14 @@ void OleVariant::MarshalArrayVariantObjectToOle(OBJECTREF * const & pObj, if (*pArrayRef != NULL) { pSafeArray = CreateSafeArrayForArrayRef(pArrayRef, vt, pElemMT); - MarshalSafeArrayForArrayRef(pArrayRef, pSafeArray, vt, pElemMT); + + PCODE pConvertCode; + { + GCX_PREEMP(); + pConvertCode = GetInstantiatedSafeArrayMethod(METHOD__STUBHELPERS__CONVERT_ARRAY_CONTENTS_TO_UNMANAGED, vt, pElemMT, FALSE)->GetMultiCallableAddrOfCode(); + } + + MarshalSafeArrayForArrayRef(pArrayRef, pSafeArray, vt, pElemMT, pConvertCode); } V_ARRAY(pOleVariant) = pSafeArray.Detach(); } @@ -3243,16 +1537,21 @@ void OleVariant::MarshalArrayVariantOleRefToObject(const VARIANT *pOleVariant, if (vt == VT_RECORD) pElemMT = GetElementTypeForRecordSafeArray(pSafeArray).GetMethodTable(); + PCODE pConvertCode; + { + GCX_PREEMP(); + pConvertCode = GetInstantiatedSafeArrayMethod(METHOD__STUBHELPERS__CONVERT_ARRAY_CONTENTS_TO_MANAGED, vt, pElemMT, FALSE)->GetMultiCallableAddrOfCode(); + } + BASEARRAYREF pArrayRef = CreateArrayRefForSafeArray(pSafeArray, vt, pElemMT); SetObjectReference(pObj, pArrayRef); - MarshalArrayRefForSafeArray(pSafeArray, (BASEARRAYREF *) pObj, vt, pElemMT); + MarshalArrayRefForSafeArray(pSafeArray, (BASEARRAYREF *) pObj, vt, pElemMT, pConvertCode); } else { SetObjectReference(pObj, NULL); } } -#endif //FEATURE_COMINTEROP /* ------------------------------------------------------------------------- * @@ -3511,6 +1810,268 @@ BASEARRAYREF OleVariant::CreateArrayRefForSafeArray(SAFEARRAY *pSafeArray, VARTY * Safearray marshaling * ------------------------------------------------------------------------- */ +namespace +{ + // Returns the managed IArrayMarshaler MethodTable for a given VARTYPE. + // This mirrors the logic in GetMarshalerAndElementTypes for SAFEARRAY-compatible types. + MethodTable* GetMarshalerMTForSafeArrayVarType(VARTYPE vt, MethodTable* pElementMT, BOOL bHeterogeneous, BOOL bNativeDataValid) + { + STANDARD_VM_CONTRACT; + + switch (vt) + { + case VT_I1: + { + TypeHandle th = CoreLibBinder::GetClass(CLASS__SBYTE); + return TypeHandle(CoreLibBinder::GetClass(CLASS__BLITTABLE_ARRAY_MARSHALER)).Instantiate(Instantiation(&th, 1)).AsMethodTable(); + } + case VT_UI1: + { + TypeHandle th = CoreLibBinder::GetClass(CLASS__BYTE); + return TypeHandle(CoreLibBinder::GetClass(CLASS__BLITTABLE_ARRAY_MARSHALER)).Instantiate(Instantiation(&th, 1)).AsMethodTable(); + } + case VT_I2: + { + TypeHandle th = CoreLibBinder::GetClass(CLASS__INT16); + return TypeHandle(CoreLibBinder::GetClass(CLASS__BLITTABLE_ARRAY_MARSHALER)).Instantiate(Instantiation(&th, 1)).AsMethodTable(); + } + case VT_UI2: + { + if (pElementMT == CoreLibBinder::GetClass(CLASS__CHAR)) + { + TypeHandle th = CoreLibBinder::GetClass(CLASS__CHAR); + return TypeHandle(CoreLibBinder::GetClass(CLASS__BLITTABLE_ARRAY_MARSHALER)).Instantiate(Instantiation(&th, 1)).AsMethodTable(); + } + TypeHandle th = CoreLibBinder::GetClass(CLASS__UINT16); + return TypeHandle(CoreLibBinder::GetClass(CLASS__BLITTABLE_ARRAY_MARSHALER)).Instantiate(Instantiation(&th, 1)).AsMethodTable(); + } + case VT_I4: + case VT_INT: + case VT_ERROR: + { + TypeHandle th = (pElementMT == CoreLibBinder::GetClass(CLASS__INTPTR)) + ? CoreLibBinder::GetClass(CLASS__INTPTR) + : CoreLibBinder::GetClass(CLASS__INT32); + return TypeHandle(CoreLibBinder::GetClass(CLASS__BLITTABLE_ARRAY_MARSHALER)).Instantiate(Instantiation(&th, 1)).AsMethodTable(); + } + case VT_UI4: + case VT_UINT: + { + TypeHandle th = (pElementMT == CoreLibBinder::GetClass(CLASS__UINTPTR)) + ? CoreLibBinder::GetClass(CLASS__UINTPTR) + : CoreLibBinder::GetClass(CLASS__UINT32); + return TypeHandle(CoreLibBinder::GetClass(CLASS__BLITTABLE_ARRAY_MARSHALER)).Instantiate(Instantiation(&th, 1)).AsMethodTable(); + } + case VT_I8: + { + TypeHandle th = (pElementMT == CoreLibBinder::GetClass(CLASS__INTPTR)) + ? CoreLibBinder::GetClass(CLASS__INTPTR) + : CoreLibBinder::GetClass(CLASS__INT64); + return TypeHandle(CoreLibBinder::GetClass(CLASS__BLITTABLE_ARRAY_MARSHALER)).Instantiate(Instantiation(&th, 1)).AsMethodTable(); + } + case VT_UI8: + { + TypeHandle th = (pElementMT == CoreLibBinder::GetClass(CLASS__UINTPTR)) + ? CoreLibBinder::GetClass(CLASS__UINTPTR) + : CoreLibBinder::GetClass(CLASS__UINT64); + return TypeHandle(CoreLibBinder::GetClass(CLASS__BLITTABLE_ARRAY_MARSHALER)).Instantiate(Instantiation(&th, 1)).AsMethodTable(); + } + case VT_R4: + { + TypeHandle th = CoreLibBinder::GetClass(CLASS__SINGLE); + return TypeHandle(CoreLibBinder::GetClass(CLASS__BLITTABLE_ARRAY_MARSHALER)).Instantiate(Instantiation(&th, 1)).AsMethodTable(); + } + case VT_R8: + { + TypeHandle th = CoreLibBinder::GetClass(CLASS__DOUBLE); + return TypeHandle(CoreLibBinder::GetClass(CLASS__BLITTABLE_ARRAY_MARSHALER)).Instantiate(Instantiation(&th, 1)).AsMethodTable(); + } + case VT_DECIMAL: + { + TypeHandle th = CoreLibBinder::GetClass(CLASS__DECIMAL); + return TypeHandle(CoreLibBinder::GetClass(CLASS__BLITTABLE_ARRAY_MARSHALER)).Instantiate(Instantiation(&th, 1)).AsMethodTable(); + } + case VT_BOOL: + return CoreLibBinder::GetClass(CLASS__VARIANT_BOOL_MARSHALER); + + case VT_DATE: + return CoreLibBinder::GetClass(CLASS__DATEMARSHALER); + + case VT_LPWSTR: + return CoreLibBinder::GetClass(CLASS__LPWSTR_MARSHALER); + + case VT_LPSTR: + { + // SAFEARRAY LPSTR marshalling always uses default best-fit/throw-on-unmappable. + MethodTable* pBestFitEnabledMT = CoreLibBinder::GetClass(CLASS__MARSHALER_OPTION_ENABLED); + MethodTable* pThrowOnUnmappableDisabledMT = CoreLibBinder::GetClass(CLASS__MARSHALER_OPTION_DISABLED); + TypeHandle thArgs[2] = { TypeHandle(pBestFitEnabledMT), TypeHandle(pThrowOnUnmappableDisabledMT) }; + return TypeHandle(CoreLibBinder::GetClass(CLASS__LPSTR_ARRAY_ELEMENT_MARSHALER)).Instantiate(Instantiation(thArgs, 2)).AsMethodTable(); + } + + case VT_CY: + return CoreLibBinder::GetClass(CLASS__CURRENCY_ARRAY_ELEMENT_MARSHALER); + + case VT_BSTR: + return CoreLibBinder::GetClass(CLASS__BSTR_ARRAY_ELEMENT_MARSHALER); + + case VT_UNKNOWN: + case VT_DISPATCH: + { + if (pElementMT == NULL || pElementMT == g_pObjectClass) + { + if (bHeterogeneous) + { + return CoreLibBinder::GetClass(CLASS__HETEROGENEOUS_INTERFACE_ARRAY_ELEMENT_MARSHALER); + } + MethodTable* pEnabledMT = CoreLibBinder::GetClass(CLASS__MARSHALER_OPTION_ENABLED); + MethodTable* pDisabledMT = CoreLibBinder::GetClass(CLASS__MARSHALER_OPTION_DISABLED); + TypeHandle thDispatch(vt == VT_DISPATCH ? pEnabledMT : pDisabledMT); + return TypeHandle(CoreLibBinder::GetClass(CLASS__INTERFACE_ARRAY_ELEMENT_MARSHALER)).Instantiate(Instantiation(&thDispatch, 1)).AsMethodTable(); + } + else if (!pElementMT->IsInterface()) + { + // For class types, resolve the default COM interface. + BOOL bDispatch = FALSE; + MethodTable* pDefaultItfMT = GetDefaultInterfaceMTForClass(pElementMT, &bDispatch); + if (pDefaultItfMT != NULL) + { + // Use the resolved interface type. + TypeHandle thElement(pDefaultItfMT); + return TypeHandle(CoreLibBinder::GetClass(CLASS__TYPED_INTERFACE_ARRAY_ELEMENT_MARSHALER)).Instantiate(Instantiation(&thElement, 1)).AsMethodTable(); + } + else + { + // No specific interface — use untyped IDispatch or IUnknown. + MethodTable* pEnabledMT = CoreLibBinder::GetClass(CLASS__MARSHALER_OPTION_ENABLED); + MethodTable* pDisabledMT = CoreLibBinder::GetClass(CLASS__MARSHALER_OPTION_DISABLED); + TypeHandle thDispatch(bDispatch ? pEnabledMT : pDisabledMT); + return TypeHandle(CoreLibBinder::GetClass(CLASS__INTERFACE_ARRAY_ELEMENT_MARSHALER)).Instantiate(Instantiation(&thDispatch, 1)).AsMethodTable(); + } + } + else + { + TypeHandle thElement(pElementMT); + return TypeHandle(CoreLibBinder::GetClass(CLASS__TYPED_INTERFACE_ARRAY_ELEMENT_MARSHALER)).Instantiate(Instantiation(&thElement, 1)).AsMethodTable(); + } + } + + case VT_VARIANT: + { + MethodTable* pOptionMT = bNativeDataValid + ? CoreLibBinder::GetClass(CLASS__MARSHALER_OPTION_ENABLED) + : CoreLibBinder::GetClass(CLASS__MARSHALER_OPTION_DISABLED); + TypeHandle thOption(pOptionMT); + return TypeHandle(CoreLibBinder::GetClass(CLASS__VARIANT_ARRAY_ELEMENT_MARSHALER)).Instantiate(Instantiation(&thOption, 1)).AsMethodTable(); + } + + case VT_RECORD: + { + _ASSERTE(pElementMT != NULL); + TypeHandle thElement(pElementMT); + if (thElement.IsBlittable()) + { + return TypeHandle(CoreLibBinder::GetClass(CLASS__BLITTABLE_ARRAY_MARSHALER)).Instantiate(Instantiation(&thElement, 1)).AsMethodTable(); + } + else + { + return TypeHandle(CoreLibBinder::GetClass(CLASS__STRUCTURE_MARSHALER)).Instantiate(Instantiation(&thElement, 1)).AsMethodTable(); + } + } + + default: + _ASSERTE(!"Unsupported VT for SafeArray marshaler"); + COMPlusThrow(kArgumentException, IDS_EE_COM_UNSUPPORTED_SIG); + return NULL; + } + } + + // Returns the element TypeHandle for a given VARTYPE and element MethodTable. + TypeHandle GetElementTypeForSafeArrayVarType(VARTYPE vt, MethodTable* pElementMT) + { + STANDARD_VM_CONTRACT; + + switch (vt) + { + case VT_BOOL: return TypeHandle(CoreLibBinder::GetClass(CLASS__BOOLEAN)); + case VT_I1: return TypeHandle(CoreLibBinder::GetClass(CLASS__SBYTE)); + case VT_UI1: return TypeHandle(CoreLibBinder::GetClass(CLASS__BYTE)); + case VT_I2: return TypeHandle(CoreLibBinder::GetClass(CLASS__INT16)); + case VT_UI2: + if (pElementMT == CoreLibBinder::GetClass(CLASS__CHAR)) + return TypeHandle(CoreLibBinder::GetClass(CLASS__CHAR)); + return TypeHandle(CoreLibBinder::GetClass(CLASS__UINT16)); + case VT_I4: + case VT_INT: + case VT_ERROR: + if (pElementMT == CoreLibBinder::GetClass(CLASS__INTPTR)) + return TypeHandle(CoreLibBinder::GetClass(CLASS__INTPTR)); + return TypeHandle(CoreLibBinder::GetClass(CLASS__INT32)); + case VT_UI4: + case VT_UINT: + if (pElementMT == CoreLibBinder::GetClass(CLASS__UINTPTR)) + return TypeHandle(CoreLibBinder::GetClass(CLASS__UINTPTR)); + return TypeHandle(CoreLibBinder::GetClass(CLASS__UINT32)); + case VT_I8: + if (pElementMT == CoreLibBinder::GetClass(CLASS__INTPTR)) + return TypeHandle(CoreLibBinder::GetClass(CLASS__INTPTR)); + return TypeHandle(CoreLibBinder::GetClass(CLASS__INT64)); + case VT_UI8: + if (pElementMT == CoreLibBinder::GetClass(CLASS__UINTPTR)) + return TypeHandle(CoreLibBinder::GetClass(CLASS__UINTPTR)); + return TypeHandle(CoreLibBinder::GetClass(CLASS__UINT64)); + case VT_R4: return TypeHandle(CoreLibBinder::GetClass(CLASS__SINGLE)); + case VT_R8: return TypeHandle(CoreLibBinder::GetClass(CLASS__DOUBLE)); + case VT_DECIMAL: return TypeHandle(CoreLibBinder::GetClass(CLASS__DECIMAL)); + case VT_DATE: return TypeHandle(CoreLibBinder::GetClass(CLASS__DATE_TIME)); + case VT_BSTR: + case VT_LPWSTR: + case VT_LPSTR: return TypeHandle(g_pStringClass); + case VT_CY: return TypeHandle(CoreLibBinder::GetClass(CLASS__DECIMAL)); + case VT_VARIANT: return TypeHandle(g_pObjectClass); + case VT_UNKNOWN: + case VT_DISPATCH: + if (pElementMT == NULL || pElementMT == g_pObjectClass) + return TypeHandle(g_pObjectClass); + if (pElementMT->IsInterface()) + return TypeHandle(pElementMT); + { + // For class types, resolve to the default interface type. + BOOL bDispatch = FALSE; + MethodTable* pDefaultItfMT = GetDefaultInterfaceMTForClass(pElementMT, &bDispatch); + if (pDefaultItfMT != NULL) + return TypeHandle(pDefaultItfMT); + return TypeHandle(g_pObjectClass); + } + case VT_RECORD: + _ASSERTE(pElementMT != NULL); + return TypeHandle(pElementMT); + default: + COMPlusThrow(kArgumentException, IDS_EE_COM_UNSUPPORTED_SIG); + return TypeHandle(); + } + } +} + +MethodDesc* GetInstantiatedSafeArrayMethod(BinderMethodID methodId, VARTYPE vt, MethodTable* pElementMT, BOOL bHeterogeneous, BOOL bNativeDataValid) +{ + STANDARD_VM_CONTRACT; + + MethodDesc* pGenericMD = CoreLibBinder::GetMethod(methodId); + + TypeHandle thElementType = GetElementTypeForSafeArrayVarType(vt, pElementMT); + TypeHandle thMarshalerType(GetMarshalerMTForSafeArrayVarType(vt, pElementMT, bHeterogeneous, bNativeDataValid)); + + TypeHandle thArgs[2] = { thElementType, thMarshalerType }; + + return MethodDesc::FindOrCreateAssociatedMethodDesc( + pGenericMD, + pGenericMD->GetMethodTable(), + FALSE, + Instantiation(thArgs, 2), + FALSE); +} + // // MarshalSafeArrayForArrayRef marshals the contents of the array ref into the given // safe array. It is assumed that the type & dimensions of the arrays are compatible. @@ -3519,7 +2080,7 @@ void OleVariant::MarshalSafeArrayForArrayRef(BASEARRAYREF *pArrayRef, SAFEARRAY *pSafeArray, VARTYPE vt, MethodTable *pInterfaceMT, - BOOL fSafeArrayIsValid /*= TRUE*/) + PCODE pConvertContentsCode) { CONTRACTL { @@ -3542,12 +2103,9 @@ void OleVariant::MarshalSafeArrayForArrayRef(BASEARRAYREF *pArrayRef, GCPROTECT_BEGIN(Array) { - // Retrieve the marshaler to use to convert the contents. - const Marshaler *marshal = GetMarshalerForVarType(vt, TRUE); - // If the array is an array of wrappers, then we need to extract the objects // being wrapped and create an array of those. - BOOL bArrayOfInterfaceWrappers; + BOOL bArrayOfInterfaceWrappers = FALSE; if (IsArrayOfWrappers(pArrayRef, &bArrayOfInterfaceWrappers)) { Array = ExtractWrappedObjectsFromArray(pArrayRef); @@ -3557,41 +2115,15 @@ void OleVariant::MarshalSafeArrayForArrayRef(BASEARRAYREF *pArrayRef, Array = *pArrayRef; } - if (marshal == NULL || marshal->ComToOleArray == NULL) + // Use managed IArrayMarshaler implementations for content conversion. + UnmanagedCallersOnlyCaller invoker(METHOD__STUBHELPERS__INVOKE_ARRAY_CONTENTS_CONVERTER); + invoker.InvokeThrowing(&Array, pSafeArray->pvData, (INT32)dwNumComponents, (void*)pConvertContentsCode); + + if (pSafeArray->cDims != 1) { - if (pSafeArray->cDims == 1) - { - // If the array is single dimensionnal then we can simply copy it over. - memcpyNoGCRefs(pSafeArray->pvData, Array->GetDataPtr(), dwNumComponents * dwComponentSize); - } - else - { - // Copy and transpose the data. - TransposeArrayData((BYTE*)pSafeArray->pvData, Array->GetDataPtr(), dwNumComponents, dwComponentSize, pSafeArray, FALSE); - } + // The array is multidimensional - transpose the data in place. + TransposeArrayData((BYTE*)pSafeArray->pvData, (BYTE*)pSafeArray->pvData, dwNumComponents, dwComponentSize, pSafeArray, FALSE); } - else - { - { - PinningHandleHolder handle(GetAppDomain()->CreatePinningHandle((OBJECTREF)Array)); - - if (bArrayOfInterfaceWrappers) - { - _ASSERTE(vt == VT_UNKNOWN || vt == VT_DISPATCH); - // Signal to code:OleVariant::MarshalInterfaceArrayComToOleHelper that this was an array - // of UnknownWrapper or DispatchWrapper. It shall use a different logic and marshal each - // element according to its specific default interface. - pInterfaceMT = NULL; - } - marshal->ComToOleArray(&Array, pSafeArray->pvData, pInterfaceMT, TRUE, FALSE, fSafeArrayIsValid, dwNumComponents); - } - - if (pSafeArray->cDims != 1) - { - // The array is multidimensionnal we need to transpose it. - TransposeArrayData((BYTE*)pSafeArray->pvData, (BYTE*)pSafeArray->pvData, dwNumComponents, dwComponentSize, pSafeArray, FALSE); - } - } } GCPROTECT_END(); } @@ -3604,7 +2136,8 @@ void OleVariant::MarshalSafeArrayForArrayRef(BASEARRAYREF *pArrayRef, void OleVariant::MarshalArrayRefForSafeArray(SAFEARRAY *pSafeArray, BASEARRAYREF *pArrayRef, VARTYPE vt, - MethodTable *pInterfaceMT) + MethodTable *pInterfaceMT, + PCODE pConvertContentsCode) { CONTRACTL { @@ -3622,59 +2155,26 @@ void OleVariant::MarshalArrayRefForSafeArray(SAFEARRAY *pSafeArray, // Retrieve the number of components. SIZE_T dwNumComponents = (*pArrayRef)->GetNumComponents(); + SIZE_T dwNativeComponentSize = GetElementSizeForVarType(vt, pInterfaceMT); - // Retrieve the marshaler to use to convert the contents. - const Marshaler *marshal = GetMarshalerForVarType(vt, TRUE); + CQuickArray TmpArray; + BYTE* pSrcData = NULL; - if (marshal == NULL || marshal->OleToComArray == NULL) + if (pSafeArray->cDims != 1) { - SIZE_T dwManagedComponentSize = (*pArrayRef)->GetComponentSize(); - -#ifdef _DEBUG - { - // If we're blasting bits, this better be a primitive type. Currency is - // an I8 on managed & unmanaged, so it's good enough. - TypeHandle th = (*pArrayRef)->GetArrayElementTypeHandle(); - - if (!CorTypeInfo::IsPrimitiveType(th.GetInternalCorElementType())) - { - _ASSERTE(!strcmp(th.AsMethodTable()->GetDebugClassName(), "System.Currency") - || !strcmp(th.AsMethodTable()->GetDebugClassName(), "System.Decimal")); - } - } -#endif - if (pSafeArray->cDims == 1) - { - // If the array is single dimensionnal then we can simply copy it over. - memcpyNoGCRefs((*pArrayRef)->GetDataPtr(), pSafeArray->pvData, dwNumComponents * dwManagedComponentSize); - } - else - { - // Copy and transpose the data. - TransposeArrayData((*pArrayRef)->GetDataPtr(), (BYTE*)pSafeArray->pvData, dwNumComponents, dwManagedComponentSize, pSafeArray, TRUE); - } + // Multi-dimensional arrays need transposition before content conversion. + TmpArray.ReSizeThrows(dwNumComponents * dwNativeComponentSize); + pSrcData = TmpArray.Ptr(); + TransposeArrayData(pSrcData, (BYTE*)pSafeArray->pvData, dwNumComponents, dwNativeComponentSize, pSafeArray, TRUE); } else { - CQuickArray TmpArray; - BYTE* pSrcData = NULL; - SIZE_T dwNativeComponentSize = GetElementSizeForVarType(vt, pInterfaceMT); - - if (pSafeArray->cDims != 1) - { - TmpArray.ReSizeThrows(dwNumComponents * dwNativeComponentSize); - pSrcData = TmpArray.Ptr(); - TransposeArrayData(pSrcData, (BYTE*)pSafeArray->pvData, dwNumComponents, dwNativeComponentSize, pSafeArray, TRUE); - } - else - { - pSrcData = (BYTE*)pSafeArray->pvData; - } - - PinningHandleHolder handle(GetAppDomain()->CreatePinningHandle((OBJECTREF)*pArrayRef)); - - marshal->OleToComArray(pSrcData, pArrayRef, pInterfaceMT); + pSrcData = (BYTE*)pSafeArray->pvData; } + + // Use managed IArrayMarshaler implementations for content conversion. + UnmanagedCallersOnlyCaller invoker(METHOD__STUBHELPERS__INVOKE_ARRAY_CONTENTS_CONVERTER); + invoker.InvokeThrowing(pArrayRef, pSrcData, (INT32)dwNumComponents, (void*)pConvertContentsCode); } void OleVariant::ConvertValueClassToVariant(OBJECTREF *pBoxedValueClass, VARIANT *pOleVariant) @@ -4083,7 +2583,6 @@ TypeHandle OleVariant::GetArrayElementTypeWrapperAware(BASEARRAYREF *pArray) } } -#ifdef FEATURE_COMINTEROP TypeHandle OleVariant::GetElementTypeForRecordSafeArray(SAFEARRAY* pSafeArray) { CONTRACTL @@ -4099,7 +2598,6 @@ TypeHandle OleVariant::GetElementTypeForRecordSafeArray(SAFEARRAY* pSafeArray) COMPlusThrow(kArgumentException, IDS_EE_CANNOT_MAP_TO_MANAGED_VC); return TypeHandle(); // Unreachable } -#endif //FEATURE_COMINTEROP void OleVariant::ConvertBSTRToString(BSTR bstr, STRINGREF *pStringObj) { @@ -4162,4 +2660,4 @@ extern "C" void QCALLTYPE Variant_ConvertValueTypeToRecord(QCall::ObjectHandleOn END_QCALL; } -#endif // FEATURE_COMINTEROP + diff --git a/src/coreclr/vm/olevariant.h b/src/coreclr/vm/olevariant.h index dec0cecb488400..a3dd84ab287a88 100644 --- a/src/coreclr/vm/olevariant.h +++ b/src/coreclr/vm/olevariant.h @@ -4,31 +4,17 @@ // File: OleVariant.h // -// - - #ifndef _H_OLEVARIANT_ #define _H_OLEVARIANT_ - -// The COM interop native array marshaler is built on top of VT_* types. -// The P/Invoke marshaler supports marshaling to WINBOOL's and ANSICHAR's. -// This is an annoying workaround to shoehorn these non-OleAut types into -// the COM interop marshaler. -#define VTHACK_INSPECTABLE 247 -#define VTHACK_HSTRING 248 -#define VTHACK_CBOOL 250 -#define VTHACK_NONBLITTABLERECORD 251 -#define VTHACK_BLITTABLERECORD 252 -#define VTHACK_ANSICHAR 253 -#define VTHACK_WINBOOL 254 - +#ifndef FEATURE_COMINTEROP +#error FEATURE_COMINTEROP is required for this file +#endif // FEATURE_COMINTEROP class OleVariant { public: -#ifdef FEATURE_COMINTEROP // New variant conversion static void MarshalOleVariantForObject(OBJECTREF * const & pObj, VARIANT *pOle); static void MarshalObjectForOleVariant(const VARIANT *pOle, OBJECTREF * const & pObj); @@ -39,9 +25,7 @@ class OleVariant static void MarshalObjectForOleVariantUncommon(const VARIANT *pOle, OBJECTREF * const & pObj); static void MarshalOleVariantForObjectUncommon(OBJECTREF * const & pObj, VARIANT *pOle); -#endif // FEATURE_COMINTEROP -#ifdef FEATURE_COMINTEROP // Safearray conversion static SAFEARRAY* CreateSafeArrayDescriptorForArrayRef(BASEARRAYREF* pArrayRef, VARTYPE vt, @@ -57,12 +41,13 @@ class OleVariant SAFEARRAY* pSafeArray, VARTYPE vt, MethodTable* pInterfaceMT, - BOOL fSafeArrayIsValid = TRUE); + PCODE pConvertContentsCode); static void MarshalArrayRefForSafeArray(SAFEARRAY* pSafeArray, BASEARRAYREF* pArrayRef, VARTYPE vt, - MethodTable* pInterfaceMT); + MethodTable* pInterfaceMT, + PCODE pConvertContentsCode); // Helper function to convert a boxed value class to an OLE variant. static void ConvertValueClassToVariant(OBJECTREF *pBoxedValueClass, VARIANT *pOleVariant); @@ -79,29 +64,20 @@ class OleVariant static HRESULT ClearAndInsertContentsIntoByrefRecordVariant(VARIANT* pOle, OBJECTREF* pObj); static BOOL IsValidArrayForSafeArrayElementType(BASEARRAYREF* pArrayRef, VARTYPE vtExpected); -#endif // FEATURE_COMINTEROP -#ifdef FEATURE_COMINTEROP static BOOL CheckVariant(VARIANT *pOle); // Type conversion utilities static void ExtractContentsFromByrefVariant(VARIANT* pByrefVar, VARIANT* pDestVar); static void InsertContentsIntoByRefVariant(VARIANT* pSrcVar, VARIANT* pByrefVar); static void CreateByrefVariantForVariant(VARIANT* pSrcVar, VARIANT* pByrefVar); -#endif // FEATURE_COMINTEROP - static TypeHandle GetTypeHandleForVarType(VARTYPE vt); - static VARTYPE GetVarTypeForTypeHandle(TypeHandle typeHnd); - - static VARTYPE GetVarTypeForValueClassArrayName(LPCUTF8 pArrayClassName); static VARTYPE GetElementVarTypeForArrayRef(BASEARRAYREF pArrayRef); // Note that Rank == 0 means SZARRAY (that is rank 1, no lower bounds) static TypeHandle GetArrayForVarType(VARTYPE vt, TypeHandle elemType, unsigned rank=0); static UINT GetElementSizeForVarType(VARTYPE vt, MethodTable* pInterfaceMT); - static MethodTable* GetNativeMethodTableForVarType(VARTYPE vt, MethodTable* pManagedMT); -#ifdef FEATURE_COMINTEROP // Determine the element type of the objects being wrapped by an array of wrappers. static TypeHandle GetWrappedArrayElementType(BASEARRAYREF* pArray); @@ -113,155 +89,24 @@ class OleVariant // Determine the type of the elements for a safe array of records. static TypeHandle GetElementTypeForRecordSafeArray(SAFEARRAY* pSafeArray); - // Helper called from MarshalIUnknownArrayComToOle and MarshalIDispatchArrayComToOle. - static void MarshalInterfaceArrayComToOleHelper(BASEARRAYREF* pComArray, void* oleArray, - MethodTable* pElementMT, BOOL bDefaultIsDispatch, - SIZE_T cElements); -#endif // FEATURE_COMINTEROP - - struct Marshaler - { - void (*OleToComArray)(void* oleArray, BASEARRAYREF* pComArray, MethodTable* pInterfaceMT); - void (*ComToOleArray)(BASEARRAYREF* pComArray, void* oleArray, MethodTable* pInterfaceMT, - BOOL fBestFitMapping, BOOL fThrowOnUnmappableChar, - BOOL fOleArrayIsValid,SIZE_T cElements); - void (*ClearOleArray)(void* oleArray, SIZE_T cElements, MethodTable* pInterfaceMT); - }; - - static const Marshaler* GetMarshalerForVarType(VARTYPE vt, BOOL fThrow); - - static void MarshalVariantArrayComToOle(BASEARRAYREF* pComArray, void* oleArray, - MethodTable* pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, BOOL fMarshalByrefArgOnly, - BOOL fOleArrayIsValid, int nOleArrayStepLength = 1); + static void MarshalVarArgVariantArrayToOle(PTRARRAYREF* pComArray, VARIANT* oleArray); private: - - // Specific marshaler functions - - static void MarshalBoolArrayOleToCom(void *oleArray, BASEARRAYREF* pComArray, - MethodTable* pInterfaceMT); - static void MarshalBoolArrayComToOle(BASEARRAYREF* pComArray, void* oleArray, - MethodTable* pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, BOOL fOleArrayIsValid, - SIZE_T cElements); - - static void MarshalWinBoolArrayOleToCom(void* oleArray, BASEARRAYREF* pComArray, - MethodTable* pInterfaceMT); - static void MarshalWinBoolArrayComToOle(BASEARRAYREF* pComArray, void* oleArray, - MethodTable* pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, BOOL fOleArrayValid, - SIZE_T cElements); - static void MarshalCBoolArrayOleToCom(void* oleArray, BASEARRAYREF* pComArray, - MethodTable* pInterfaceMT); - static void MarshalCBoolArrayComToOle(BASEARRAYREF* pComArray, void* oleArray, - MethodTable* pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, BOOL fOleArrayValid, - SIZE_T cElements); - - static void MarshalAnsiCharArrayOleToCom(void* oleArray, BASEARRAYREF* pComArray, - MethodTable* pInterfaceMT); - static void MarshalAnsiCharArrayComToOle(BASEARRAYREF* pComArray, void* oleArray, - MethodTable* pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, BOOL fOleArrayValid, - SIZE_T cElements); - -#ifdef FEATURE_COMINTEROP - static void MarshalIDispatchArrayComToOle(BASEARRAYREF* pComArray, void* oleArray, - MethodTable* pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, BOOL fOleArrayValid, - SIZE_T cElements); -#endif // FEATURE_COMINTEROP - -#ifdef FEATURE_COMINTEROP - static void MarshalBSTRArrayOleToCom(void* oleArray, BASEARRAYREF* pComArray, - MethodTable* pInterfaceMT); - static void MarshalBSTRArrayComToOle(BASEARRAYREF* pComArray, void* oleArray, - MethodTable* pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, BOOL fOleArrayValid, - SIZE_T cElements); - static void ClearBSTRArray(void* oleArray, SIZE_T cElements, MethodTable* pInterfaceMT); -#endif // FEATURE_COMINTEROP - - static void MarshalNonBlittableRecordArrayOleToCom(void* oleArray, BASEARRAYREF* pComArray, - MethodTable* pInterfaceMT); - static void MarshalNonBlittableRecordArrayComToOle(BASEARRAYREF* pComArray, void* oleArray, - MethodTable* pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, BOOL fOleArrayValid, - SIZE_T cElements); - static void ClearNonBlittableRecordArray(void* oleArray, - SIZE_T cElements, MethodTable* pInterfaceMT); - - static void MarshalLPWSTRArrayOleToCom(void* oleArray, BASEARRAYREF* pComArray, - MethodTable* pInterfaceMT); - static void MarshalLPWSTRRArrayComToOle(BASEARRAYREF* pComArray, void* oleArray, - MethodTable* pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, BOOL fOleArrayValid, - SIZE_T cElements); - static void ClearLPWSTRArray(void* oleArray, - SIZE_T cElements, MethodTable* pInterfaceMT); - - static void MarshalLPSTRArrayOleToCom(void* oleArray, BASEARRAYREF* pComArray, - MethodTable* pInterfaceMT); - static void MarshalLPSTRRArrayComToOle(BASEARRAYREF* pComArray, void* oleArray, - MethodTable* pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, BOOL fOleArrayValid, - SIZE_T cElements); - static void ClearLPSTRArray(void* oleArray, - SIZE_T cElements, MethodTable* pInterfaceMT); - - static void MarshalDateArrayOleToCom(void* oleArray, BASEARRAYREF* pComArray, - MethodTable* pInterfaceMT); - static void MarshalDateArrayComToOle(BASEARRAYREF* pComArray, void* oleArray, - MethodTable* pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, BOOL fOleArrayValid, - SIZE_T cElements); - - static void MarshalRecordArrayOleToCom(void* oleArray, BASEARRAYREF* pComArray, MethodTable* pElementMT); - static void MarshalRecordArrayComToOle(BASEARRAYREF* pComArray, void* oleArray, MethodTable* pElementMT, - BOOL fBestFitMapping, BOOL fThrowOnUnmappableChar, - BOOL fOleArrayValid, - SIZE_T cElements); - static void ClearRecordArray(void* oleArray, SIZE_T cElements, MethodTable* pElementMT); - -#ifdef FEATURE_COMINTEROP static HRESULT MarshalCommonOleRefVariantForObject(OBJECTREF *pObj, VARIANT *pOle); - static void MarshalInterfaceArrayOleToCom(void* oleArray, BASEARRAYREF* pComArray, - MethodTable* pInterfaceMT); - static void MarshalIUnknownArrayComToOle(BASEARRAYREF* pComArray, void* oleArray, - MethodTable* pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, BOOL fOleArrayValid, - SIZE_T cElements); - static void ClearInterfaceArray(void* oleArray, SIZE_T cElements, MethodTable* pInterfaceMT); - -#ifdef FEATURE_COMINTEROP + static void MarshalRecordVariantOleToObject(const VARIANT* pOleVariant, OBJECTREF * const & pComVariant); -#endif - static void MarshalCurrencyArrayOleToCom(void* oleArray, BASEARRAYREF* pComArray, - MethodTable* pInterfaceMT); - static void MarshalCurrencyArrayComToOle(BASEARRAYREF* pComArray, void* oleArray, - MethodTable* pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, BOOL fOleArrayValid, - SIZE_T cElements); - - static void MarshalVariantArrayOleToCom(void* oleArray, BASEARRAYREF* pComArray, - MethodTable* pInterfaceMT); - static void MarshalVariantArrayComToOle(BASEARRAYREF* pComArray, void* oleArray, - MethodTable* pInterfaceMT, BOOL fBestFitMapping, - BOOL fThrowOnUnmappableChar, BOOL fOleArrayValid, - SIZE_T cElements); - static void ClearVariantArray(void* oleArray, SIZE_T cElements, MethodTable* pInterfaceMT); - -#ifdef FEATURE_COMINTEROP static void MarshalArrayVariantOleToObject(const VARIANT* pOleVariant, OBJECTREF * const & pObj); static void MarshalArrayVariantOleRefToObject(const VARIANT* pOleVariant, OBJECTREF * const & pObj); static void MarshalArrayVariantObjectToOle(OBJECTREF * const & pObj, VARIANT* pOleVariant); -#endif -#endif // FEATURE_COMINTEROP }; +// Returns the instantiated MethodDesc for a StubHelpers array marshalling method +// (e.g. ConvertArrayContentsToUnmanaged/ConvertArrayContentsToManaged) for a given +// SAFEARRAY VARTYPE and element MethodTable. +MethodDesc* GetInstantiatedSafeArrayMethod(BinderMethodID methodId, VARTYPE vt, MethodTable* pElementMT, BOOL bHeterogeneous, BOOL bNativeDataValid = FALSE); + extern "C" void QCALLTYPE Variant_ConvertValueTypeToRecord(QCall::ObjectHandleOnStack obj, VARIANT* pOle); #endif diff --git a/src/coreclr/vm/qcallentrypoints.cpp b/src/coreclr/vm/qcallentrypoints.cpp index b7633063e87fab..1692140748aee8 100644 --- a/src/coreclr/vm/qcallentrypoints.cpp +++ b/src/coreclr/vm/qcallentrypoints.cpp @@ -394,15 +394,6 @@ static const Entry s_QCall[] = DllImportEntry(MarshalNative_GetEndComSlot) DllImportEntry(MarshalNative_ChangeWrapperHandleStrength) #endif - DllImportEntry(MngdNativeArrayMarshaler_ConvertSpaceToNative) - DllImportEntry(MngdNativeArrayMarshaler_ConvertContentsToNative) - DllImportEntry(MngdNativeArrayMarshaler_ConvertSpaceToManaged) - DllImportEntry(MngdNativeArrayMarshaler_ConvertContentsToManaged) - DllImportEntry(MngdNativeArrayMarshaler_ClearNativeContents) - DllImportEntry(MngdFixedArrayMarshaler_ConvertContentsToNative) - DllImportEntry(MngdFixedArrayMarshaler_ConvertSpaceToManaged) - DllImportEntry(MngdFixedArrayMarshaler_ConvertContentsToManaged) - DllImportEntry(MngdFixedArrayMarshaler_ClearNativeContents) #ifdef FEATURE_COMINTEROP DllImportEntry(MngdSafeArrayMarshaler_CreateMarshaler) DllImportEntry(MngdSafeArrayMarshaler_ConvertSpaceToNative) diff --git a/src/coreclr/vm/stubhelpers.cpp b/src/coreclr/vm/stubhelpers.cpp index 477030bf729bb0..7e64050dfac66a 100644 --- a/src/coreclr/vm/stubhelpers.cpp +++ b/src/coreclr/vm/stubhelpers.cpp @@ -21,6 +21,7 @@ #ifdef FEATURE_COMINTEROP #include #include "olecontexthelpers.h" +#include "olevariant.h" #include "runtimecallablewrapper.h" #include "comcallablewrapper.h" #include "clrtocomcall.h" diff --git a/src/tests/Interop/ArrayMarshalling/SafeArray/SafeArrayNative.cpp b/src/tests/Interop/ArrayMarshalling/SafeArray/SafeArrayNative.cpp index cf3571f4ed9bf8..c815ce15755f9c 100644 --- a/src/tests/Interop/ArrayMarshalling/SafeArray/SafeArrayNative.cpp +++ b/src/tests/Interop/ArrayMarshalling/SafeArray/SafeArrayNative.cpp @@ -309,3 +309,209 @@ extern "C" DLL_EXPORT HRESULT STDMETHODCALLTYPE XorBoolArrayInStruct(StructWithS { return XorBoolArray(str.array, result); } + +// Creates a 2D SAFEARRAY of VT_I4 with dimensions [rows x cols]. +// Data is filled by logical indices with value = row * cols + col. +extern "C" DLL_EXPORT HRESULT STDMETHODCALLTYPE Create2DIntSafeArray(int rows, int cols, SAFEARRAY** ppResult) +{ + if (rows < 0 || cols < 0) + return E_INVALIDARG; + + SAFEARRAYBOUND bounds[2]; + bounds[0].lLbound = 0; + bounds[0].cElements = (ULONG)rows; + bounds[1].lLbound = 0; + bounds[1].cElements = (ULONG)cols; + + SAFEARRAY* psa = ::SafeArrayCreate(VT_I4, 2, bounds); + if (!psa) + return E_OUTOFMEMORY; + + for (LONG r = 0; r < rows; r++) + { + for (LONG c = 0; c < cols; c++) + { + LONG indices[2] = { r, c }; + int value = r * cols + c; + HRESULT hr = ::SafeArrayPutElement(psa, indices, &value); + if (FAILED(hr)) + { + ::SafeArrayDestroy(psa); + return hr; + } + } + } + + *ppResult = psa; + return S_OK; +} + +// Verifies a 2D SAFEARRAY of VT_I4 with dimensions [rows x cols]. +// Expected value at [r,c] = r * cols + c. +extern "C" DLL_EXPORT HRESULT STDMETHODCALLTYPE Verify2DIntSafeArray(SAFEARRAY* psa, int rows, int cols) +{ + HRESULT hr; + VARTYPE vt; + RETURN_IF_FAILED(::SafeArrayGetVartype(psa, &vt)); + if (vt != VT_I4) + return E_INVALIDARG; + + if (psa->cDims != 2) + return E_INVALIDARG; + + for (LONG r = 0; r < rows; r++) + { + for (LONG c = 0; c < cols; c++) + { + LONG indices[2] = { r, c }; + int value = 0; + RETURN_IF_FAILED(::SafeArrayGetElement(psa, indices, &value)); + int expected = r * cols + c; + if (value != expected) + return E_FAIL; + } + } + + return S_OK; +} + +// Creates a 2D SAFEARRAY of VT_BOOL with dimensions [rows x cols]. +// Value at [r,c] = ((r + c) % 2 == 0) ? VARIANT_TRUE : VARIANT_FALSE. +extern "C" DLL_EXPORT HRESULT STDMETHODCALLTYPE Create2DBoolSafeArray(int rows, int cols, SAFEARRAY** ppResult) +{ + if (rows < 0 || cols < 0) + return E_INVALIDARG; + + SAFEARRAYBOUND bounds[2]; + bounds[0].lLbound = 0; + bounds[0].cElements = (ULONG)rows; + bounds[1].lLbound = 0; + bounds[1].cElements = (ULONG)cols; + + SAFEARRAY* psa = ::SafeArrayCreate(VT_BOOL, 2, bounds); + if (!psa) + return E_OUTOFMEMORY; + + for (LONG r = 0; r < rows; r++) + { + for (LONG c = 0; c < cols; c++) + { + LONG indices[2] = { r, c }; + VARIANT_BOOL value = ((r + c) % 2 == 0) ? VARIANT_TRUE : VARIANT_FALSE; + HRESULT hr = ::SafeArrayPutElement(psa, indices, &value); + if (FAILED(hr)) + { + ::SafeArrayDestroy(psa); + return hr; + } + } + } + + *ppResult = psa; + return S_OK; +} + +// Verifies a 2D SAFEARRAY of VT_BOOL with dimensions [rows x cols]. +extern "C" DLL_EXPORT HRESULT STDMETHODCALLTYPE Verify2DBoolSafeArray(SAFEARRAY* psa, int rows, int cols) +{ + HRESULT hr; + VARTYPE vt; + RETURN_IF_FAILED(::SafeArrayGetVartype(psa, &vt)); + if (vt != VT_BOOL) + return E_INVALIDARG; + + if (psa->cDims != 2) + return E_INVALIDARG; + + for (LONG r = 0; r < rows; r++) + { + for (LONG c = 0; c < cols; c++) + { + LONG indices[2] = { r, c }; + VARIANT_BOOL value = VARIANT_FALSE; + RETURN_IF_FAILED(::SafeArrayGetElement(psa, indices, &value)); + VARIANT_BOOL expected = ((r + c) % 2 == 0) ? VARIANT_TRUE : VARIANT_FALSE; + if (value != expected) + return E_FAIL; + } + } + + return S_OK; +} + +// Creates a 2D SAFEARRAY of VT_BSTR with dimensions [rows x cols]. +// Value at [r,c] = "r,c". +extern "C" DLL_EXPORT HRESULT STDMETHODCALLTYPE Create2DStringSafeArray(int rows, int cols, SAFEARRAY** ppResult) +{ + if (rows < 0 || cols < 0) + return E_INVALIDARG; + + SAFEARRAYBOUND bounds[2]; + bounds[0].lLbound = 0; + bounds[0].cElements = (ULONG)rows; + bounds[1].lLbound = 0; + bounds[1].cElements = (ULONG)cols; + + SAFEARRAY* psa = ::SafeArrayCreate(VT_BSTR, 2, bounds); + if (!psa) + return E_OUTOFMEMORY; + + for (LONG r = 0; r < rows; r++) + { + for (LONG c = 0; c < cols; c++) + { + LONG indices[2] = { r, c }; + WCHAR buf[32]; + swprintf_s(buf, 32, L"%d,%d", (int)r, (int)c); + BSTR bstr = TP_SysAllocString(buf); + if (!bstr) + { + ::SafeArrayDestroy(psa); + return E_OUTOFMEMORY; + } + HRESULT hr = ::SafeArrayPutElement(psa, indices, bstr); + ::SysFreeString(bstr); + if (FAILED(hr)) + { + ::SafeArrayDestroy(psa); + return hr; + } + } + } + + *ppResult = psa; + return S_OK; +} + +// Verifies a 2D SAFEARRAY of VT_BSTR with dimensions [rows x cols]. +extern "C" DLL_EXPORT HRESULT STDMETHODCALLTYPE Verify2DStringSafeArray(SAFEARRAY* psa, int rows, int cols) +{ + HRESULT hr; + VARTYPE vt; + RETURN_IF_FAILED(::SafeArrayGetVartype(psa, &vt)); + if (vt != VT_BSTR) + return E_INVALIDARG; + + if (psa->cDims != 2) + return E_INVALIDARG; + + for (LONG r = 0; r < rows; r++) + { + for (LONG c = 0; c < cols; c++) + { + LONG indices[2] = { r, c }; + BSTR value = nullptr; + RETURN_IF_FAILED(::SafeArrayGetElement(psa, indices, &value)); + + WCHAR expected[32]; + swprintf_s(expected, 32, L"%d,%d", (int)r, (int)c); + + bool match = (value != nullptr && wcscmp(value, expected) == 0); + ::SysFreeString(value); + if (!match) + return E_FAIL; + } + } + + return S_OK; +} diff --git a/src/tests/Interop/ArrayMarshalling/SafeArray/SafeArrayTest.cs b/src/tests/Interop/ArrayMarshalling/SafeArray/SafeArrayTest.cs index 860dfefd2d58d1..293e89883a982c 100644 --- a/src/tests/Interop/ArrayMarshalling/SafeArray/SafeArrayTest.cs +++ b/src/tests/Interop/ArrayMarshalling/SafeArray/SafeArrayTest.cs @@ -88,6 +88,99 @@ public static int TestEntryPoint() return 100; } + [ConditionalFact(typeof(TestLibrary.PlatformDetection), nameof(TestLibrary.PlatformDetection.IsBuiltInComEnabled))] + public static void MultidimensionalIntArray() + { + const int rows = 3; + const int cols = 4; + + SafeArrayNative.Create2DIntSafeArray(rows, cols, out int[,] result); + + Assert.Equal(rows, result.GetLength(0)); + Assert.Equal(cols, result.GetLength(1)); + + for (int r = 0; r < rows; r++) + { + for (int c = 0; c < cols; c++) + { + Assert.Equal(r * cols + c, result[r, c]); + } + } + } + + [ConditionalFact(typeof(TestLibrary.PlatformDetection), nameof(TestLibrary.PlatformDetection.IsBuiltInComEnabled))] + public static void MultidimensionalIntArrayRoundTrip() + { + const int rows = 3; + const int cols = 4; + + SafeArrayNative.Create2DIntSafeArray(rows, cols, out int[,] result); + + SafeArrayNative.Verify2DIntSafeArray(result, rows, cols); + } + + [ConditionalFact(typeof(TestLibrary.PlatformDetection), nameof(TestLibrary.PlatformDetection.IsBuiltInComEnabled))] + public static void MultidimensionalBoolArray() + { + const int rows = 2; + const int cols = 3; + + SafeArrayNative.Create2DBoolSafeArray(rows, cols, out bool[,] result); + + Assert.Equal(rows, result.GetLength(0)); + Assert.Equal(cols, result.GetLength(1)); + + for (int r = 0; r < rows; r++) + { + for (int c = 0; c < cols; c++) + { + Assert.Equal((r + c) % 2 == 0, result[r, c]); + } + } + } + + [ConditionalFact(typeof(TestLibrary.PlatformDetection), nameof(TestLibrary.PlatformDetection.IsBuiltInComEnabled))] + public static void MultidimensionalBoolArrayRoundTrip() + { + const int rows = 2; + const int cols = 3; + + SafeArrayNative.Create2DBoolSafeArray(rows, cols, out bool[,] result); + + SafeArrayNative.Verify2DBoolSafeArray(result, rows, cols); + } + + [ConditionalFact(typeof(TestLibrary.PlatformDetection), nameof(TestLibrary.PlatformDetection.IsBuiltInComEnabled))] + public static void MultidimensionalStringArray() + { + const int rows = 2; + const int cols = 3; + + SafeArrayNative.Create2DStringSafeArray(rows, cols, out string[,] result); + + Assert.Equal(rows, result.GetLength(0)); + Assert.Equal(cols, result.GetLength(1)); + + for (int r = 0; r < rows; r++) + { + for (int c = 0; c < cols; c++) + { + Assert.Equal($"{r},{c}", result[r, c]); + } + } + } + + [ConditionalFact(typeof(TestLibrary.PlatformDetection), nameof(TestLibrary.PlatformDetection.IsBuiltInComEnabled))] + public static void MultidimensionalStringArrayRoundTrip() + { + const int rows = 2; + const int cols = 3; + + SafeArrayNative.Create2DStringSafeArray(rows, cols, out string[,] result); + + SafeArrayNative.Verify2DStringSafeArray(result, rows, cols); + } + private static bool XorArray(bool[] values) { bool retVal = false; @@ -221,4 +314,46 @@ out double result [DllImport(nameof(SafeArrayNative), PreserveSig = false)] public static extern void XorBoolArrayInStruct(StructWithSafeArray str, out bool result); + + [DllImport(nameof(SafeArrayNative), PreserveSig = false)] + public static extern void Create2DIntSafeArray( + int rows, + int cols, + [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_I4)] out int[,] result + ); + + [DllImport(nameof(SafeArrayNative), PreserveSig = false)] + public static extern void Verify2DIntSafeArray( + [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_I4)] int[,] array, + int rows, + int cols + ); + + [DllImport(nameof(SafeArrayNative), PreserveSig = false)] + public static extern void Create2DBoolSafeArray( + int rows, + int cols, + [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_BOOL)] out bool[,] result + ); + + [DllImport(nameof(SafeArrayNative), PreserveSig = false)] + public static extern void Verify2DBoolSafeArray( + [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_BOOL)] bool[,] array, + int rows, + int cols + ); + + [DllImport(nameof(SafeArrayNative), PreserveSig = false)] + public static extern void Create2DStringSafeArray( + int rows, + int cols, + [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_BSTR)] out string[,] result + ); + + [DllImport(nameof(SafeArrayNative), PreserveSig = false)] + public static extern void Verify2DStringSafeArray( + [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_BSTR)] string[,] array, + int rows, + int cols + ); } diff --git a/src/tests/Interop/COM/NETClients/IDispatch/Program.cs b/src/tests/Interop/COM/NETClients/IDispatch/Program.cs index e7b7b159ff8560..99f5bc03da463a 100644 --- a/src/tests/Interop/COM/NETClients/IDispatch/Program.cs +++ b/src/tests/Interop/COM/NETClients/IDispatch/Program.cs @@ -7,6 +7,7 @@ namespace NetClient using System; using System.Drawing; using System.Globalization; + using System.Linq; using System.Reflection; using System.Runtime.InteropServices; @@ -346,6 +347,19 @@ static void Validate_ValueCoerce_ReturnToManaged() Assert.Equal("True", dispatchCoerceTesting.BoolToString()); } + static void Validate_Sum_IntArray_SafeArray() + { + var dispatchTesting = new DispatchTesting(); + + int[] data = [1, 2, 3, 4, 5]; + int expectedSum = data.Sum(); + + Console.WriteLine($"Calling {nameof(IDispatchTesting.Sum_IntArray_SafeArray)} ..."); + int sum = dispatchTesting.Sum_IntArray_SafeArray(data); + Console.WriteLine($"Call to {nameof(IDispatchTesting.Sum_IntArray_SafeArray)} complete: sum = {sum}"); + Assert.Equal(expectedSum, sum); + } + static void Validate_GetDispId_Methods() { var dispatchTesting = new DispatchTesting(); @@ -378,6 +392,7 @@ public static int TestEntryPoint() Validate_LCID_Marshaled(); Validate_Enumerator(); Validate_ValueCoerce_ReturnToManaged(); + Validate_Sum_IntArray_SafeArray(); Validate_GetDispId_Methods(); } catch (Exception e) diff --git a/src/tests/Interop/COM/NETServer/DispatchTesting.cs b/src/tests/Interop/COM/NETServer/DispatchTesting.cs index 6258f987b65889..59b9e774b85245 100644 --- a/src/tests/Interop/COM/NETServer/DispatchTesting.cs +++ b/src/tests/Interop/COM/NETServer/DispatchTesting.cs @@ -103,6 +103,17 @@ public object TriggerCustomMarshaler(object objIn, ref object objRef) return ret; } + public int Sum_IntArray_SafeArray(int[] d) + { + int sum = 0; + foreach (int val in d) + { + sum += val; + } + + return sum; + } + [DispId(1000)] public string GetDispIdAsString() { diff --git a/src/tests/Interop/COM/NativeClients/Dispatch/Client.cpp b/src/tests/Interop/COM/NativeClients/Dispatch/Client.cpp index a6253fd07d75c5..22d9aa033d6604 100644 --- a/src/tests/Interop/COM/NativeClients/Dispatch/Client.cpp +++ b/src/tests/Interop/COM/NativeClients/Dispatch/Client.cpp @@ -3,6 +3,8 @@ #include "ClientTests.h" #include +#include +#include #include @@ -13,6 +15,7 @@ void Validate_LCID_Marshaled(); void Validate_Enumerator(); void Validate_ParamCoerce(); void Validate_TriggerCustomMarshaler(); +void Validate_Sum_IntArray_SafeArray(); template struct ComInit @@ -52,6 +55,7 @@ int __cdecl main() Validate_Enumerator(); Validate_ParamCoerce(); Validate_TriggerCustomMarshaler(); + Validate_Sum_IntArray_SafeArray(); } catch (HRESULT hr) { @@ -676,3 +680,70 @@ void Validate_ParamCoerce() VarDecFromI8(int64_t(1) << 32, &V_DECIMAL(&arg)); Validate_ParamCoerce_Exception(dispatchCoerceTesting, lcid, methodId, arg, DISP_E_OVERFLOW); } + +void Validate_Sum_IntArray_SafeArray() +{ + HRESULT hr; + + CoreShimComActivation csact{ W("NETServer"), W("DispatchTesting") }; + + ComSmartPtr dispatchTesting; + THROW_IF_FAILED(::CoCreateInstance(CLSID_DispatchTesting, nullptr, CLSCTX_INPROC, IID_IDispatchTesting, (void**)&dispatchTesting)); + + LPOLESTR methodName = (LPOLESTR)W("Sum_IntArray_SafeArray"); + LCID lcid = MAKELCID(LANG_USER_DEFAULT, SORT_DEFAULT); + DISPID methodId; + + ::wprintf(W("Invoke %s\n"), methodName); + THROW_IF_FAILED(dispatchTesting->GetIDsOfNames( + IID_NULL, + &methodName, + 1, + lcid, + &methodId)); + + const std::array data{ 1, 2, 3, 4, 5 }; + const int expectedSum = std::accumulate(data.begin(), data.end(), 0); + const int count = (int)data.size(); + + SAFEARRAYBOUND bound; + bound.lLbound = 0; + bound.cElements = count; + SAFEARRAY *sa = ::SafeArrayCreate(VT_I4, 1, &bound); + THROW_FAIL_IF_FALSE(sa != nullptr); + + for (LONG i = 0; i < count; ++i) + { + THROW_IF_FAILED(::SafeArrayPutElement(sa, &i, (void*)&data[i])); + } + + DISPPARAMS params; + params.cArgs = 1; + params.rgvarg = new VARIANTARG[params.cArgs]; + params.cNamedArgs = 0; + params.rgdispidNamedArgs = nullptr; + + VariantInit(¶ms.rgvarg[0]); + V_VT(¶ms.rgvarg[0]) = VT_ARRAY | VT_I4; + V_ARRAY(¶ms.rgvarg[0]) = sa; + + VARIANT result; + VariantInit(&result); + + THROW_IF_FAILED(dispatchTesting->Invoke( + methodId, + IID_NULL, + lcid, + DISPATCH_METHOD, + ¶ms, + &result, + nullptr, + nullptr + )); + + THROW_FAIL_IF_FALSE(V_I4(&result) == expectedSum); + + delete[] params.rgvarg; + + ::SafeArrayDestroy(sa); +} diff --git a/src/tests/Interop/COM/NativeServer/DispatchTesting.h b/src/tests/Interop/COM/NativeServer/DispatchTesting.h index f4690df1221728..7164a041ee0600 100644 --- a/src/tests/Interop/COM/NativeServer/DispatchTesting.h +++ b/src/tests/Interop/COM/NativeServer/DispatchTesting.h @@ -184,6 +184,10 @@ class DispatchTesting : public UnknownImpl, public IDispatchTesting V_UNKNOWN(pVarResult) = new Enumerator(10); return S_OK; } + case 8: + { + return Sum_IntArray_SafeArray_Proxy(pDispParams, pVarResult); + } case 1000: { return GetDispIdAsString_Proxy(pVarResult); @@ -283,6 +287,41 @@ class DispatchTesting : public UnknownImpl, public IDispatchTesting return S_OK; } + virtual HRESULT STDMETHODCALLTYPE Sum_IntArray_SafeArray( + /*[in]*/ SAFEARRAY *d, + /*[out,retval]*/ int *pRetVal) + { + if (d == nullptr || pRetVal == nullptr) + return E_POINTER; + + VARTYPE type; + HRESULT hr = ::SafeArrayGetVartype(d, &type); + if (FAILED(hr)) + return hr; + + if (type != VT_I4) + return E_INVALIDARG; + + LONG lowerBound, upperBound; + hr = ::SafeArrayGetLBound(d, 1, &lowerBound); + if (FAILED(hr)) + return hr; + + hr = ::SafeArrayGetUBound(d, 1, &upperBound); + if (FAILED(hr)) + return hr; + + int *data = static_cast(d->pvData); + int result = 0; + for (LONG i = lowerBound; i <= upperBound; ++i) + { + result += data[i - lowerBound]; + } + + *pRetVal = result; + return S_OK; + } + HRESULT STDMETHODCALLTYPE GetDispIdAsString( /* [out,retval] */ BSTR *pRetVal) { @@ -543,6 +582,26 @@ class DispatchTesting : public UnknownImpl, public IDispatchTesting return S_OK; } + HRESULT Sum_IntArray_SafeArray_Proxy(_In_ DISPPARAMS *pDispParams, _Inout_ VARIANT *pVarResult) + { + HRESULT hr; + + size_t expectedArgCount = 1; + RETURN_IF_FAILED(VerifyValues(uint32_t(expectedArgCount), pDispParams->cArgs)); + + if (pVarResult == nullptr) + return E_POINTER; + + size_t argIdx = expectedArgCount - 1; + + VARIANTARG *currArg = NextArg(pDispParams->rgvarg, argIdx); + RETURN_IF_FAILED(VerifyValues(VARENUM(VT_ARRAY | VT_I4), VARENUM(currArg->vt))); + SAFEARRAY *sa = currArg->parray; + + RETURN_IF_FAILED(::VariantChangeType(pVarResult, pVarResult, 0, VT_I4)); + return Sum_IntArray_SafeArray(sa, (int*)&V_I4(pVarResult)); + } + HRESULT GetDispIdAsString_Proxy(_Inout_ VARIANT *pVarResult) { if (pVarResult == nullptr) @@ -591,7 +650,8 @@ const WCHAR * const DispatchTesting::Names[] = W("TriggerException"), W("DoubleHVAValues"), W("PassThroughLCID"), - W("ExplicitGetEnumerator") + W("ExplicitGetEnumerator"), + W("Sum_IntArray_SafeArray") }; const int DispatchTesting::NamesCount = ARRAY_SIZE(DispatchTesting::Names); diff --git a/src/tests/Interop/COM/ServerContracts/Server.Contracts.cs b/src/tests/Interop/COM/ServerContracts/Server.Contracts.cs index a57d5a0dd86746..607f218965864e 100644 --- a/src/tests/Interop/COM/ServerContracts/Server.Contracts.cs +++ b/src/tests/Interop/COM/ServerContracts/Server.Contracts.cs @@ -311,6 +311,8 @@ void DoubleNumeric_ReturnByRef ( [DispId(/*DISPID_NEWENUM*/-4)] System.Collections.IEnumerator GetEnumerator(); + int Sum_IntArray_SafeArray([MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_I4)] int[] d); + // Test matching signatures and different metadata (ie DISPID) [DispId(1000)] diff --git a/src/tests/Interop/COM/ServerContracts/Server.Contracts.h b/src/tests/Interop/COM/ServerContracts/Server.Contracts.h index ef87bb9c920aab..10d64a0588099f 100644 --- a/src/tests/Interop/COM/ServerContracts/Server.Contracts.h +++ b/src/tests/Interop/COM/ServerContracts/Server.Contracts.h @@ -455,6 +455,10 @@ IDispatchTesting : IDispatch virtual HRESULT STDMETHODCALLTYPE ExplicitGetEnumerator( /* [retval][out] */ IUnknown** retval) = 0; + + virtual HRESULT STDMETHODCALLTYPE Sum_IntArray_SafeArray( + /*[in]*/ SAFEARRAY *d, + /*[out,retval]*/ int *pRetVal) = 0; }; struct __declspec(uuid("83AFF8E4-C46A-45DB-9D91-2ADB5164545E")) From 13b9f3782c747d1ebbecb853675d82dbab19d1b6 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 19:38:25 +0000 Subject: [PATCH 071/115] Fix SIGBUS crash on ARMv7: use Unsafe.ReadUnaligned in Frozen.Hashing (#127653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `System.Collections.Frozen.Hashing` was casting `char*` to `uint*` and dereferencing directly, causing unaligned 32-bit reads. On ARMv7 (e.g., Android Unity IL2CPP), the OS does not fixup misaligned accesses, resulting in SIGBUS crashes when calling `ToFrozenSet(StringComparer.Ordinal)` on strings with length ≥ 4. Fixes #127641 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> Co-authored-by: Jan Kotas --- .../Collections/Frozen/String/Hashing.cs | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/Hashing.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/Hashing.cs index 212e2cca3bf624..5966f4e60423d4 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/Hashing.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/Hashing.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Diagnostics; using System.Numerics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -47,27 +48,26 @@ public static unsafe int GetHashCodeOrdinal(ReadOnlySpan s) return (int)(Hash1Start + (hash2 * Factor)); case 4: - hash1 = (BitOperations.RotateLeft(Hash1Start, 5) + Hash1Start) ^ ((uint*)src)[0]; - hash2 = (BitOperations.RotateLeft(Hash1Start, 5) + Hash1Start) ^ ((uint*)src)[1]; + hash1 = (BitOperations.RotateLeft(Hash1Start, 5) + Hash1Start) ^ Unsafe.ReadUnaligned(src); + hash2 = (BitOperations.RotateLeft(Hash1Start, 5) + Hash1Start) ^ Unsafe.ReadUnaligned(src + 2); return (int)(hash1 + (hash2 * Factor)); default: hash1 = Hash1Start; hash2 = hash1; - uint* ptrUInt32 = (uint*)src; + char* ptr = src; while (length >= 4) { - hash1 = (BitOperations.RotateLeft(hash1, 5) + hash1) ^ ptrUInt32[0]; - hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ ptrUInt32[1]; - ptrUInt32 += 2; + hash1 = (BitOperations.RotateLeft(hash1, 5) + hash1) ^ Unsafe.ReadUnaligned(ptr); + hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ Unsafe.ReadUnaligned(ptr + 2); + ptr += 4; length -= 4; } - char* ptrChar = (char*)ptrUInt32; while (length-- > 0) { - hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ *ptrChar++; + hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ *ptr++; } return (int)(hash1 + (hash2 * Factor)); @@ -111,28 +111,27 @@ public static unsafe int GetHashCodeOrdinalIgnoreCaseAscii(ReadOnlySpan s) return (int)(Hash1Start + (hash2 * Factor)); case 4: - hash1 = (BitOperations.RotateLeft(Hash1Start, 5) + Hash1Start) ^ (((uint*)src)[0] | LowercaseUInt32); - hash2 = (BitOperations.RotateLeft(Hash1Start, 5) + Hash1Start) ^ (((uint*)src)[1] | LowercaseUInt32); + hash1 = (BitOperations.RotateLeft(Hash1Start, 5) + Hash1Start) ^ (Unsafe.ReadUnaligned(src) | LowercaseUInt32); + hash2 = (BitOperations.RotateLeft(Hash1Start, 5) + Hash1Start) ^ (Unsafe.ReadUnaligned(src + 2) | LowercaseUInt32); return (int)(hash1 + (hash2 * Factor)); default: hash1 = Hash1Start; hash2 = hash1; - uint* ptrUInt32 = (uint*)src; + char* ptr = src; while (length >= 4) { - hash1 = (BitOperations.RotateLeft(hash1, 5) + hash1) ^ (ptrUInt32[0] | LowercaseUInt32); - hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ (ptrUInt32[1] | LowercaseUInt32); - ptrUInt32 += 2; + hash1 = (BitOperations.RotateLeft(hash1, 5) + hash1) ^ (Unsafe.ReadUnaligned(ptr) | LowercaseUInt32); + hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ (Unsafe.ReadUnaligned(ptr + 2) | LowercaseUInt32); + ptr += 4; length -= 4; } - char* ptrChar = (char*)ptrUInt32; while (length-- > 0) { - hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ (*ptrChar | LowercaseUInt32); - ptrChar++; + hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ (*ptr | LowercaseUInt32); + ptr++; } return (int)(hash1 + (hash2 * Factor)); From e406822cd48745b98a8f3c7480082b2f6203b761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Strehovsk=C3=BD?= Date: Sat, 2 May 2026 05:43:42 +0900 Subject: [PATCH 072/115] Always add ManagedDataDescriptorProvider to compilationRoots (#127643) This just causes `error LNK2001: unresolved external symbol DotNetManagedContractDescriptor` --- src/coreclr/tools/aot/ILCompiler/Program.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler/Program.cs b/src/coreclr/tools/aot/ILCompiler/Program.cs index 2ba003f6a177c5..469a09b95224a7 100644 --- a/src/coreclr/tools/aot/ILCompiler/Program.cs +++ b/src/coreclr/tools/aot/ILCompiler/Program.cs @@ -257,8 +257,7 @@ public int Run() } } - if (Get(_command.EnableDebugInfo)) - compilationRoots.Add(new ManagedDataDescriptorProvider()); + compilationRoots.Add(new ManagedDataDescriptorProvider()); string win32resourcesModule = Get(_command.Win32ResourceModuleName); if (typeSystemContext.Target.IsWindows && !string.IsNullOrEmpty(win32resourcesModule)) From bd9e85e4b37557e649fccc083f33a50acdda2dde Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 21:47:36 +0000 Subject: [PATCH 073/115] Fix `!m_RedirectContextInUse` assert in `RestoreContextSimulated` on win-x86 (#127638) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On x86 Windows without `RtlRestoreContext`, `RestoreContextSimulated` called `HandleThreadAbort()` while `m_RedirectContextInUse` was still `true`. `HandleThreadAbort()` constructs a `ThreadAbortException` by running managed code (resource string loading, etc.), during which a concurrent GC redirect fires `MarkRedirectContextInUse()` → assert `!m_RedirectContextInUse`. **Fix:** Move the `RestoreContextSimulated` call in `RedirectedHandledJITCase` to after the existing `COMPlusCheckForAbort()` block, so both the x86 SEH path and the `RtlRestoreContext` path share a single abort-check code path: Fixes #127637 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- src/coreclr/vm/threadsuspend.cpp | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/coreclr/vm/threadsuspend.cpp b/src/coreclr/vm/threadsuspend.cpp index f265094ce9876a..b2e81e569ea905 100644 --- a/src/coreclr/vm/threadsuspend.cpp +++ b/src/coreclr/vm/threadsuspend.cpp @@ -2586,8 +2586,6 @@ extern "C" PCONTEXT __stdcall GetCurrentSavedRedirectContext() void Thread::RestoreContextSimulated(Thread* pThread, CONTEXT* pCtx, void* pFrame, DWORD dwLastError) { - pThread->HandleThreadAbort(); // Might throw an exception. - // A counter to avoid a nasty case where an // up-stack filter throws another exception // causing our filter to be run again for @@ -2672,16 +2670,6 @@ void __stdcall Thread::RedirectedHandledJITCase(RedirectReason reason) // We will restore the state as it was at the point of redirection // and continue normal execution. -#ifdef TARGET_X86 - if (!g_pfnRtlRestoreContext) - { - RestoreContextSimulated(pThread, pCtx, &frame, dwLastError); - - // we never return to the caller. - UNREACHABLE(); - } -#endif // TARGET_X86 - UINT_PTR uAbortAddr; UINT_PTR uResumePC = (UINT_PTR)GetIP(pCtx); CopyOSContext(pThread->m_OSContext, pCtx); @@ -2707,6 +2695,16 @@ void __stdcall Thread::RedirectedHandledJITCase(RedirectReason reason) SetIP(pCtx, uAbortAddr); } +#ifdef TARGET_X86 + if (!g_pfnRtlRestoreContext) + { + RestoreContextSimulated(pThread, pCtx, &frame, dwLastError); + + // we never return to the caller. + UNREACHABLE(); + } +#endif // TARGET_X86 + // Unlink the frame in preparation for resuming in managed code frame.Pop(); From ad4354d626bd060d76a6291e244190fc53d0f3fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Strehovsk=C3=BD?= Date: Sat, 2 May 2026 10:52:38 +0900 Subject: [PATCH 074/115] Fix NativeAOT GC roots after universal transition (#127640) When a GC stack walk starts from a hijacked universal-transition frame, the iterator unwinds through the thunk and then yields the managed caller at the post-call IP. That caller is not actually the active frame yet, so reporting scratch registers from its post-call GC info can expose stale thunk state. In the failing System.Linq.Tests NativeAOT case, the precise GC root came from REGDISPLAY.pRax while CoffNativeCodeManager::EnumGcRefs was called with isActiveStackFrame=true. RAX contained the resolved interface dispatch target, System.Linq.Enumerable.Iterator.System.Collections.IEnumerator.get_Current, so object validation treated a code pointer as a GC object and fail-fast asserted. Clear ActiveStackFrame after unwinding the non-EH universal-transition thunk sequence so the yielded managed caller still reports its non-scratch roots and the conservative thunk range, but does not report scratch registers until the thunk has completed. Validation: before the fix, the parallel System.Linq.Tests NativeAOT stress loop completed 69 runs with 63 successes and 6 fail-fast crashes; sampled dumps all showed the same pRax code-pointer root. After rebuilding with this fix, the same loop ran for 612.3 seconds at parallelism 4 and completed 132 runs with 132 successes, 0 crashes, and 0 test failures. --- src/coreclr/nativeaot/Runtime/StackFrameIterator.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/coreclr/nativeaot/Runtime/StackFrameIterator.cpp b/src/coreclr/nativeaot/Runtime/StackFrameIterator.cpp index 928e16439e5bad..f90c2dd32dddd1 100644 --- a/src/coreclr/nativeaot/Runtime/StackFrameIterator.cpp +++ b/src/coreclr/nativeaot/Runtime/StackFrameIterator.cpp @@ -2072,6 +2072,10 @@ void StackFrameIterator::UnwindNonEHThunkSequence() // The iterator has reached the next managed frame. Publish the computed lower bound value. ASSERT(m_pConservativeStackRangeLowerBound == NULL); m_pConservativeStackRangeLowerBound = pLowestLowerBound; + + // The active frame was the thunk we just unwound through, not the managed caller. Do not + // report scratch registers from the caller's post-call GC state until the thunk has completed. + m_dwFlags &= ~ActiveStackFrame; } // This function is called immediately before a given frame is yielded from the iterator From eb1e2c3795988be578051fb874e7e7daacfadad6 Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Fri, 1 May 2026 19:23:26 -0700 Subject: [PATCH 075/115] JIT: make funclet order correspond to EH clause order (#127590) For Wasm the JIT host will need to match up funclets with EH clauses without relying on the offset data in the EH clause. To allow this, set things up so that the host can infer which funclets are associated with an EH clause by making the ordering of EH clauses and the order of funclets (reported via unwind info) correspond. Note this is not 1-1 as EH clauses with filters will inspire two funclets. In those cases the JIT will always put the filter funclet before the catch funclet. --- src/coreclr/jit/codegencommon.cpp | 74 +++++++++++------------ src/coreclr/jit/compiler.h | 8 ++- src/coreclr/jit/fgbasic.cpp | 4 -- src/coreclr/jit/fgwasm.cpp | 69 +++++++++++++-------- src/coreclr/jit/flowgraph.cpp | 99 +++++++++++++++++++++---------- 5 files changed, 152 insertions(+), 102 deletions(-) diff --git a/src/coreclr/jit/codegencommon.cpp b/src/coreclr/jit/codegencommon.cpp index 041aa67e3f5c99..5ebd0dd3a83a59 100644 --- a/src/coreclr/jit/codegencommon.cpp +++ b/src/coreclr/jit/codegencommon.cpp @@ -2542,14 +2542,6 @@ void CodeGen::genReportEH() if (m_compiler->opts.dspEHTable) { printf("*************** EH table for %s\n", m_compiler->info.compFullName); - } -#endif // DEBUG - - unsigned XTnum; - -#ifdef DEBUG - if (m_compiler->opts.dspEHTable) - { printf("%d EH table entries\n", m_compiler->compHndBBtabCount); } #endif // DEBUG @@ -2561,10 +2553,11 @@ void CodeGen::genReportEH() EHClauseInfo* clauses = new (m_compiler, CMK_Codegen) EHClauseInfo[m_compiler->compHndBBtabCount]; // Set up EH clause table, but don't report anything to the VM, yet. - XTnum = 0; - for (EHblkDsc* const HBtab : EHClauses(m_compiler)) + // + for (unsigned int XTnum = 0; XTnum < m_compiler->compHndBBtabCount; XTnum++) { - UNATIVE_OFFSET tryBeg, tryEnd, hndBeg, hndEnd, hndTyp; + EHblkDsc* const HBtab = &m_compiler->compHndBBtab[XTnum]; + UNATIVE_OFFSET tryBeg, tryEnd, hndBeg, hndEnd, hndTyp; tryBeg = m_compiler->ehCodeOffset(HBtab->ebdTryBeg); hndBeg = m_compiler->ehCodeOffset(HBtab->ebdHndBeg); @@ -2593,7 +2586,10 @@ void CodeGen::genReportEH() clause.TryLength = tryEnd; clause.HandlerOffset = hndBeg; clause.HandlerLength = hndEnd; - clauses[XTnum++] = {clause, HBtab}; + + unsigned const vmIndex = m_compiler->compEHTabOrderToVMClauseOrder[XTnum]; + + clauses[vmIndex] = {clause, HBtab}; } genReportEHClauses(clauses); @@ -2605,45 +2601,43 @@ void CodeGen::genReportEH() // genReportEH: create and report EH info to the VM // // Arguments: -// clauses -- eh clause data to report +// clauses -- eh clause data to fill in and report // void CodeGen::genReportEHClauses(EHClauseInfo* clauses) { - // The JIT's ordering of EH clauses does not guarantee that clauses covering the same try region are contiguous. - // We need this property to hold true so the CORINFO_EH_CLAUSE_SAMETRY flag is accurate. - jitstd::sort(clauses, clauses + m_compiler->compHndBBtabCount, - [this](const EHClauseInfo& left, const EHClauseInfo& right) { - const unsigned short leftTryIndex = left.HBtab->ebdTryBeg->bbTryIndex; - const unsigned short rightTryIndex = right.HBtab->ebdTryBeg->bbTryIndex; + // Now, report EH clauses to the VM + // + INDEBUG(unsigned lastFuncletIndex = 0); + + for (unsigned vmIndex = 0; vmIndex < m_compiler->compHndBBtabCount; vmIndex++) + { + CORINFO_EH_CLAUSE& clause = clauses[vmIndex].clause; + unsigned const XTnum = m_compiler->compVMClauseOrderToEHTabOrder[vmIndex]; + EHblkDsc* const HBtab = &m_compiler->compHndBBtab[XTnum]; - if (leftTryIndex == rightTryIndex) +#ifdef DEBUG + // We should be seeing funclet numbers increase predictably + // as we go through the EH table in VM order. + // + if (HBtab->HasFilter()) { - // We have two clauses mapped to the same try region. - // Make sure we report the clause with the smaller index first. - const ptrdiff_t leftIndex = left.HBtab - this->m_compiler->compHndBBtab; - const ptrdiff_t rightIndex = right.HBtab - this->m_compiler->compHndBBtab; - return leftIndex < rightIndex; + assert(HBtab->ebdFuncIndex == (lastFuncletIndex + 2)); } + else + { + assert(HBtab->ebdFuncIndex == (lastFuncletIndex + 1)); + } + lastFuncletIndex = HBtab->ebdFuncIndex; +#endif - return leftTryIndex < rightTryIndex; - }); - - unsigned XTnum; - - // Now, report EH clauses to the VM in order of increasing try region index. - for (XTnum = 0; XTnum < m_compiler->compHndBBtabCount; XTnum++) - { - CORINFO_EH_CLAUSE& clause = clauses[XTnum].clause; - EHblkDsc* const HBtab = clauses[XTnum].HBtab; - - if (XTnum > 0) + if (vmIndex > 0) { // CORINFO_EH_CLAUSE_SAMETRY flag means that the current clause covers same // try block as the previous one. The runtime cannot reliably infer this information from // native code offsets because of different try blocks can have same offsets. Alternative // solution to this problem would be inserting extra nops to ensure that different try // blocks have different offsets. - if (EHblkDsc::ebdIsSameTry(HBtab, clauses[XTnum - 1].HBtab)) + if (EHblkDsc::ebdIsSameTry(HBtab, clauses[vmIndex - 1].HBtab)) { // The SAMETRY bit should only be set on catch clauses. This is ensured in IL, where only 'catch' is // allowed to be mutually-protect. E.g., the C# "try {} catch {} catch {} finally {}" actually exists in @@ -2653,10 +2647,8 @@ void CodeGen::genReportEHClauses(EHClauseInfo* clauses) } } - m_compiler->eeSetEHinfo(XTnum, &clause); + m_compiler->eeSetEHinfo(vmIndex, &clause); } - - assert(XTnum == m_compiler->compHndBBtabCount); } #ifndef TARGET_WASM diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index f86c520837532c..7562b34fefb930 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -9459,9 +9459,11 @@ class Compiler } // Things that MAY belong either in CodeGen or CodeGenContext - FuncInfoDsc* compFuncInfos; - unsigned short compCurrFuncIdx; - unsigned short compFuncInfoCount; + FuncInfoDsc* compFuncInfos; + unsigned short compCurrFuncIdx; + unsigned short compFuncInfoCount; + unsigned short* compVMClauseOrderToEHTabOrder; + unsigned short* compEHTabOrderToVMClauseOrder; unsigned short compFuncCount() { diff --git a/src/coreclr/jit/fgbasic.cpp b/src/coreclr/jit/fgbasic.cpp index 1f3def222fff24..b208a2ab990c96 100644 --- a/src/coreclr/jit/fgbasic.cpp +++ b/src/coreclr/jit/fgbasic.cpp @@ -5380,10 +5380,6 @@ void Compiler::fgMoveBlocksAfter(BasicBlock* bStart, BasicBlock* bEnd, BasicBloc // Return Value: // The last block that was relocated, or nullptr on failure. // -// Notes: -// This function can invalidate all pointers into the EH table, as well as -// change the size of the EH table! -// BasicBlock* Compiler::fgRelocateEHRange(unsigned regionIndex, FG_RELOCATE_TYPE relocateType) { INDEBUG(const char* reason = "None";) diff --git a/src/coreclr/jit/fgwasm.cpp b/src/coreclr/jit/fgwasm.cpp index 3bca9c2052ac55..66fb058fc935f3 100644 --- a/src/coreclr/jit/fgwasm.cpp +++ b/src/coreclr/jit/fgwasm.cpp @@ -2478,8 +2478,11 @@ PhaseStatus Compiler::fgWasmVirtualIP() unsigned virtualIP = 0; unsigned updatesAdded = 0; - // Prefill the EH data fields that are not dependent on - // the Virtual IP. + // Prefill the EH data fields that are not dependent on the Virtual IP. + // + // Note this and subsequent accesses to the clause info must respect + // the order in which clauses will be reported to the VM, not the order + // in the compHndBBtab. // EHClauseInfo* clauses = nullptr; @@ -2487,9 +2490,10 @@ PhaseStatus Compiler::fgWasmVirtualIP() { clauses = new (this, CMK_WasmEH) EHClauseInfo[compHndBBtabCount]; - for (EHblkDsc* const dsc : EHClauses(this)) + for (unsigned XTnum = 0; XTnum < compHndBBtabCount; XTnum++) { - const unsigned index = ehGetIndex(dsc); + EHblkDsc* const dsc = ehGetDsc(XTnum); + CORINFO_EH_CLAUSE clause; clause.ClassToken = dsc->HasFilter() ? 0 : dsc->ebdTyp; clause.Flags = ToCORINFO_EH_CLAUSE_FLAGS(dsc->ebdHandlerType); @@ -2497,7 +2501,10 @@ PhaseStatus Compiler::fgWasmVirtualIP() clause.TryLength = 0; clause.HandlerOffset = 0; clause.HandlerLength = 0; - clauses[index] = {clause, dsc}; + + unsigned const vmIndex = compEHTabOrderToVMClauseOrder[XTnum]; + + clauses[vmIndex] = {clause, dsc}; } } @@ -2588,7 +2595,10 @@ PhaseStatus Compiler::fgWasmVirtualIP() if ((tryDsc != nullptr) && (block == tryDsc->ebdTryBeg)) { virtualIP++; - clauses[block->getTryIndex()].clause.TryOffset = virtualIP; + + const unsigned tryIndex = block->getTryIndex(); + const unsigned clauseIndex = compEHTabOrderToVMClauseOrder[tryIndex]; + clauses[clauseIndex].clause.TryOffset = virtualIP; // Multiple try regions can begin at the same block. // Update all of their offsets here. @@ -2600,8 +2610,9 @@ PhaseStatus Compiler::fgWasmVirtualIP() // These should be mutual-protect trys. // assert(EHblkDsc::ebdIsSameTry(tryDsc, enclosingDsc)); - const unsigned enclosingIndex = ehGetIndex(enclosingDsc); - clauses[enclosingIndex].clause.TryOffset = virtualIP; + const unsigned enclosingTryIndex = ehGetIndex(enclosingDsc); + const unsigned enclosingClauseIndex = compEHTabOrderToVMClauseOrder[enclosingTryIndex]; + clauses[enclosingClauseIndex].clause.TryOffset = virtualIP; } } } @@ -2609,7 +2620,9 @@ PhaseStatus Compiler::fgWasmVirtualIP() if ((hndDsc != nullptr) && (block == hndDsc->ebdHndBeg)) { virtualIP++; - clauses[block->getHndIndex()].clause.HandlerOffset = virtualIP; + const unsigned hndIndex = block->getHndIndex(); + const unsigned clauseIndex = compEHTabOrderToVMClauseOrder[hndIndex]; + clauses[clauseIndex].clause.HandlerOffset = virtualIP; } if ((hndDsc != nullptr) && hndDsc->HasFilter() && (block == hndDsc->ebdFilter)) @@ -2617,7 +2630,9 @@ PhaseStatus Compiler::fgWasmVirtualIP() virtualIP++; // For filters we store the offset in the class token field. // - clauses[block->getHndIndex()].clause.ClassToken = virtualIP; + const unsigned filterIndex = block->getHndIndex(); + const unsigned clauseIndex = compEHTabOrderToVMClauseOrder[filterIndex]; + clauses[clauseIndex].clause.ClassToken = virtualIP; } // For now, just refresh the stack Virtual IP at the start of each non-empty @@ -2635,8 +2650,10 @@ PhaseStatus Compiler::fgWasmVirtualIP() if ((tryDsc != nullptr) && (block == tryDsc->ebdTryLast)) { virtualIP++; - assert(virtualIP > clauses[block->getTryIndex()].clause.TryOffset); - clauses[block->getTryIndex()].clause.TryLength = virtualIP; + const unsigned tryIndex = block->getTryIndex(); + const unsigned clauseIndex = compEHTabOrderToVMClauseOrder[tryIndex]; + assert(virtualIP > clauses[clauseIndex].clause.TryOffset); + clauses[clauseIndex].clause.TryLength = virtualIP; // Multiple try regions can end at the same block. // Update all of their extents here. @@ -2647,9 +2664,10 @@ PhaseStatus Compiler::fgWasmVirtualIP() { if (enclosingDsc->ebdTryLast == block) { - const unsigned enclosingIndex = ehGetIndex(enclosingDsc); - assert(virtualIP > clauses[enclosingIndex].clause.TryOffset); - clauses[enclosingIndex].clause.TryLength = virtualIP; + const unsigned enclosingTryIndex = ehGetIndex(enclosingDsc); + const unsigned enclosingClauseIndex = compEHTabOrderToVMClauseOrder[enclosingTryIndex]; + assert(virtualIP > clauses[enclosingClauseIndex].clause.TryOffset); + clauses[enclosingClauseIndex].clause.TryLength = virtualIP; } } } @@ -2657,8 +2675,10 @@ PhaseStatus Compiler::fgWasmVirtualIP() if ((hndDsc != nullptr) && (block == hndDsc->ebdHndLast)) { virtualIP++; - assert(virtualIP > clauses[block->getHndIndex()].clause.HandlerOffset); - clauses[block->getHndIndex()].clause.HandlerLength = virtualIP; + const unsigned hndIndex = block->getHndIndex(); + const unsigned clauseIndex = compEHTabOrderToVMClauseOrder[hndIndex]; + assert(virtualIP > clauses[clauseIndex].clause.HandlerOffset); + clauses[clauseIndex].clause.HandlerLength = virtualIP; } if ((hndDsc != nullptr) && hndDsc->HasFilter() && (block->Next() == hndDsc->ebdHndBeg)) @@ -2677,19 +2697,20 @@ PhaseStatus Compiler::fgWasmVirtualIP() JITDUMP("EH virtual IP ranges\n"); for (EHblkDsc* const dsc : EHClauses(this)) { - const unsigned index = ehGetIndex(dsc); + const unsigned index = ehGetIndex(dsc); + const unsigned clauseIndex = compEHTabOrderToVMClauseOrder[index]; - JITDUMP("EH#%02u: Try [%04u..%04u)", index, clauses[index].clause.TryOffset, - clauses[index].clause.TryLength); + JITDUMP("EH#%02u: Try [%04u..%04u)", index, clauses[clauseIndex].clause.TryOffset, + clauses[clauseIndex].clause.TryLength); if (dsc->HasFilter()) { - JITDUMP(" Filter [%04u..%04u)\n", clauses[index].clause.ClassToken, - clauses[index].clause.HandlerOffset); + JITDUMP(" Filter [%04u..%04u)\n", clauses[clauseIndex].clause.ClassToken, + clauses[clauseIndex].clause.HandlerOffset); } - JITDUMP(" Handler [%04u..%04u)\n", clauses[index].clause.HandlerOffset, - clauses[index].clause.HandlerLength); + JITDUMP(" Handler [%04u..%04u)\n", clauses[clauseIndex].clause.HandlerOffset, + clauses[clauseIndex].clause.HandlerLength); } } #endif // DEBUG diff --git a/src/coreclr/jit/flowgraph.cpp b/src/coreclr/jit/flowgraph.cpp index 654cb4a75c83f5..7fa0f4a3f7bf02 100644 --- a/src/coreclr/jit/flowgraph.cpp +++ b/src/coreclr/jit/flowgraph.cpp @@ -7,7 +7,8 @@ #pragma hdrstop #endif -#include "lower.h" // for LowerRange() +#include "lower.h" // for LowerRange() +#include "jitstd/algorithm.h" // for sort() // Flowgraph Miscellany @@ -3132,10 +3133,6 @@ PhaseStatus Compiler::fgCreateFunclets() { assert(!fgFuncletsCreated); - fgCreateFuncletPrologBlocks(); - - unsigned XTnum; - EHblkDsc* HBtab; const unsigned int funcCnt = ehFuncletCount() + 1; if (!FitsIn(funcCnt)) @@ -3145,8 +3142,6 @@ PhaseStatus Compiler::fgCreateFunclets() FuncInfoDsc* funcInfo = new (this, CMK_BasicBlock) FuncInfoDsc[funcCnt]; - unsigned short funcIdx; - // Setup the root FuncInfoDsc and prepare to start associating // FuncInfoDsc's with their corresponding EH region memset((void*)funcInfo, 0, funcCnt * sizeof(FuncInfoDsc)); @@ -3163,43 +3158,87 @@ PhaseStatus Compiler::fgCreateFunclets() } #endif assert(funcInfo[0].funKind == FUNC_ROOT); - funcIdx = 1; - // Because we iterate from the top to the bottom of the compHndBBtab array, we are iterating - // from most nested (innermost) to least nested (outermost) EH region. It would be reasonable - // to iterate in the opposite order, but the order of funclets shouldn't matter. - // - // We move every handler region to the end of the function: each handler will become a funclet. - // - // Note that fgRelocateEHRange() can add new entries to the EH table. However, they will always - // be added *after* the current index, so our iteration here is not invalidated. - // It *can* invalidate the compHndBBtab pointer itself, though, if it gets reallocated! + unsigned short* vmClauseOrderToEHTabOrder = nullptr; + unsigned short* ehTabOrderToVMClauseOrder = nullptr; + unsigned short funcIdx = 1; - for (XTnum = 0; XTnum < compHndBBtabCount; XTnum++) + if (compHndBBtabCount > 0) { - HBtab = ehGetDsc(XTnum); // must re-compute this every loop, since fgRelocateEHRange changes the table - if (HBtab->HasFilter()) + fgCreateFuncletPrologBlocks(); + + // We want to have the funclet order match the order of emission of EH clauses to the runtime. + // We cannot change the order of entries in the JIT's EH table, as there are many places + // in the JIT that depend on the current ordering. + // + // So, build mappings from vm order to EH table order and vice versa. + // + vmClauseOrderToEHTabOrder = new (this, CMK_BasicBlock) unsigned short[compHndBBtabCount]; + ehTabOrderToVMClauseOrder = new (this, CMK_BasicBlock) unsigned short[compHndBBtabCount]; + + unsigned XTnum; + + for (XTnum = 0; XTnum < compHndBBtabCount; XTnum++) + { + vmClauseOrderToEHTabOrder[XTnum] = (unsigned short)XTnum; + } + + // The JIT's ordering of EH clauses does not guarantee that clauses covering the same try region are contiguous. + // We need this property to hold true so the CORINFO_EH_CLAUSE_SAMETRY flag is accurate. + // + jitstd::sort(vmClauseOrderToEHTabOrder, vmClauseOrderToEHTabOrder + compHndBBtabCount, + [this](const unsigned short& leftIndex, const unsigned short& rightIndex) { + const unsigned short leftTryIndex = ehGetDsc(leftIndex)->ebdTryBeg->bbTryIndex; + const unsigned short rightTryIndex = ehGetDsc(rightIndex)->ebdTryBeg->bbTryIndex; + + if (leftTryIndex == rightTryIndex) + { + // We have two clauses mapped to the same try region. + // Make sure we order the clause with the smaller index first. + return leftIndex < rightIndex; + } + + return leftTryIndex < rightTryIndex; + }); + + // We move every handler region to the end of the function: each handler will become a funclet. + // + for (unsigned orderNum = 0; orderNum < compHndBBtabCount; orderNum++) { + XTnum = vmClauseOrderToEHTabOrder[orderNum]; + EHblkDsc* const HBtab = ehGetDsc(XTnum); + if (HBtab->HasFilter()) + { + assert(funcIdx < funcCnt); + funcInfo[funcIdx].funKind = FUNC_FILTER; + funcInfo[funcIdx].funEHIndex = (unsigned short)XTnum; + funcIdx++; + } assert(funcIdx < funcCnt); - funcInfo[funcIdx].funKind = FUNC_FILTER; + funcInfo[funcIdx].funKind = FUNC_HANDLER; funcInfo[funcIdx].funEHIndex = (unsigned short)XTnum; + HBtab->ebdFuncIndex = funcIdx; funcIdx++; + fgRelocateEHRange(XTnum, FG_RELOCATE_HANDLER); + } + + // Fill in the inverse mapping. + // + for (unsigned i = 0; i < compHndBBtabCount; i++) + { + ehTabOrderToVMClauseOrder[vmClauseOrderToEHTabOrder[i]] = (unsigned short)i; } - assert(funcIdx < funcCnt); - funcInfo[funcIdx].funKind = FUNC_HANDLER; - funcInfo[funcIdx].funEHIndex = (unsigned short)XTnum; - HBtab->ebdFuncIndex = funcIdx; - funcIdx++; - fgRelocateEHRange(XTnum, FG_RELOCATE_HANDLER); } // We better have populated all of them by now assert(funcIdx == funcCnt); // Publish - compCurrFuncIdx = 0; - compFuncInfos = funcInfo; - compFuncInfoCount = (unsigned short)funcCnt; + compCurrFuncIdx = 0; + compFuncInfos = funcInfo; + compFuncInfoCount = (unsigned short)funcCnt; + compVMClauseOrderToEHTabOrder = vmClauseOrderToEHTabOrder; + compEHTabOrderToVMClauseOrder = ehTabOrderToVMClauseOrder; fgFuncletsCreated = true; From b74595701421282895300b139f2064745c583f7d Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 May 2026 11:57:04 +0900 Subject: [PATCH 076/115] Fix NativeAOT hex config parser to handle `0x`/`0X` prefix (#127644) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NativeAOT's `RhConfig::Environment::TryGetIntegerValue` had a hand-rolled hex parser that rejected the `0x`/`0X` prefix — returning a parse error when it encountered `x`. This meant env vars like `DOTNET_GCHeapHardLimit=0xC0000000` silently failed to parse, leaving the hard limit unset. With `GCLargePages=2` also set, the GC would then return `CLR_E_GC_LARGE_PAGE_MISSING_HARD_LIMIT` and fail initialization. CoreCLR's equivalent uses `strtoul(..., 16)` which handles the prefix natively. ## Description - **`src/coreclr/nativeaot/Runtime/RhConfig.cpp`** — In `TryGetIntegerValue`, skip a leading `0x`/`0X` prefix when parsing in hex mode, before entering the digit loop. Additionally, return `false` (parse error) when the value is exactly `"0x"` or `"0X"` with no hex digits following the prefix, matching CoreCLR's `strtoul` behavior: ```cpp uint32_t startIndex = 0; if (!decimal && cchResult >= 2 && buffer[0] == '0' && (buffer[1] == 'x' || buffer[1] == 'X')) { startIndex = 2; if (startIndex == cchResult) return false; // parse error - hex prefix without any digits } for (uint32_t i = startIndex; i < cchResult; i++) ``` This aligns NativeAOT's config parsing with CoreCLR's `strtoul`-based behavior and fixes the `Collect_Aggressive_LargePages` test failure under NativeAOT.
Original prompt ## Problem NativeAOT's `RhConfig::Environment::TryGetIntegerValue` in `src/coreclr/nativeaot/Runtime/RhConfig.cpp` uses a hand-rolled hex parser that does not handle the `0x` or `0X` prefix. This causes config values like `DOTNET_GCHeapHardLimit=0xC0000000` to fail to parse, because when the parser encounters the `x` character it returns `false` (parse error). CoreCLR's equivalent code (`CLRConfigNoCache::TryAsInteger` in `src/coreclr/inc/clrconfignocache.h`) uses `strtoul(_value, &endPtr, radix)` which natively handles the `0x` prefix when radix is 16. This causes the test `Collect_Aggressive_LargePages` added in PR #127290 to fail under NativeAOT: the `GCHeapHardLimit` fails to parse, so no hard limit is set, but `GCLargePages=2` succeeds → the GC returns `CLR_E_GC_LARGE_PAGE_MISSING_HARD_LIMIT` and the process exits with -1. ## Fix In `src/coreclr/nativeaot/Runtime/RhConfig.cpp`, in the `TryGetIntegerValue` method, when parsing in hex mode (i.e., `decimal` is false), skip a leading `0x` or `0X` prefix before entering the digit-parsing loop. This matches the behavior of `strtoul` with radix 16 that CoreCLR uses. Specifically, after reading the environment variable into `buffer` and before the parsing loop, add: ```cpp uint32_t startIndex = 0; if (!decimal && cchResult >= 2 && buffer[0] == '0' && (buffer[1] == 'x' || buffer[1] == 'X')) { startIndex = 2; } ``` Then change the loop from `for (uint32_t i = 0; ...)` to `for (uint32_t i = startIndex; ...)`. The following is the prior conversation context from the user's chat exploration (may be truncated): User: ``` 16:22:36.657 Running test: GC\API\GC\Collect_Aggressive_LargePages\Collect_Aggressive_LargePages.cmd Return code: 1 Raw output file: C:\h\w\B1EC0A05\w\B5C209C7\uploads\API\GC\Collect_Aggressive_LargePages\output.txt Raw output: BEGIN EXECUTION call C:\h\w\B1EC0A05\p\nativeaottest.cmd C:\h\w\B1EC0A05\w\B5C209C7\e\GC\API\GC\Collect_Aggressive_LargePages\ Collect_Aggressive_LargePages.dll Expected: 100 Actual: -1 END EXECUTION - FAILED FAILED Test Harness Exitcode is : 1 To run the test: Set up CORE_ROOT and run. > C:\h\w\B1EC0A05\w\B5C209C7\e\GC\GC\native\..\..\API\GC\Collect_Aggressive_LargePages\Collect_Aggressive_LargePages.cmd Xunit.Sdk.TrueException: Return code: 1 Raw output file: C:\h\w\B1EC0A05\w\B5C209C7\uploads\API\GC\Collect_Aggressive_LargePages\output.txt Raw output: BEGIN EXECUTION call C:\h\w\B1EC0A05\p\nativeaottest.cmd C:\h\w\B1EC0A05\w\B5C209C7\e\GC\API\GC\Collect_Aggressive_LargePages\ Collect_Aggressive_LargePages.dll Expected: 100 Actual: -1 END EXECUTION - FAILED FAILED Test Harness Exitcode is : 1 To run the test: Set up CORE_ROOT and run. > C:\h\w\B1EC0A05\w\B5C209C7\e\GC\GC\native\..\..\API\GC\Collect_Aggressive_LargePages\Collect_Aggressive_LargePages.cmd at Xunit.Assert.True(Nullable`1, String) at TestLibrary.OutOfProcessTest.RunOutOfProcessTest(String, String) at Program.<
$>g__TestExecutor26|0_27(StreamWriter, StreamWriter, Program.<>c__DisplayClass0_0&) 16:22:36.755 Failed test: GC\API\GC\Collect_Aggressive_LargePages\Collect_Aggressive_LargePages.cmd ``` the test added here is failing with native AOT. why? Assistant: The test fails on NativeAOT because **NativeAOT's config reader cannot parse the `0x` prefix** in the `DOTNET_GCHeapHardLimit=0xC0000000` environment variable. Looking at NativeAOT's `RhConfig::Environment::TryGetIntegerValue` in: ```c++ name=src/coreclr/nativeaot/Runtime/RhConfig.cpp url=https://github.com/dotnet/runtime/blob/bfaabae213cf02d343846d253b7149a03819680d/src/coreclr/nativeaot/Runtime/RhConfig.cpp#L58-L84 uint64_t uiResult = 0; for (uint32_t i = 0; i < cchResult; i++) { TCHAR ch = buffer[i]; // ... else { uiResult *= 16; if ((ch >= '0') && (ch <= '9')) uiResult += ch - '0'; else if ((ch >= 'a') && (ch <= 'f')) uiResult += (ch - 'a') + 10; else if ((ch >= 'A') && (ch <= 'F')) uiResult += (ch - 'A') + 10; else return false; // parse error <-- 'x' hits this } } ``` When the value is `0xC0000000`, the parser reads hex digits character-by-character. It handles `0`, but then encounters `x`, which isn't a valid hex digit — so it **returns `false`** (parse error). This means `GCHeapHardLimit` is never set. **The cascade:** 1. `DOTNET_GCHeapHardLimit=0xC0000000` fails to parse → no hard limit is set 2. `DOTNET_GCLargePages=2` is read successfully (it's a simple `2`, valid hex) → `use_large_pages_p = true` 3. The GC hits this check in [`interface.cpp`](https://github.com/dotnet/runtime/blob/bfaabae213cf02d343846d253b7149a03819680d/src/coreclr/gc/interface.cpp#L276-L278): `if ((!gc_heap::heap_hard_limit) && gc_heap::use_large_pages_p)` → returns `CLR_E_GC_LARGE_PAGE_MISSING_HARD_LIMIT` 4. GC initialization fails →...
*This pull request was created from Copilot chat.* > --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com> Co-authored-by: Michal Strehovský --- src/coreclr/nativeaot/Runtime/RhConfig.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/coreclr/nativeaot/Runtime/RhConfig.cpp b/src/coreclr/nativeaot/Runtime/RhConfig.cpp index af6d130bd7dde9..283fe09e3b8fd3 100644 --- a/src/coreclr/nativeaot/Runtime/RhConfig.cpp +++ b/src/coreclr/nativeaot/Runtime/RhConfig.cpp @@ -56,7 +56,14 @@ bool RhConfig::Environment::TryGetIntegerValue(const char* name, uint64_t* value // Environment variable was set. Convert it to an integer. uint64_t uiResult = 0; - for (uint32_t i = 0; i < cchResult; i++) + uint32_t startIndex = 0; + if (!decimal && cchResult >= 2 && buffer[0] == '0' && (buffer[1] == 'x' || buffer[1] == 'X')) + { + startIndex = 2; + if (startIndex == cchResult) + return false; // parse error - hex prefix without any digits + } + for (uint32_t i = startIndex; i < cchResult; i++) { TCHAR ch = buffer[i]; From fcfb4aedc05ea13f66c960bfcf509b62326d930a Mon Sep 17 00:00:00 2001 From: Barbara Rosiak <76071368+barosiak@users.noreply.github.com> Date: Fri, 1 May 2026 22:07:44 -0700 Subject: [PATCH 077/115] Remove DacDbi GetAppDomainObject API from native and cDAC (#127673) GetRawExposedObjectHandleForDebugger() always returned NULL, making GetAppDomainObject a no-op. This pr removes it from all layers: - IDacDbiInterface (abstract interface + IDL) - DacDbiInterfaceImpl (native implementation) - cDAC managed interface and implementation - AppDomain::GetRawExposedObjectHandleForDebugger (sole consumer) - CordbAppDomain::GetObject call site (simplified, since deletion created new dead code) --------- Co-authored-by: Rachel Jarvi --- src/coreclr/debug/daccess/dacdbiimpl.cpp | 20 ------------------ src/coreclr/debug/daccess/dacdbiimpl.h | 3 --- src/coreclr/debug/di/rsappdomain.cpp | 21 ++++--------------- src/coreclr/debug/inc/dacdbiinterface.h | 17 --------------- src/coreclr/inc/dacdbi.idl | 1 - src/coreclr/vm/appdomain.hpp | 3 --- .../Dbi/DacDbiImpl.cs | 3 --- .../Dbi/IDacDbiInterface.cs | 3 --- 8 files changed, 4 insertions(+), 67 deletions(-) diff --git a/src/coreclr/debug/daccess/dacdbiimpl.cpp b/src/coreclr/debug/daccess/dacdbiimpl.cpp index dfb417762d77d8..2785108723ebba 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.cpp +++ b/src/coreclr/debug/daccess/dacdbiimpl.cpp @@ -664,26 +664,6 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetAppDomainId(VMPTR_AppDomain vm return hr; } -// Get the managed AppDomain object for an AppDomain. -HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetAppDomainObject(VMPTR_AppDomain vmAppDomain, OUT VMPTR_OBJECTHANDLE * pRetVal) -{ - DD_ENTER_MAY_THROW; - - HRESULT hr = S_OK; - EX_TRY - { - - AppDomain* pAppDomain = vmAppDomain.GetDacPtr(); - OBJECTHANDLE hAppDomainManagedObject = pAppDomain->GetRawExposedObjectHandleForDebugger(); - VMPTR_OBJECTHANDLE vmObj = VMPTR_OBJECTHANDLE::NullPtr(); - vmObj.SetDacTargetPtr(hAppDomainManagedObject); - *pRetVal = vmObj; - - } - EX_CATCH_HRESULT(hr); - return hr; -} - // Get the full AD friendly name for the given EE AppDomain. HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetAppDomainFullName(VMPTR_AppDomain vmAppDomain, IStringHolder * pStrName) { diff --git a/src/coreclr/debug/daccess/dacdbiimpl.h b/src/coreclr/debug/daccess/dacdbiimpl.h index fa65322b383cc2..54d1a5bf000d05 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.h +++ b/src/coreclr/debug/daccess/dacdbiimpl.h @@ -87,9 +87,6 @@ class DacDbiInterfaceImpl : // Get the AppDomain ID for an AppDomain. HRESULT STDMETHODCALLTYPE GetAppDomainId(VMPTR_AppDomain vmAppDomain, OUT ULONG * pRetVal); - // Get the managed AppDomain object for an AppDomain. - HRESULT STDMETHODCALLTYPE GetAppDomainObject(VMPTR_AppDomain vmAppDomain, OUT VMPTR_OBJECTHANDLE * pRetVal); - // Get the full AD friendly name for the appdomain. HRESULT STDMETHODCALLTYPE GetAppDomainFullName(VMPTR_AppDomain vmAppDomain, IStringHolder * pStrName); diff --git a/src/coreclr/debug/di/rsappdomain.cpp b/src/coreclr/debug/di/rsappdomain.cpp index 9c0c7dc8c69de5..03289b461bc993 100644 --- a/src/coreclr/debug/di/rsappdomain.cpp +++ b/src/coreclr/debug/di/rsappdomain.cpp @@ -702,8 +702,8 @@ HRESULT CordbAppDomain::GetName(ULONG32 cchName, } /* - * GetObject returns the runtime app domain object. - * Note: this is lazily initialized and may be NULL + * GetObject always returns S_FALSE with a null object. + * The runtime AppDomain object is not available through this API. */ HRESULT CordbAppDomain::GetObject(ICorDebugValue **ppObject) { @@ -714,24 +714,11 @@ HRESULT CordbAppDomain::GetObject(ICorDebugValue **ppObject) ATT_REQUIRE_STOPPED_MAY_FAIL(GetProcess()); _ASSERTE(!m_vmAppDomain.IsNull()); - IDacDbiInterface * pDac = NULL; HRESULT hr = S_OK; EX_TRY { - pDac = m_pProcess->GetDAC(); - VMPTR_OBJECTHANDLE vmObjHandle; - IfFailThrow(pDac->GetAppDomainObject(m_vmAppDomain, &vmObjHandle)); - if (!vmObjHandle.IsNull()) - { - ICorDebugReferenceValue * pRefValue = NULL; - hr = CordbReferenceValue::BuildFromGCHandle(this, vmObjHandle, &pRefValue); - *ppObject = pRefValue; - } - else - { - *ppObject = NULL; - hr = S_FALSE; - } + *ppObject = NULL; + hr = S_FALSE; } EX_CATCH_HRESULT(hr); diff --git a/src/coreclr/debug/inc/dacdbiinterface.h b/src/coreclr/debug/inc/dacdbiinterface.h index b3e4a053bd92bd..0b8797a4eccb8f 100644 --- a/src/coreclr/debug/inc/dacdbiinterface.h +++ b/src/coreclr/debug/inc/dacdbiinterface.h @@ -265,23 +265,6 @@ IDacDbiInterface : public IUnknown // virtual HRESULT STDMETHODCALLTYPE GetAppDomainId(VMPTR_AppDomain vmAppDomain, OUT ULONG * pRetVal) = 0; - // - // Get the managed AppDomain object for an AppDomain. - // - // Arguments: - // vmAppDomain - VM pointer to the AppDomain object of interest - // pRetVal - [out] Objecthandle for the managed app domain object or the Null VMPTR if there is no object created yet. - // - // Return Value: - // S_OK on success; otherwise, an appropriate failure HRESULT. - // - // Notes: - // The AppDomain managed object is lazily constructed on the AppDomain the first time - // it is requested. It may be NULL. - // - virtual HRESULT STDMETHODCALLTYPE GetAppDomainObject(VMPTR_AppDomain vmAppDomain, OUT VMPTR_OBJECTHANDLE * pRetVal) = 0; - - // // Get the full AD friendly name for the given EE AppDomain. // diff --git a/src/coreclr/inc/dacdbi.idl b/src/coreclr/inc/dacdbi.idl index 18c7b8ffd04f75..f8e647b376d45a 100644 --- a/src/coreclr/inc/dacdbi.idl +++ b/src/coreclr/inc/dacdbi.idl @@ -202,7 +202,6 @@ interface IDacDbiInterface : IUnknown // App Domains HRESULT GetAppDomainId([in] VMPTR_AppDomain vmAppDomain, [out] ULONG * pRetVal); - HRESULT GetAppDomainObject([in] VMPTR_AppDomain vmAppDomain, [out] VMPTR_OBJECTHANDLE * pRetVal); HRESULT GetAppDomainFullName([in] VMPTR_AppDomain vmAppDomain, [in] IDacDbiStringHolder pStrName); HRESULT GetModuleSimpleName([in] VMPTR_Module vmModule, [in] IDacDbiStringHolder pStrFilename); HRESULT GetAssemblyPath([in] VMPTR_Assembly vmAssembly, [in] IDacDbiStringHolder pStrFilename, [out] BOOL * pResult); diff --git a/src/coreclr/vm/appdomain.hpp b/src/coreclr/vm/appdomain.hpp index 08138af6a315ae..3e6893f0884da6 100644 --- a/src/coreclr/vm/appdomain.hpp +++ b/src/coreclr/vm/appdomain.hpp @@ -675,9 +675,6 @@ class AppDomain final STRINGREF *IsStringInterned(STRINGREF *pString); STRINGREF *GetOrInternString(STRINGREF *pString); - OBJECTREF GetRawExposedObject() { LIMITED_METHOD_CONTRACT; return NULL; } - OBJECTHANDLE GetRawExposedObjectHandleForDebugger() { LIMITED_METHOD_DAC_CONTRACT; return (OBJECTHANDLE)NULL; } - #ifndef DACCESS_COMPILE PTR_NativeImage GetNativeImage(LPCUTF8 compositeFileName); PTR_NativeImage SetNativeImage(LPCUTF8 compositeFileName, PTR_NativeImage pNativeImage); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs index 2ea47ab69e8f94..e446e9d9511559 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs @@ -120,9 +120,6 @@ public int GetAppDomainId(ulong vmAppDomain, uint* pRetVal) return hr; } - public int GetAppDomainObject(ulong vmAppDomain, ulong* pRetVal) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.GetAppDomainObject(vmAppDomain, pRetVal) : HResults.E_NOTIMPL; - public int GetAppDomainFullName(ulong vmAppDomain, nint pStrName) { int hr = HResults.S_OK; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs index 77aa05e7fdacbb..107627c28b6e86 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs @@ -166,9 +166,6 @@ public unsafe partial interface IDacDbiInterface [PreserveSig] int GetAppDomainId(ulong vmAppDomain, uint* pRetVal); - [PreserveSig] - int GetAppDomainObject(ulong vmAppDomain, ulong* pRetVal); - [PreserveSig] int GetAppDomainFullName(ulong vmAppDomain, nint pStrName); From 11a3209b045dead41c611af9cd9e2adbaf588c25 Mon Sep 17 00:00:00 2001 From: Vlad Brezae Date: Sat, 2 May 2026 18:26:52 +0300 Subject: [PATCH 078/115] Don't report a interp->native transition when it is done from a pinvoke method (#127660) We didn't report these transitions when they were triggered from ILStubs because they already explicitly do this. Following the change of using transient IL belonging to the actual PInvoke method rather than separate ILStub methods (which should contain the same logic as the ILStub used to), we would now report the same event twice. We need therefore to also ignore the case where we are doing a PInvoke call from an actual PInvoke method. Fixes src/tests/profiler/transitions/transitions.csproj which regressed after https://github.com/dotnet/runtime/pull/126509 on interpreter. --- src/coreclr/vm/interpexec.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coreclr/vm/interpexec.cpp b/src/coreclr/vm/interpexec.cpp index f6e8947984a19d..e5d99b4c501b04 100644 --- a/src/coreclr/vm/interpexec.cpp +++ b/src/coreclr/vm/interpexec.cpp @@ -308,14 +308,14 @@ void InvokeUnmanagedCalliWithTransition(PCODE ftn, void *cookie, int8_t *stack, { GCX_PREEMP(); #ifdef PROFILING_SUPPORTED - if (CORProfilerTrackTransitions() && !pFrame->startIp->Method->methodHnd->IsILStub()) + if (CORProfilerTrackTransitions() && !pFrame->startIp->Method->methodHnd->IsILStub() && !pFrame->startIp->Method->methodHnd->IsPInvoke()) { ProfilerManagedToUnmanagedTransitionMD(pFrame->startIp->Method->methodHnd, COR_PRF_TRANSITION_CALL); } #endif InvokeUnmanagedCalli(ftn, cookie, pArgs, pRet); #ifdef PROFILING_SUPPORTED - if (CORProfilerTrackTransitions() && !pFrame->startIp->Method->methodHnd->IsILStub()) + if (CORProfilerTrackTransitions() && !pFrame->startIp->Method->methodHnd->IsILStub() && !pFrame->startIp->Method->methodHnd->IsPInvoke()) { ProfilerUnmanagedToManagedTransitionMD(pFrame->startIp->Method->methodHnd, COR_PRF_TRANSITION_CALL); } From 805d59a0082767fa7bb2f510a8451cda0623261e Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Sat, 2 May 2026 14:34:13 -0700 Subject: [PATCH 079/115] Update fgMorphSmpOp to canonicalize commutative and compare ops at the start of POST-ORDER (#127661) --- src/coreclr/jit/compiler.h | 2 + src/coreclr/jit/morph.cpp | 134 ++++++++++++++++++++++++++++--------- 2 files changed, 104 insertions(+), 32 deletions(-) diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index 7562b34fefb930..74ebf2548df805 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -6932,6 +6932,8 @@ class Compiler GenTree* fgMorphSmpOpOptional(GenTreeOp* tree, bool* optAssertionPropDone); GenTree* fgMorphConst(GenTree* tree); + void fgPushConstantsRight(GenTreeOp* tree); + GenTreeOp* fgMorphCommutative(GenTreeOp* tree); GenTree* fgMorphReduceAddOps(GenTree* tree); diff --git a/src/coreclr/jit/morph.cpp b/src/coreclr/jit/morph.cpp index a48e09d08bb82e..e713a94e0e095f 100644 --- a/src/coreclr/jit/morph.cpp +++ b/src/coreclr/jit/morph.cpp @@ -6952,6 +6952,17 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, MorphAddrContext* mac, bool* optA * First do any PRE-ORDER processing */ + //------------------------------------------------------------------------- + // NOTE: We may not have canonicalized or folded trees yet and so op1, op2, + // or both may be constant. While PRE-ORDER shouldn't generally be doing + // opts with constants, any such handling that is added needs to handle + // this edge. POST-ORDER, however, occurs after such canonicalization. + // + // -- PRE-ORDER checking for constants tends to miss optimizations since its + // child operands haven't been processed yet. This means it will fail to + // handle any scenario where morphing a child results in a new constant. + // ------------------------------------------------------------------------- + switch (oper) { // Some arithmetic operators need to use a helper call to the EE @@ -7446,21 +7457,38 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, MorphAddrContext* mac, bool* optA // For integer `a`, even if negative. if (opts.OptimizationEnabled()) { - assert(tree->OperIs(GT_EQ, GT_NE)); - if (op1->OperIs(GT_MOD) && varTypeIsIntegral(op1) && op2->IsIntegralConst(0)) + // We handle this in pre-order because MOD may otherwise be morphed into a helper call + + GenTree* cnsNode = nullptr; + GenTree* otherNode = nullptr; + + if (op2->IsIntegralConst(0)) { - GenTree* op1op2 = op1->AsOp()->gtOp2; - if (op1op2->IsCnsIntOrI()) + cnsNode = op2; + otherNode = op1; + } + else if (op1->IsIntegralConst(0)) + { + cnsNode = op1; + otherNode = op2; + } + + if ((cnsNode != nullptr) && otherNode->OperIs(GT_MOD) && varTypeIsIntegral(otherNode)) + { + GenTree* divisor = otherNode->gtGetOp2(); + if (divisor->IsCnsIntOrI()) { - const ssize_t modValue = op1op2->AsIntCon()->IconValue(); + const ssize_t modValue = divisor->AsIntCon()->IconValue(); if (isPow2(modValue)) { JITDUMP("\nTransforming:\n"); DISPTREE(tree); - op1->SetOper(GT_AND); // Change % => & - op1op2->AsIntConCommon()->SetIconValue(modValue - 1); // Change c => c - 1 - fgUpdateConstTreeValueNumber(op1op2); + otherNode->SetOper(GT_AND); // Change % => & + otherNode->gtFlags &= ~(GTF_DIV_MOD_NO_OVERFLOW | GTF_DIV_MOD_NO_BY_ZERO); + + divisor->AsIntConCommon()->SetIconValue(modValue - 1); // Change c => c - 1 + fgUpdateConstTreeValueNumber(divisor); JITDUMP("\ninto:\n"); DISPTREE(tree); @@ -7468,9 +7496,8 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, MorphAddrContext* mac, bool* optA } } } - } - FALLTHROUGH; + } case GT_GT: { @@ -7768,6 +7795,14 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, MorphAddrContext* mac, bool* optA case GT_EQ: case GT_NE: + { + fgPushConstantsRight(tree->AsOp()); + assert(tree->OperIsCompare()); + + oper = tree->OperGet(); + op1 = tree->gtGetOp1(); + op2 = tree->gtGetOp2(); + if (op2->IsIntegralConst()) { tree = fgOptimizeEqualityComparisonWithConst(tree->AsOp()); @@ -7777,22 +7812,20 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, MorphAddrContext* mac, bool* optA op1 = tree->gtGetOp1(); op2 = tree->gtGetOp2(); } - goto COMPARE; + break; + } case GT_LT: case GT_LE: case GT_GE: case GT_GT: - // Change "CNS relop op2" to "op2 relop* CNS" - if (op1->IsIntegralConst() && tree->OperIsCompare() && gtCanSwapOrder(op1, op2)) - { - std::swap(tree->AsOp()->gtOp1, tree->AsOp()->gtOp2); - tree->gtOper = GenTree::SwapRelop(tree->OperGet()); + { + fgPushConstantsRight(tree->AsOp()); + assert(tree->OperIsCompare()); - oper = tree->OperGet(); - op1 = tree->gtGetOp1(); - op2 = tree->gtGetOp2(); - } + oper = tree->OperGet(); + op1 = tree->gtGetOp1(); + op2 = tree->gtGetOp2(); if (op1->OperIs(GT_CAST) || op2->OperIs(GT_CAST)) { @@ -7810,7 +7843,7 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, MorphAddrContext* mac, bool* optA op2 = tree->gtGetOp2(); } - if (opts.OptimizationEnabled() && fgGlobalMorph && tree->OperIs(GT_GT, GT_LT, GT_LE, GT_GE)) + if (opts.OptimizationEnabled() && fgGlobalMorph) { // Normalize unsigned comparisons to signed if both operands a known to be never negative. if (tree->IsUnsigned() && varTypeIsIntegral(op1) && op1->IsNeverNegative(this) && @@ -7828,11 +7861,8 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, MorphAddrContext* mac, bool* optA } } } - - COMPARE: - - noway_assert(tree->OperIsCompare()); break; + } case GT_MUL: @@ -10302,6 +10332,48 @@ GenTree* Compiler::fgOptimizeHWIntrinsicAssociative(GenTreeHWIntrinsic* tree) } #endif // FEATURE_HW_INTRINSICS +//------------------------------------------------------------------------ +// fgPushConstantsRight: Pushes constants to the right to help canonicalize the shape +// +// Arguments: +// tree - the commutative or comparison node +// +void Compiler::fgPushConstantsRight(GenTreeOp* tree) +{ + assert(tree->OperIsCommutative() || tree->OperIsCompare()); + + GenTree* op1 = tree->gtGetOp1(); + GenTree* op2 = tree->gtGetOp2(); + + if (op1->OperIsConst()) + { + // We have one of the following: + // * CNS op X - SWAP - X op CNS + // * CNS op CNS - NO SWAP - CNS op CNS + // * CNS op CNS_HDL - SWAP - CNS_HDL op CNS + // * CNS_HDL op X - SWAP - X op CNS_HDL + // * CNS_HDL op CNS - NO SWAP - CNS_HDL op CNS + // * CNS_HDL op CNS_HDL - NO SWAP - CNS_HDL op CNS_HDL + // + // This preserves the ordering if op1 and op2 are the + // same kind of constant and otherwise pushes constants + // to the right. The special scenario is that when one + // is a constant and the other a handle, we prefer the + // handle to be on the left. + + if (!op2->OperIsConst() || (!op1->IsIconHandle() && op2->IsIconHandle())) + { + tree->gtOp1 = op2; + tree->gtOp2 = op1; + + if (tree->OperIsCompare()) + { + tree->gtOper = GenTree::SwapRelop(tree->gtOper); + } + } + } +} + //------------------------------------------------------------------------ // fgOptimizeCommutativeArithmetic: Optimizes commutative operations. // @@ -10316,13 +10388,7 @@ GenTree* Compiler::fgOptimizeCommutativeArithmetic(GenTreeOp* tree) assert(tree->OperIs(GT_ADD, GT_MUL, GT_OR, GT_XOR, GT_AND)); assert(!tree->gtOverflowEx()); - // Commute constants to the right. - if (tree->gtGetOp1()->OperIsConst() && !tree->gtGetOp1()->TypeIs(TYP_REF)) - { - // TODO-Review: We used to assert here that "(!op2->OperIsConst() || !opts.OptEnabled(CLFLG_CONSTANTFOLD))". - // This may indicate a missed "remorph". Task is to re-enable this assertion and investigate. - std::swap(tree->gtOp1, tree->gtOp2); - } + fgPushConstantsRight(tree); if (fgOperIsBitwiseRotationRoot(tree->OperGet())) { @@ -12450,6 +12516,10 @@ GenTreeOp* Compiler::fgMorphLongMul(GenTreeOp* mul) GenTree* Compiler::fgMorphTree(GenTree* tree, MorphAddrContext* mac) { + // We should never be called from CSE. Various optimizations below + // assume that it is safe to swap operands if one is a constant. + assert(!optValnumCSE_phase); + assert(tree); tree->ClearMorphed(); From ebcaacc31a0da8c4f36e33dbc736bae5264acb3f Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 May 2026 23:17:57 -0700 Subject: [PATCH 080/115] [cDAC] DacDbi code-version node APIs for ReJIT/SOS (#126980) ## Description Implements the cDAC `DacDbiImpl` APIs `GetActiveRejitILCodeVersionNode`, `GetNativeCodeVersionNode`, and `GetILCodeVersionNode` using existing `CodeVersions` and `ReJIT` contracts. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rcj1 <77995559+rcj1@users.noreply.github.com> Co-authored-by: rcj1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Contracts/Loader_1.cs | 6 +- .../Dbi/DacDbiImpl.cs | 125 +++++++++++++++++- 2 files changed, 124 insertions(+), 7 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs index 1a4ddc04445b03..36dd018aae0e02 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs @@ -532,7 +532,8 @@ ModuleLookupTables ILoader.GetLookupTables(ModuleHandle handle) TargetPointer ILoader.GetModuleLookupMapElement(TargetPointer table, uint token, out TargetNUInt flags) { - if (table == TargetPointer.Null) + uint rid = EcmaMetadataUtils.GetRowId(token); + if (table == TargetPointer.Null || rid == 0) { flags = new TargetNUInt(0); return TargetPointer.Null; @@ -540,9 +541,6 @@ TargetPointer ILoader.GetModuleLookupMapElement(TargetPointer table, uint token, Data.ModuleLookupMap lookupMap = _target.ProcessedData.GetOrAdd(table); ulong supportedFlagsMask = lookupMap.SupportedFlagsMask.Value; - - uint rid = EcmaMetadataUtils.GetRowId(token); - ArgumentOutOfRangeException.ThrowIfZero(rid); (TargetPointer rval, uint _) = IterateModuleLookupMap(table, rid, SearchLookupMap).FirstOrDefault(); flags = new TargetNUInt(rval & supportedFlagsMask); return rval & ~supportedFlagsMask; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs index e446e9d9511559..9ec9c2f30c53e0 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs @@ -1793,13 +1793,132 @@ public int GetMDStructuresVersion(uint* pMDStructuresVersion) } public int GetActiveRejitILCodeVersionNode(ulong vmModule, uint methodTk, ulong* pVmILCodeVersionNode) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.GetActiveRejitILCodeVersionNode(vmModule, methodTk, pVmILCodeVersionNode) : HResults.E_NOTIMPL; + { + int hr = HResults.S_OK; + try + { + if (pVmILCodeVersionNode is null) + throw new ArgumentException("Output pointer cannot be null.", nameof(pVmILCodeVersionNode)); + + *pVmILCodeVersionNode = 0; + + if (!_target.Contracts.TryGetContract(out IReJIT rejit)) + return hr; + + ILoader loader = _target.Contracts.Loader; + Contracts.ModuleHandle module = loader.GetModuleHandleFromModulePtr(new TargetPointer(vmModule)); + ModuleLookupTables lookupTables = loader.GetLookupTables(module); + TargetPointer methodDesc = TargetPointer.Null; + + if ((EcmaMetadataUtils.TokenType)(methodTk & EcmaMetadataUtils.TokenTypeMask) != EcmaMetadataUtils.TokenType.mdtMethodDef) + throw new ArgumentException("methodTk must be a MethodDef token.", nameof(methodTk)); + methodDesc = loader.GetModuleLookupMapElement(lookupTables.MethodDefToDesc, methodTk, out _); + + if (methodDesc != TargetPointer.Null) + { + ICodeVersions codeVersions = _target.Contracts.CodeVersions; + ILCodeVersionHandle ilCodeVersion = codeVersions.GetActiveILCodeVersion(methodDesc); + if (ilCodeVersion.IsValid + && ilCodeVersion.IsExplicit + && rejit.GetRejitState(ilCodeVersion) == RejitState.Active) + { + *pVmILCodeVersionNode = ilCodeVersion.ILCodeVersionNode.Value; + } + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacy is not null) + { + ulong resultLocal; + int hrLocal = _legacy.GetActiveRejitILCodeVersionNode(vmModule, methodTk, &resultLocal); + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + Debug.Assert(*pVmILCodeVersionNode == resultLocal, $"cDAC: {*pVmILCodeVersionNode:x}, DAC: {resultLocal:x}"); + } +#endif + + return hr; + } public int GetNativeCodeVersionNode(ulong vmMethod, ulong codeStartAddress, ulong* pVmNativeCodeVersionNode) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.GetNativeCodeVersionNode(vmMethod, codeStartAddress, pVmNativeCodeVersionNode) : HResults.E_NOTIMPL; + { + int hr = HResults.S_OK; + try + { + if (pVmNativeCodeVersionNode is null) + throw new ArgumentException("Output pointer cannot be null.", nameof(pVmNativeCodeVersionNode)); + + *pVmNativeCodeVersionNode = 0; + + TargetCodePointer codeAddress = new TargetCodePointer(codeStartAddress); + ICodeVersions codeVersions = _target.Contracts.CodeVersions; + + NativeCodeVersionHandle nativeCodeVersion = codeVersions.GetNativeCodeVersionForIP(codeAddress); + if (nativeCodeVersion.Valid && nativeCodeVersion.IsExplicit) + { + *pVmNativeCodeVersionNode = nativeCodeVersion.CodeVersionNodeAddress.Value; + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacy is not null) + { + ulong resultLocal; + int hrLocal = _legacy.GetNativeCodeVersionNode(vmMethod, codeStartAddress, &resultLocal); + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + Debug.Assert(*pVmNativeCodeVersionNode == resultLocal, $"cDAC: {*pVmNativeCodeVersionNode:x}, DAC: {resultLocal:x}"); + } +#endif + + return hr; + } public int GetILCodeVersionNode(ulong vmNativeCodeVersionNode, ulong* pVmILCodeVersionNode) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.GetILCodeVersionNode(vmNativeCodeVersionNode, pVmILCodeVersionNode) : HResults.E_NOTIMPL; + { + int hr = HResults.S_OK; + try + { + if (pVmILCodeVersionNode is null) + throw new ArgumentException("Output pointer cannot be null.", nameof(pVmILCodeVersionNode)); + + *pVmILCodeVersionNode = 0; + + ICodeVersions codeVersions = _target.Contracts.CodeVersions; + NativeCodeVersionHandle nativeCodeVersion = NativeCodeVersionHandle.CreateExplicit(new TargetPointer(vmNativeCodeVersionNode)); + ILCodeVersionHandle ilCodeVersion = codeVersions.GetILCodeVersion(nativeCodeVersion); + if (ilCodeVersion.IsValid && ilCodeVersion.IsExplicit) + { + *pVmILCodeVersionNode = ilCodeVersion.ILCodeVersionNode.Value; + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacy is not null) + { + ulong resultLocal; + int hrLocal = _legacy.GetILCodeVersionNode(vmNativeCodeVersionNode, &resultLocal); + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + Debug.Assert(*pVmILCodeVersionNode == resultLocal, $"cDAC: {*pVmILCodeVersionNode:x}, DAC: {resultLocal:x}"); + } +#endif + + return hr; + } public int GetILCodeVersionNodeData(ulong ilCodeVersionNode, DacDbiSharedReJitInfo* pData) => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.GetILCodeVersionNodeData(ilCodeVersionNode, pData) : HResults.E_NOTIMPL; From 99261f7ec5d0bfc47bbbad8248058162c0d2c9d1 Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Sun, 3 May 2026 22:10:27 +0200 Subject: [PATCH 081/115] [wasm][coreclr] Do not use SetCleanupNeededForFinalizedThread (#127562) > [!NOTE] > PR description generated with Copilot assistance. On wasm there is no finalizer thread. GC finalizers run inline on the main thread, causing `SetCleanupNeededForFinalizedThread` to hit the `IsFinalizerThread()` assert when a `Thread` object is finalized. The call to `CleanupFinalizedThreads` is not reached anyway because `HaveExtraWorkForFinalizer` returns false on wasm. Also enable System.Threading.Thread tests on browser/CoreCLR. --- src/coreclr/vm/comsynchronizable.cpp | 2 ++ src/libraries/tests.proj | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coreclr/vm/comsynchronizable.cpp b/src/coreclr/vm/comsynchronizable.cpp index 67dd9c2f620d1a..c98cbc492656fc 100644 --- a/src/coreclr/vm/comsynchronizable.cpp +++ b/src/coreclr/vm/comsynchronizable.cpp @@ -630,7 +630,9 @@ FCIMPL1(void, ThreadNative::Finalize, ThreadBaseObject* pThisUNSAFE) } thread->SetThreadState(Thread::TS_Finalized); +#ifdef FEATURE_MULTITHREADING Thread::SetCleanupNeededForFinalizedThread(); +#endif // FEATURE_MULTITHREADING } } FCIMPLEND diff --git a/src/libraries/tests.proj b/src/libraries/tests.proj index 1290579eeede68..c3a6d07d77d383 100644 --- a/src/libraries/tests.proj +++ b/src/libraries/tests.proj @@ -185,8 +185,6 @@ - - From fcd092d376288c6c8c587217565f74107b9a3593 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Mon, 4 May 2026 04:13:42 +0800 Subject: [PATCH 082/115] Revert "Simplify TrimmerSingleWarn intermediate assembly update using MSBuild item Update" (#127656) Reverts dotnet/runtime#125630 The `Update` in a target doesn't work (see https://github.com/dotnet/msbuild/issues/2835 for the MSBuild behavior). Fixes the dependency flow issue here: https://github.com/dotnet/sdk/pull/53847#issuecomment-4355555116. --- .../build/Microsoft.NET.ILLink.targets | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets b/src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets index 92976f23cd65ba..fd8868d1967b61 100644 --- a/src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets +++ b/src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets @@ -227,9 +227,19 @@ Copyright (c) .NET Foundation. All rights reserved. - - false - + + <__SingleWarnIntermediateAssembly Include="@(ResolvedFileToPublish)" /> + <__SingleWarnIntermediateAssembly Remove="@(IntermediateAssembly)" /> + + <_SingleWarnIntermediateAssembly Include="@(ResolvedFileToPublish)" /> + <_SingleWarnIntermediateAssembly Remove="@(__SingleWarnIntermediateAssembly)" /> + + <_SingleWarnIntermediateAssembly> + false + + + + From b6b7d929d43ec150af954e2e963ccb9b024274ba Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Sun, 3 May 2026 22:23:21 -0700 Subject: [PATCH 083/115] Ensure we preserve vn and produce a canonical tree (#127689) This ensures that we preserve the VN when morphing a particular `MOD->AND` pattern and ensures that a particular `SUB->ADD` pattern produces a "canonical" tree with the constant as the second operand. --- src/coreclr/jit/morph.cpp | 42 +++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/src/coreclr/jit/morph.cpp b/src/coreclr/jit/morph.cpp index e713a94e0e095f..8ffff718ad2af3 100644 --- a/src/coreclr/jit/morph.cpp +++ b/src/coreclr/jit/morph.cpp @@ -6736,6 +6736,7 @@ GenTree* Compiler::fgMorphLeaf(GenTree* tree) tree->SetOper(GT_CNS_INT); tree->AsIntConCommon()->SetIconValue(ssize_t(addrInfo.handle)); tree->gtFlags |= GTF_ICON_FTN_ADDR; + fgUpdateConstTreeValueNumber(tree); INDEBUG(tree->AsIntCon()->gtTargetHandle = reinterpret_cast(funcHandle)); break; @@ -7484,7 +7485,7 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, MorphAddrContext* mac, bool* optA JITDUMP("\nTransforming:\n"); DISPTREE(tree); - otherNode->SetOper(GT_AND); // Change % => & + otherNode->SetOper(GT_AND, GenTree::PRESERVE_VN); // Change % => & otherNode->gtFlags &= ~(GTF_DIV_MOD_NO_OVERFLOW | GTF_DIV_MOD_NO_BY_ZERO); divisor->AsIntConCommon()->SetIconValue(modValue - 1); // Change c => c - 1 @@ -7902,11 +7903,11 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, MorphAddrContext* mac, bool* optA fgUpdateConstTreeValueNumber(op2); oper = GT_ADD; - tree->ChangeOper(oper); + tree->ChangeOper(oper, GenTree::PRESERVE_VN); goto CM_ADD_OP; } - /* Check for "cns1 - op2" , we change it to "(cns1 + (-op2))" */ + /* Check for "cns1 - op2" , we change it to "((-op2) + cns1)" */ noway_assert(op1); if (op1->IsCnsIntOrI()) @@ -7919,7 +7920,7 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, MorphAddrContext* mac, bool* optA // GT_CAST(GT_SUB(0, s_1.ubyte)) if (op1->IsIntegralConst(0)) { - tree->ChangeOper(GT_NEG); + tree->ChangeOper(GT_NEG, GenTree::PRESERVE_VN); tree->gtType = genActualType(op2->TypeGet()); tree->AsOp()->gtOp1 = op2; @@ -7929,11 +7930,15 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, MorphAddrContext* mac, bool* optA return tree; } - tree->AsOp()->gtOp2 = op2 = gtNewOperNode(GT_NEG, genActualType(op2->TypeGet()), op2); + op2 = gtNewOperNode(GT_NEG, genActualType(op2->TypeGet()), op2); fgMorphTreeDone(op2); + std::swap(op2, op1); + tree->AsOp()->gtOp1 = op1; + tree->AsOp()->gtOp2 = op2; + oper = GT_ADD; - tree->ChangeOper(oper); + tree->ChangeOper(oper, GenTree::PRESERVE_VN); goto CM_ADD_OP; } } @@ -8061,8 +8066,9 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, MorphAddrContext* mac, bool* optA if (transform) { - tree->ChangeOper(GT_MUL); + tree->ChangeOper(GT_MUL, GenTree::PRESERVE_VN); op2->AsDblCon()->SetDconValue(divisor); + fgUpdateConstTreeValueNumber(op2); oper = GT_MUL; goto CM_OVF_OP; @@ -8925,6 +8931,7 @@ GenTree* Compiler::fgOptimizeEqualityComparisonWithConst(GenTreeOp* cmp) { goto SKIP; // Unsupported type or invalid shift amount. } + fgUpdateConstTreeValueNumber(andMask); andOp->gtOp1 = rshiftOp->gtGetOp1(); DEBUG_DESTROY_NODE(rshiftOp->gtGetOp2()); @@ -8957,6 +8964,7 @@ GenTree* Compiler::fgOptimizeEqualityComparisonWithConst(GenTreeOp* cmp) { gtReverseCond(cmp); op2->SetIntegralValue(0); + fgUpdateConstTreeValueNumber(op2); } } } @@ -10109,6 +10117,7 @@ GenTree* Compiler::fgOptimizeHWIntrinsic(GenTreeHWIntrinsic* node) { ExtractEffectiveOp(GT_NOT, node, /* destroyNodes */ true); cmpOp3->AsIntConCommon()->SetIntegralValue(static_cast(mode)); + fgUpdateConstTreeValueNumber(cmpOp3); return fgMorphHWIntrinsicRequired(op1Intrin); } break; @@ -10663,7 +10672,7 @@ GenTree* Compiler::fgOptimizeMultiply(GenTreeOp* mul) } else { - mul->ChangeOper(GT_NEG); + mul->ChangeOper(GT_NEG, GenTree::PRESERVE_VN); mul->AsOp()->gtOp2 = nullptr; return mul; } @@ -10995,7 +11004,7 @@ GenTree* Compiler::fgOptimizeBitwiseXor(GenTreeOp* xorOp) else if (op2->IsIntegralConst(-1)) { /* "x ^ -1" is "~x" */ - xorOp->ChangeOper(GT_NOT); + xorOp->ChangeOper(GT_NOT, GenTree::PRESERVE_VN); xorOp->gtOp2 = nullptr; DEBUG_DESTROY_NODE(op2); @@ -11014,7 +11023,7 @@ GenTree* Compiler::fgOptimizeBitwiseXor(GenTreeOp* xorOp) { // "x ^ -0.0" is "-x" - xorOp->ChangeOper(GT_NEG); + xorOp->ChangeOper(GT_NEG, GenTree::PRESERVE_VN); xorOp->gtOp2 = nullptr; DEBUG_DESTROY_NODE(op2); @@ -11323,13 +11332,16 @@ GenTree* Compiler::fgMorphSmpOpOptional(GenTreeOp* tree, bool* optAssertionPropD /* Change '(val + iadd) * imul' -> '(val * imul) + (iadd * imul)' */ oper = GT_ADD; - tree->ChangeOper(oper); + tree->ChangeOper(oper, GenTree::PRESERVE_VN); op2->AsIntCon()->SetValueTruncating(iadd * imul); + fgUpdateConstTreeValueNumber(op2); + // changing from (val + iadd) to (val * imul), so don't preserve VN op1->ChangeOper(GT_MUL); add->AsIntCon()->SetIconValue(imul); + fgValueNumberTreeConst(add); } } @@ -11368,13 +11380,17 @@ GenTree* Compiler::fgMorphSmpOpOptional(GenTreeOp* tree, bool* optAssertionPropD /* Change "(val + iadd) << ishf" into "(val<ChangeOper(GT_ADD); + tree->ChangeOper(GT_ADD, GenTree::PRESERVE_VN); // we are reusing the shift amount node here, but the type we want is that of the shift result op2->gtType = op1->gtType; op2->AsIntConCommon()->SetValueTruncating(iadd << ishf); + fgUpdateConstTreeValueNumber(op2); + + // changing from (val + iadd) to (val << ishf), so don't preserve VN op1->ChangeOper(GT_LSH); cns->AsIntConCommon()->SetIconValue(ishf); + fgUpdateConstTreeValueNumber(cns); } } @@ -12396,7 +12412,7 @@ GenTree* Compiler::fgRecognizeAndMorphBitwiseRotation(GenTree* tree) { tree->AsOp()->gtOp1 = rotatedValue; tree->AsOp()->gtOp2 = rotateIndex; - tree->ChangeOper(rotateOp); + tree->ChangeOper(rotateOp, GenTree::PRESERVE_VN); unsigned childFlags = 0; for (GenTree* op : tree->Operands()) From 9bf1cb92c001f5e8472845ffac8f7824047e3f78 Mon Sep 17 00:00:00 2001 From: Johan Lorensson Date: Mon, 4 May 2026 09:19:11 +0200 Subject: [PATCH 084/115] [Diagnostics]: High-performance EventSource runtime async profiler. (#127238) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Motivation TPL includes support to capture compiler async (AsyncV1) events to stitch together async callstacks in tools like PerfView, VS .NET Async Profiler, and Application Insights Profiler. The challenge using TPL events profiling async-heavy workloads is that they are verbose and produce a lot of data, creating too much overhead on the profiled process and skewing measurements. Each TPL event is written into ETW/EventPipe/UserEvents causing latency (kernel call) as well as additional data (~100-byte header). Even a small event without a stack takes 200–500 ns to emit at 100+ bytes. TPL tracking of async execution generates heavy traffic on the eventing subsystem, increasing the risk of dropping events. ### TPL overhead (synthetic benchmark) | Async resume/suspend rate | Throughput drop | ETL size (20 s) | Dropped events | |---|---|---|---| | 1M/s | >75% | | severe (requires enlarged ETW buffers) | | 100K/s | ~45% | 3+ GB | high (requires enlarged ETW buffers) | | 10K/s | ~10% | | moderate | TPL depends on a complete chain of events to recreate async callstacks — losing any events makes post-processing unreliable. There have been ideas for quite some time to look into a more lightweight approach to track async method execution, making it possible to recreate async callstacks for sync callstacks captured by external tools like OS CPU samplers and profilers. With the introduction of runtime async (AsyncV2), it was decided to revisit this and see what we could do to improve the profiler experience of async code. The async profiler is not tied to runtime async methods (AsyncV2), so it will be able to handle compiler async methods (AsyncV1) as well, but this PR focuses on AsyncV2. Follow-up PRs will add AsyncV1 support, making it possible to use the new async profiler to collect both AsyncV1 and AsyncV2 async callstacks. --- ## Design **NOTE: All formats introduced by this PR are currently considered **internal** and can be changed without notice.** This PR adds `AsyncProfilerBufferedEventSource` — a high-performance EventSource for async method profiling that uses per-thread buffered event emission with centralized flush coordination. ### Core architecture - Per-thread event buffers with lock-free acquire/release for zero-contention writes on the hot path. - Delta timestamp encoding using compressed variable-length integers (LEB128 + zigzag), reducing per-event timestamp overhead from 8 bytes to typically 1–2 bytes under load. - Delta IP encoding using compressed variable-length integers, reducing bytes per frame IP. - Centralized `AsyncThreadContextCache` with background flush timer for idle and dead thread buffer reclamation. - Continuation wrapper table for compact async callstack representation, mapping runtime IPs to table indices — enables matching sync callstacks captured by OS CPU profilers through the resume async callstack event. ### Event types Events cover the full async lifecycle: | Category | Events | |---|---| | Async context | create, resume, suspend, complete | | Async method | resume, complete | | Exception unwind | unhandled, handled | | Async callstacks | create, resume, suspend | ### Buffer management - Configurable buffer size via `DOTNET_AsyncProfilerEventSource_EventBufferSize` (default 16 KB − 256 bytes). - Optimized buffer serialization with low overhead. - SyncPoint mechanism for coordinated config changes across writer threads. - BlockContext flag for safe flush-thread access to live thread buffers with 100 ms spin timeout to prevent flush thread stalls. ### Integration - Async profiler wired into `AsyncInstrumentation` alongside the async debugger. - Cross-runtime design: EventSource + AsyncProfiler with CoreCLR-specific parts in a dedicated source file. ### Test coverage Comprehensive test suite (`AsyncProfilerTests`) validating event correctness, buffer serialization, delta encoding, callstack capture, config changes, and multi-threaded stress scenarios. --- ## Performance results ### Overhead comparison: async profiler vs. TPL | Async resume/suspend rate | TPL overhead | Async profiler overhead | Improvement | |---|---|---|---| | 1M/s | >75% | ~20% | ~4× | | 100K/s | ~45% | <1% | ~40× | | 10K/s | ~10% | <0.3% (noise) | ~30× | > **Note:** The 1M/s scenario is extreme — the benchmark does virtually no work, only exercising the internal async dispatch loop. Any real user code in the async methods will quickly reduce the relative overhead. ### Data volume ETL file size is down **~10×** for scenarios capturing data to recreate async callstacks. For the 1M/s scenario over 20 seconds: | | ETL size | Dropped events | |---|---|---| | **TPL** | 3+ GB | many (default ETW settings) | | **Async profiler** | ~330 MB | none (default ETW settings) | ### VS CPU profiling visibility Running the 1M/s scenario under VS CPU profiling, none of the async profiler methods stand out — most are in the 0.01–0.03% self-CPU range with very low sample counts. The instrumented `DispatchContinuations` function shows ~2% overhead compared to the uninstrumented version. Even in very heavy async workloads, the async profiler does not pollute VS CPU profiling output. --- ## Continuation wrapper optimization My initial ambition was to track the async callstack on any thread at any point using a single event including the resumed async callstack executed through the dispatch loop. Initially that strategy hit issues due to ambiguity mapping methods between sync callstacks collected by the OS CPU sampler and the resumed async callstack active at that time. This can be solved using `CompleteAsyncMethod` events to recreate async callstacks at any point in time. `CompleteAsyncMethod` is just a signal event consuming a couple of bytes in the event buffer, but it introduces ~30–40 ns per completed method. The major cost is capturing the QPC (~15–20 ns, platform-dependent); the rest is raw memcpy + delta encoding. Reading the timestamp directly via CPU instruction could bring this down to ~10 ns total — a potential future optimization (would require a JIT intrinsic). Instead of continuing to optimize `CompleteAsyncMethod`, I revisited the original problem: if we inject an **anchor** in the sync callstack captured by external tools, we can use it to tie into the async callstack emitted via the resume async callstack event. Since we control the dispatch loop running each continuation, it's possible to call through an **indexed wrapper** that places enough information in the sync callstack to identify the current resumed continuation. With up to 255 frames in an async callstack, the pre-generated wrappers are capped at 32 and recycled, emitting a reset event into the stream to signal reuse to parsers. This mechanism makes it possible to recreate any async callstack tied to sync callstacks captured by external tools using a **single event** (`ResumeAsyncCallstack`). Calling through the wrapper costs ~5 ns per method resume — compared to ~30–40 ns for `CompleteAsyncMethod` plus increased output size, this ended up as a very successful optimization. --- ## Timestamp correlation All async profiler events are emitted using existing EventSource infrastructure, meaning it's possible to listen on the event stream using in-proc `EventListener`s, `ICorProfiler`, as well as external ETW/EventPipe/UserEvent clients. When events are used to recreate async callstacks for other events capturing sync callstacks emitted into the same event subsystem, all events share the same timestamp infrastructure. If events are emitted using different timestamp infrastructure not in sync, timestamps need to be re-synchronized and adjusted before use. Each buffered event uses the machine's QPC infrastructure (`Stopwatch.GetTimestamp`), and each event buffer includes the timestamp of first and last event. The metadata event emitted at the beginning of the stream includes a reference QPC + QPC frequency + reference UTC time in ticks, making it possible to convert all buffered events to wall clock time. Every minute, a clock sync event is emitted into the stream re-syncing QPC and UTC time. --- ## Future work - AsyncV1 support will come as follow-up PR(s). --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Steve Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../System.Private.CoreLib.csproj | 1 + .../CompilerServices/AsyncHelpers.CoreCLR.cs | 115 +- .../CompilerServices/AsyncProfiler.CoreCLR.cs | 627 +++++ .../src/System.Private.CoreLib.csproj | 1 + .../BasicEventSourceTest/TestUtilities.cs | 1 + .../System.Private.CoreLib.Shared.projitems | 4 +- .../CompilerServices/AsyncInstrumentation.cs | 51 +- .../Runtime/CompilerServices/AsyncProfiler.cs | 1144 ++++++++ .../AsyncProfilerEventSource.cs | 99 + .../System/Threading/Tasks/TplEventSource.cs | 20 - .../AsyncProfilerTests.cs | 2322 +++++++++++++++++ .../RuntimeAsyncTests.cs | 33 +- .../System.Threading.Tasks.Tests.csproj | 1 + 13 files changed, 4327 insertions(+), 92 deletions(-) create mode 100644 src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.CoreCLR.cs create mode 100644 src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs create mode 100644 src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfilerEventSource.cs create mode 100644 src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs diff --git a/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj b/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj index c72920560ecaa7..34eaf7557a6e46 100644 --- a/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj +++ b/src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj @@ -206,6 +206,7 @@ + diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs index a874ddb5b58989..b74b5ddef1eaa3 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs @@ -139,6 +139,13 @@ internal unsafe ref struct AsyncDispatcherInfo // to match an inflight Task to the corresponding Continuation chain. public Task? CurrentTask; +#if TARGET_64BIT + [FieldOffset(24)] +#else + [FieldOffset(12)] +#endif + public AsyncProfiler.Info AsyncProfilerInfo; + // Information about current task dispatching, to be used for async // stackwalking. [ThreadStatic] @@ -498,7 +505,7 @@ internal void InstrumentedHandleSuspended(AsyncInstrumentation.Flags flags, ref { if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) { - Continuation? nextContinuation = t_runtimeAsyncAwaitState.SentinelContinuation!.Next; + Continuation? nextContinuation = state.SentinelContinuation!.Next; AsyncDebugger.HandleSuspended(nextContinuation, newContinuation); @@ -659,12 +666,13 @@ private unsafe void InstrumentedDispatchContinuations(AsyncInstrumentation.Flags ref byte resultLoc = ref nextContinuation != null ? ref nextContinuation.GetResultStorageOrNull() : ref GetResultStorage(); RuntimeAsyncInstrumentationHelpers.ResumeRuntimeAsyncMethod(ref asyncDispatcherInfo, flags, curContinuation); - Continuation? newContinuation = curContinuation.ResumeInfo->Resume(curContinuation, ref resultLoc); + Continuation? newContinuation = RuntimeAsyncInstrumentationHelpers.ResumeContinuation(ref asyncDispatcherInfo, flags, curContinuation, ref resultLoc); if (newContinuation != null) { newContinuation.Next = nextContinuation; - RuntimeAsyncInstrumentationHelpers.SuspendRuntimeAsyncContext(flags, curContinuation, newContinuation); + + RuntimeAsyncInstrumentationHelpers.AwaitSuspendedRuntimeAsyncContext(ref asyncDispatcherInfo, flags, curContinuation, newContinuation, awaitState.SentinelContinuation!.Next); InstrumentedHandleSuspended(flags, ref awaitState, newContinuation); awaitState.Pop(); @@ -672,7 +680,7 @@ private unsafe void InstrumentedDispatchContinuations(AsyncInstrumentation.Flags return; } - RuntimeAsyncInstrumentationHelpers.CompleteRuntimeAsyncMethod(flags, curContinuation); + RuntimeAsyncInstrumentationHelpers.CompleteRuntimeAsyncMethod(ref asyncDispatcherInfo, flags, curContinuation); } catch (Exception ex) { @@ -698,7 +706,7 @@ private unsafe void InstrumentedDispatchContinuations(AsyncInstrumentation.Flags return; } - RuntimeAsyncInstrumentationHelpers.UnwindRuntimeAsyncMethodHandledException(flags, curContinuation, unwindedFrames); + RuntimeAsyncInstrumentationHelpers.UnwindRuntimeAsyncMethodHandledException(ref asyncDispatcherInfo, flags, curContinuation, unwindedFrames); handlerContinuation.SetException(ex); asyncDispatcherInfo.NextContinuation = handlerContinuation; @@ -723,7 +731,7 @@ private unsafe void InstrumentedDispatchContinuations(AsyncInstrumentation.Flags if (QueueContinuationFollowUpActionIfNecessary(asyncDispatcherInfo.NextContinuation)) { - RuntimeAsyncInstrumentationHelpers.SuspendRuntimeAsyncContext(ref asyncDispatcherInfo, flags, curContinuation); + RuntimeAsyncInstrumentationHelpers.QueueSuspendedRuntimeAsyncContext(ref asyncDispatcherInfo, flags, curContinuation, asyncDispatcherInfo.NextContinuation); awaitState.Pop(); refDispatcherInfo = asyncDispatcherInfo.Next; @@ -837,6 +845,15 @@ private static void InstrumentedFinalizeRuntimeAsyncTask(RuntimeAsyncTask { if (AsyncInstrumentation.IsEnabled.CreateAsyncContext(flags)) { + if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) + { + Continuation? nextContinuation = state.SentinelContinuation!.Next; + if (nextContinuation != null) + { + AsyncProfiler.CreateAsyncContext.Create((ulong)task.Id, nextContinuation); + } + } + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) { task.NotifyDebuggerOfRuntimeAsyncState(); @@ -1160,21 +1177,32 @@ public static bool InstrumentCheckPoint public static void ResumeRuntimeAsyncContext(Task task, ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags) { info.CurrentTask = task; + AsyncProfiler.InitInfo(ref info.AsyncProfilerInfo); if (AsyncInstrumentation.IsEnabled.ResumeAsyncContext(flags)) { + if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) + { + AsyncProfiler.ResumeAsyncContext.Resume(ref info); + } + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) { - AsyncDebugger.ResumeAsyncContext(task.Id); + AsyncDebugger.ResumeAsyncContext(task); } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SuspendRuntimeAsyncContext(ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags, Continuation curContinuation) + public static void QueueSuspendedRuntimeAsyncContext(ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags, Continuation curContinuation, Continuation nextContinuation) { if (AsyncInstrumentation.IsEnabled.SuspendAsyncContext(flags)) { + if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) + { + AsyncProfiler.SuspendAsyncContext.Suspend(ref info, nextContinuation); + } + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) { AsyncDebugger.SuspendAsyncContext(ref info, curContinuation); @@ -1183,10 +1211,15 @@ public static void SuspendRuntimeAsyncContext(ref AsyncDispatcherInfo info, Asyn } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SuspendRuntimeAsyncContext(AsyncInstrumentation.Flags flags, Continuation curContinuation, Continuation newContinuation) + public static void AwaitSuspendedRuntimeAsyncContext(ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags, Continuation curContinuation, Continuation newContinuation, Continuation? nextContinuation) { if (AsyncInstrumentation.IsEnabled.SuspendAsyncContext(flags)) { + if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) + { + AsyncProfiler.SuspendAsyncContext.Suspend(ref info, nextContinuation ?? newContinuation); + } + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) { AsyncDebugger.SuspendAsyncContext(curContinuation, newContinuation); @@ -1199,6 +1232,11 @@ public static void CompleteRuntimeAsyncContext(ref AsyncDispatcherInfo info, Asy { if (AsyncInstrumentation.IsEnabled.CompleteAsyncContext(flags)) { + if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) + { + AsyncProfiler.CompleteAsyncContext.Complete(ref info.AsyncProfilerInfo); + } + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) { AsyncDebugger.CompleteAsyncContext(info.CurrentTask); @@ -1206,10 +1244,20 @@ public static void CompleteRuntimeAsyncContext(ref AsyncDispatcherInfo info, Asy } } - public static void UnwindRuntimeAsyncMethodUnhandledException(ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags, Exception ex, Continuation curContinuation, uint _) + public static void UnwindRuntimeAsyncMethodUnhandledException(ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags, Exception ex, Continuation curContinuation, uint unwindedFrames) { + if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) + { + AsyncProfiler.ContinuationWrapper.UnwindIndex(ref info.AsyncProfilerInfo, unwindedFrames); + } + if (AsyncInstrumentation.IsEnabled.UnwindAsyncException(flags)) { + if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) + { + AsyncProfiler.AsyncMethodException.Unhandled(ref info.AsyncProfilerInfo, unwindedFrames); + } + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) { AsyncDebugger.AsyncMethodUnhandledException(info.CurrentTask, ex, curContinuation); @@ -1217,10 +1265,20 @@ public static void UnwindRuntimeAsyncMethodUnhandledException(ref AsyncDispatche } } - public static void UnwindRuntimeAsyncMethodHandledException(AsyncInstrumentation.Flags flags, Continuation curContinuation, uint unwindedFrames) + public static void UnwindRuntimeAsyncMethodHandledException(ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags, Continuation curContinuation, uint unwindedFrames) { + if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) + { + AsyncProfiler.ContinuationWrapper.UnwindIndex(ref info.AsyncProfilerInfo, unwindedFrames); + } + if (AsyncInstrumentation.IsEnabled.UnwindAsyncException(flags)) { + if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) + { + AsyncProfiler.AsyncMethodException.Handled(ref info.AsyncProfilerInfo, unwindedFrames); + } + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) { AsyncDebugger.AsyncMethodHandledException(curContinuation, unwindedFrames); @@ -1233,6 +1291,11 @@ public static void ResumeRuntimeAsyncMethod(ref AsyncDispatcherInfo info, AsyncI { if (AsyncInstrumentation.IsEnabled.ResumeAsyncMethod(flags)) { + if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) + { + AsyncProfiler.ResumeAsyncMethod.Resume(ref info.AsyncProfilerInfo); + } + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) { AsyncDebugger.ResumeAsyncMethod(ref info, curContinuation); @@ -1241,16 +1304,40 @@ public static void ResumeRuntimeAsyncMethod(ref AsyncDispatcherInfo info, AsyncI } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void CompleteRuntimeAsyncMethod(AsyncInstrumentation.Flags flags, Continuation curContinuation) + public static void CompleteRuntimeAsyncMethod(ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags, Continuation curContinuation) { + if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) + { + AsyncProfiler.ContinuationWrapper.IncrementIndex(ref info.AsyncProfilerInfo); + } + if (AsyncInstrumentation.IsEnabled.CompleteAsyncMethod(flags)) { + if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) + { + AsyncProfiler.CompleteAsyncMethod.Complete(ref info.AsyncProfilerInfo); + } + if (AsyncInstrumentation.IsEnabled.AsyncDebugger(flags)) { AsyncDebugger.CompleteAsyncMethod(curContinuation); } } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Continuation? ResumeContinuation(ref AsyncDispatcherInfo info, AsyncInstrumentation.Flags flags, Continuation curContinuation, ref byte resultLoc) + { + if (AsyncInstrumentation.IsEnabled.AsyncProfiler(flags)) + { + return AsyncProfiler.ContinuationWrapper.Dispatch(ref info, curContinuation, ref resultLoc); + } + + unsafe + { + return curContinuation.ResumeInfo->Resume(curContinuation, ref resultLoc); + } + } } internal static class AsyncDebugger @@ -1261,9 +1348,9 @@ public static void CreateAsyncContext(Task task) TplEventSource.Log.TraceOperationBegin(task.Id, "System.Runtime.CompilerServices.AsyncHelpers+RuntimeAsyncTask", 0); } - public static void ResumeAsyncContext(int id) + public static void ResumeAsyncContext(Task task) { - TplEventSource.Log.TraceSynchronousWorkBegin(id, CausalitySynchronousWork.Execution); + TplEventSource.Log.TraceSynchronousWorkBegin(task.Id, CausalitySynchronousWork.Execution); } public static void SuspendAsyncContext(ref AsyncDispatcherInfo info, Continuation curContinuation) diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.CoreCLR.cs new file mode 100644 index 00000000000000..9ce157dd648596 --- /dev/null +++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.CoreCLR.cs @@ -0,0 +1,627 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.Tracing; +using Serializer = System.Runtime.CompilerServices.AsyncProfiler.EventBuffer.Serializer; + +namespace System.Runtime.CompilerServices +{ + internal static partial class AsyncProfiler + { + internal static partial class CreateAsyncContext + { + public static void Create(ulong id, Continuation nextContinuation) + { + Info info = default; + AsyncThreadContext context = AsyncThreadContext.Acquire(ref info); + + SyncPoint.Check(context); + + EventKeywords eventKeywords = context.ActiveEventKeywords; + if (IsEnabled.AnyAsyncEvents(eventKeywords)) + { + long currentTimestamp = Stopwatch.GetTimestamp(); + if (IsEnabled.CreateAsyncContextEvent(eventKeywords)) + { + EmitEvent(context, currentTimestamp, id); + } + + if (IsEnabled.CreateAsyncCallstackEvent(eventKeywords)) + { + AsyncCallstack.EmitEvent(context, currentTimestamp, AsyncEventID.CreateAsyncCallstack, id, nextContinuation); + } + } + + AsyncThreadContext.Release(context); + } + } + + internal static partial class ResumeAsyncContext + { + public static ulong GetId(ref AsyncDispatcherInfo info) + { + if (info.CurrentTask != null) + { + return (ulong)info.CurrentTask.Id; + } + return 0; + } + + public static void Resume(ref AsyncDispatcherInfo info) + { + AsyncThreadContext context = AsyncThreadContext.Acquire(ref info.AsyncProfilerInfo); + + Resume(ref info, context, GetId(ref info), context.ActiveEventKeywords); + + AsyncThreadContext.Release(context); + } + + public static void Resume(ref AsyncDispatcherInfo info, AsyncThreadContext context, ulong id, EventKeywords activeEventKeywords) + { + if (SyncPoint.Check(context)) + { + return; + } + + if (IsEnabled.AnyAsyncEvents(activeEventKeywords)) + { + long currentTimestamp = Stopwatch.GetTimestamp(); + if (IsEnabled.ResumeAsyncContextEvent(activeEventKeywords)) + { + EmitEvent(context, currentTimestamp, id); + } + + if (IsEnabled.ResumeAsyncCallstackEvent(activeEventKeywords)) + { + AsyncCallstack.EmitEvent(context, currentTimestamp, id, info.NextContinuation); + } + } + } + } + + internal static partial class SuspendAsyncContext + { + public static void Suspend(ref AsyncDispatcherInfo info, Continuation nextContinuation) + { + AsyncThreadContext context = AsyncThreadContext.Acquire(ref info.AsyncProfilerInfo); + + SyncPoint.Check(context); + + EventKeywords activeEventKeywords = context.ActiveEventKeywords; + if (IsEnabled.AnyAsyncEvents(activeEventKeywords)) + { + long currentTimestamp = Stopwatch.GetTimestamp(); + if (IsEnabled.SuspendAsyncContextEvent(activeEventKeywords)) + { + EmitEvent(context, currentTimestamp); + } + + if (IsEnabled.SuspendAsyncCallstackEvent(activeEventKeywords)) + { + AsyncCallstack.EmitEvent(context, currentTimestamp, AsyncEventID.SuspendAsyncCallstack, GetId(ref info), nextContinuation); + } + } + + AsyncThreadContext.Release(context); + } + + private static ulong GetId(ref AsyncDispatcherInfo info) + { + if (info.CurrentTask != null) + { + return (ulong)info.CurrentTask.Id; + } + return 0; + } + } + + /// + /// Provides a table of 32 functionally identical continuation wrapper methods, each with + /// a unique native IP address. When resuming an async continuation, the profiler dispatches + /// through the wrapper at index (ContinuationIndex & COUNT_MASK), then increments the index. + /// + /// This creates a rotating pattern of unique return addresses on the native callstack. An OS + /// CPU profiler (e.g., ETW, perf) captures these native IPs in its stack samples. The async + /// profiler emits the wrapper IP table in the metadata event, so a post-processing tool can + /// identify which wrapper IPs appear in a native callstack and correlate them with the + /// async resume callstack events emitted at the same logical point. This bridges the gap + /// between synchronous native stack samples and the asynchronous continuation chain. + /// + /// Every COUNT (32) continuations, a ResetAsyncContinuationWrapperIndex event is emitted + /// so the tool knows the index has wrapped around and can correctly map subsequent samples. + /// + /// Each wrapper is marked [NoInlining] to guarantee a distinct native IP, and + /// [AggressiveOptimization] to ensure stable JIT output (skip tiered compilation). + /// + [StackTraceHidden] + internal static partial class ContinuationWrapper + { + public static void InitInfo(ref Info info) + { + info.ContinuationTable = ref Unsafe.As(ref s_continuationWrappers); + info.ContinuationIndex = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Continuation? Dispatch(ref AsyncDispatcherInfo info, Continuation curContinuation, ref byte resultLoc) + { + nint dispatcher = Unsafe.Add(ref info.AsyncProfilerInfo.ContinuationTable, info.AsyncProfilerInfo.ContinuationIndex & COUNT_MASK); + unsafe + { + return ((delegate*)(dispatcher))(curContinuation, ref resultLoc); + } + } + + public static long[] GetContinuationWrapperIPs() + { + long[] ips = new long[COUNT]; + for (int i = 0; i < COUNT; i++) + { + ips[i] = Unsafe.Add(ref Unsafe.As(ref s_continuationWrappers), i); + } + return ips; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_0(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_1(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_2(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_3(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_4(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_5(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_6(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_7(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_8(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_9(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_10(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_11(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_12(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_13(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_14(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_15(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_16(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_17(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_18(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_19(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_20(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_21(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_22(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_23(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_24(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_25(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_26(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_27(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_28(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_29(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_30(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static unsafe Continuation? Continuation_Wrapper_31(Continuation continuation, ref byte resultLoc) + { + return continuation.ResumeInfo->Resume(continuation, ref resultLoc); + } + + private static unsafe ContinuationWrapperTable InitContinuationWrappers() + { + ContinuationWrapperTable wrappers = default; + wrappers[0] = (nint)(delegate*)&Continuation_Wrapper_0; + wrappers[1] = (nint)(delegate*)&Continuation_Wrapper_1; + wrappers[2] = (nint)(delegate*)&Continuation_Wrapper_2; + wrappers[3] = (nint)(delegate*)&Continuation_Wrapper_3; + wrappers[4] = (nint)(delegate*)&Continuation_Wrapper_4; + wrappers[5] = (nint)(delegate*)&Continuation_Wrapper_5; + wrappers[6] = (nint)(delegate*)&Continuation_Wrapper_6; + wrappers[7] = (nint)(delegate*)&Continuation_Wrapper_7; + wrappers[8] = (nint)(delegate*)&Continuation_Wrapper_8; + wrappers[9] = (nint)(delegate*)&Continuation_Wrapper_9; + wrappers[10] = (nint)(delegate*)&Continuation_Wrapper_10; + wrappers[11] = (nint)(delegate*)&Continuation_Wrapper_11; + wrappers[12] = (nint)(delegate*)&Continuation_Wrapper_12; + wrappers[13] = (nint)(delegate*)&Continuation_Wrapper_13; + wrappers[14] = (nint)(delegate*)&Continuation_Wrapper_14; + wrappers[15] = (nint)(delegate*)&Continuation_Wrapper_15; + wrappers[16] = (nint)(delegate*)&Continuation_Wrapper_16; + wrappers[17] = (nint)(delegate*)&Continuation_Wrapper_17; + wrappers[18] = (nint)(delegate*)&Continuation_Wrapper_18; + wrappers[19] = (nint)(delegate*)&Continuation_Wrapper_19; + wrappers[20] = (nint)(delegate*)&Continuation_Wrapper_20; + wrappers[21] = (nint)(delegate*)&Continuation_Wrapper_21; + wrappers[22] = (nint)(delegate*)&Continuation_Wrapper_22; + wrappers[23] = (nint)(delegate*)&Continuation_Wrapper_23; + wrappers[24] = (nint)(delegate*)&Continuation_Wrapper_24; + wrappers[25] = (nint)(delegate*)&Continuation_Wrapper_25; + wrappers[26] = (nint)(delegate*)&Continuation_Wrapper_26; + wrappers[27] = (nint)(delegate*)&Continuation_Wrapper_27; + wrappers[28] = (nint)(delegate*)&Continuation_Wrapper_28; + wrappers[29] = (nint)(delegate*)&Continuation_Wrapper_29; + wrappers[30] = (nint)(delegate*)&Continuation_Wrapper_30; + wrappers[31] = (nint)(delegate*)&Continuation_Wrapper_31; + return wrappers; + } + + [InlineArray(COUNT)] + private struct ContinuationWrapperTable + { + private nint _element; + } + + private static ContinuationWrapperTable s_continuationWrappers = InitContinuationWrappers(); + } + + private static partial class SyncPoint + { + private static unsafe void ResumeAsyncCallstacks(AsyncThreadContext context) + { + //Write recursively all the resume async callstack events. + AsyncDispatcherInfo* info = AsyncDispatcherInfo.t_current; + if (info != null) + { + ResumeRuntimeAsyncCallstacks(info, context); + } + + } + + private static unsafe void ResumeRuntimeAsyncCallstacks(AsyncDispatcherInfo* info, AsyncThreadContext context) + { + if (info != null) + { + ResumeRuntimeAsyncCallstacks(info->Next, context); + ResumeAsyncContext.Resume(ref *info, context, ResumeAsyncContext.GetId(ref *info), Config.ActiveEventKeywords); + } + } + } + + private static partial class AsyncCallstack + { + private const int MaxAsyncMethodFrameSize = Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt32Size; + + public ref struct CaptureRuntimeAsyncCallstackState + { + public Continuation? Continuation; + public ulong LastNativeIP; + public byte Count; + } + + public static bool CaptureRuntimeAsyncCallstack(byte[] buffer, ref int index, ref CaptureRuntimeAsyncCallstackState state) + { + if (index > buffer.Length || state.Continuation == null) + { + return false; + } + + byte maxAsyncCallstackFrames = (byte)Math.Min(byte.MaxValue, (buffer.Length - index) / MaxAsyncMethodFrameSize); + if (maxAsyncCallstackFrames == 0) + { + return false; + } + + ulong currentNativeIP = 0; + ulong previousNativeIP = state.LastNativeIP; + + unsafe + { + currentNativeIP = (ulong)state.Continuation.ResumeInfo->DiagnosticIP; + } + + Span callstackSpan = buffer.AsSpan(index); + int callstackSpanIndex = 0; + + // First frame (Count == 0) is written as absolute; subsequent frames + // (including the first frame of a continuation call after overflow) + // are written as deltas from the previous frame. + if (state.Count == 0) + { + callstackSpanIndex += Serializer.WriteCompressedUInt64(callstackSpan.Slice(callstackSpanIndex, Serializer.MaxCompressedUInt64Size), currentNativeIP); + } + else + { + callstackSpanIndex += Serializer.WriteCompressedInt64(callstackSpan.Slice(callstackSpanIndex, Serializer.MaxCompressedInt64Size), (long)(currentNativeIP - previousNativeIP)); + } + + callstackSpanIndex += Serializer.WriteCompressedInt32(callstackSpan.Slice(callstackSpanIndex, Serializer.MaxCompressedInt32Size), state.Continuation.State); + state.Count++; + + state.Continuation = state.Continuation.Next; + while (state.Count < maxAsyncCallstackFrames && state.Continuation != null) + { + previousNativeIP = currentNativeIP; + + unsafe + { + currentNativeIP = (ulong)state.Continuation.ResumeInfo->DiagnosticIP; + } + + callstackSpanIndex += Serializer.WriteCompressedInt64(callstackSpan.Slice(callstackSpanIndex, Serializer.MaxCompressedInt64Size), (long)(currentNativeIP - previousNativeIP)); + callstackSpanIndex += Serializer.WriteCompressedInt32(callstackSpan.Slice(callstackSpanIndex, Serializer.MaxCompressedInt32Size), state.Continuation.State); + + state.Count++; + state.Continuation = state.Continuation.Next; + } + + state.LastNativeIP = currentNativeIP; + index += callstackSpanIndex; + + return state.Continuation == null || state.Count == byte.MaxValue; + } + + public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, ulong id, Continuation? asyncCallstack) + { + EmitEvent(context, currentTimestamp, AsyncEventID.ResumeAsyncCallstack, id, AsyncCallstackType.Runtime, asyncCallstack); + } + + public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, AsyncEventID eventID, ulong id, Continuation? asyncCallstack) + { + EmitEvent(context, currentTimestamp, eventID, id, AsyncCallstackType.Runtime, asyncCallstack); + } + + public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, AsyncEventID eventID, ulong id, AsyncCallstackType type, Continuation? asyncCallstack) + { + EmitEvent(context, currentTimestamp, currentTimestamp - context.LastEventTimestamp, eventID, id, type, asyncCallstack); + } + + public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, long delta, AsyncEventID eventID, ulong id, AsyncCallstackType type, Continuation? asyncCallstack) + { + if (asyncCallstack != null) + { + ref EventBuffer eventBuffer = ref context.EventBuffer; + + // Max callstack data that can fit in the buffer after flush. + int maxCallstackBytes = Math.Min( + byte.MaxValue * MaxAsyncMethodFrameSize, + eventBuffer.Data.Length); + + CaptureRuntimeAsyncCallstackState state = default; + state.Continuation = asyncCallstack; + + // Static callstack payload: type (1) + callstackId (1) + frameCount (1) + id (max 10 bytes compressed). + const int MaxStaticEventPayloadSize = sizeof(byte) + sizeof(byte) + sizeof(byte) + Serializer.MaxCompressedUInt64Size; + + if (Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, delta, eventID, MaxStaticEventPayloadSize, out Serializer.AsyncEventHeaderRollbackData rollbackData)) + { + int frameCountOffset = CallstackHeader(ref eventBuffer, id, type, 0); + + byte[] buffer = eventBuffer.Data; + int startIndex = eventBuffer.Index; + int currentIndex = startIndex; + + if (!CaptureRuntimeAsyncCallstack(buffer, ref currentIndex, ref state)) + { + byte[]? rentedArray = RentArray(maxCallstackBytes); + if (rentedArray != null) + { + int length = currentIndex - startIndex; + int index = length; + + Buffer.BlockCopy(buffer, startIndex, rentedArray, 0, length); + CaptureRuntimeAsyncCallstack(rentedArray, ref index, ref state); + + // Rollback async event header before flushing. + Serializer.RollbackAsyncEventHeader(context, in rollbackData); + context.Flush(); + + // Write the callstack again. + if (Serializer.AsyncEventHeader(context, ref eventBuffer, context.LastEventTimestamp, 0, eventID, MaxStaticEventPayloadSize + index)) + { + CallstackHeader(ref eventBuffer, id, type, state.Count); + CallstackData(ref eventBuffer, rentedArray, index); + } + + ArrayPool.Shared.Return(rentedArray); + } + else + { + // Rollback async event header since we can't write the callstack. + Serializer.RollbackAsyncEventHeader(context, in rollbackData); + } + } + else + { + // Patch frame count in the event buffer using the offset from CallstackHeader. + eventBuffer.Data[frameCountOffset] = state.Count; + eventBuffer.Index += currentIndex - startIndex; + } + } + } + } + + private static int CallstackHeader(ref EventBuffer eventBuffer, ulong id, AsyncCallstackType type, byte callstackFrameCount) + { + // Callstack header layout: type (1 byte) + callstackId (1 byte, reserved for future use) + frameCount (1 byte) + id (max 10 bytes compressed). + const int MaxCallstackHeaderSize = sizeof(byte) + sizeof(byte) + sizeof(byte) + Serializer.MaxCompressedUInt64Size; + + ref int index = ref eventBuffer.Index; + + Span callstackHeaderSpan = eventBuffer.Data.AsSpan(index, MaxCallstackHeaderSize); + int spanIndex = 0; + + callstackHeaderSpan[spanIndex++] = (byte)type; + callstackHeaderSpan[spanIndex++] = 0; // Reserved callstack ID for future callstack interning. + + int frameCountOffset = index + spanIndex; + callstackHeaderSpan[spanIndex++] = callstackFrameCount; + + spanIndex += Serializer.WriteCompressedUInt64(callstackHeaderSpan.Slice(spanIndex), id); + eventBuffer.Index += spanIndex; + + return frameCountOffset; + } + + private static void CallstackData(ref EventBuffer eventBuffer, byte[] callstackData, int callstackDataByteCount) + { + ref int index = ref eventBuffer.Index; + Buffer.BlockCopy(callstackData, 0, eventBuffer.Data, index, callstackDataByteCount); + index += callstackDataByteCount; + } + + private static byte[]? RentArray(int minimumLength) + { + byte[]? rentedArray = null; + try + { + rentedArray = ArrayPool.Shared.Rent(minimumLength); + } + catch + { + //AsyncProfiler can't throw, return null if renting fails. + } + + return rentedArray; + } + } + } +} diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj index 0f808ede316353..fa8900ae1a3e06 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj @@ -64,6 +64,7 @@ + diff --git a/src/libraries/System.Diagnostics.Tracing/tests/BasicEventSourceTest/TestUtilities.cs b/src/libraries/System.Diagnostics.Tracing/tests/BasicEventSourceTest/TestUtilities.cs index 9de8946d9275ce..14a8b96a6a3d25 100644 --- a/src/libraries/System.Diagnostics.Tracing/tests/BasicEventSourceTest/TestUtilities.cs +++ b/src/libraries/System.Diagnostics.Tracing/tests/BasicEventSourceTest/TestUtilities.cs @@ -33,6 +33,7 @@ public static void CheckNoEventSourcesRunning(string message = "") eventSource.Name != "System.Runtime" && eventSource.Name != "System.Diagnostics.Metrics" && eventSource.Name != "Microsoft-Diagnostics-DiagnosticSource" && + eventSource.Name != "System.Runtime.CompilerServices.AsyncProfilerEventSource" && // event source from xunit runner eventSource.Name != "xUnit.TestEventSource" && diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 2c48c4cba7ffd1..1874075bdabba7 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -841,7 +841,7 @@ - + @@ -942,6 +942,8 @@ + + diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncInstrumentation.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncInstrumentation.cs index e04495632eb28b..512362ef1dcd0e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncInstrumentation.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncInstrumentation.cs @@ -10,7 +10,7 @@ namespace System.Runtime.CompilerServices { internal static class AsyncInstrumentation { - public static bool IsSupported => Debugger.IsSupported && EventSource.IsSupported; + public static bool IsSupported => Debugger.IsSupported || EventSource.IsSupported; [Flags] public enum Flags : uint @@ -30,8 +30,8 @@ public enum Flags : uint AsyncProfiler = 0x1000000, AsyncDebugger = 0x2000000, - // Bit 32 reserved for initialization state. - Uninitialized = 0x80000000 + // Bit 32 reserved for synchronization flag. + Synchronize = 0x80000000 } public const Flags DefaultFlags = @@ -49,7 +49,7 @@ public static class IsEnabled public static bool ResumeAsyncMethod(Flags flags) => (Flags.ResumeAsyncMethod & flags) != 0; public static bool CompleteAsyncMethod(Flags flags) => (Flags.CompleteAsyncMethod & flags) != 0; public static bool AsyncProfiler(Flags flags) => (Flags.AsyncProfiler & flags) != 0; - public static bool AsyncDebugger(Flags flags) => (Flags.AsyncDebugger & flags) != 0 && Task.s_asyncDebuggingEnabled; + public static bool AsyncDebugger(Flags flags) => (Flags.AsyncDebugger & flags) != 0; } public static Flags ActiveFlags => s_activeFlags; @@ -58,9 +58,9 @@ public static class IsEnabled public static Flags SyncActiveFlags() { Flags flags = s_activeFlags; - if (IsUninitialized(flags)) + if ((flags & Flags.Synchronize) != 0) { - return InitializeFlags(); + return SynchronizeFlags(); } return flags; } @@ -75,55 +75,32 @@ public static void UpdateAsyncProfilerFlags(Flags asyncProfilerFlags) lock (s_lock) { s_asyncProfilerActiveFlags = asyncProfilerFlags; - if (IsInitialized(s_activeFlags)) - { - s_activeFlags = s_asyncProfilerActiveFlags | s_asyncDebuggerActiveFlags; - } - } - } - - public static void UpdateAsyncDebuggerFlags(Flags asyncDebuggerFlags) - { - if (asyncDebuggerFlags != Flags.Disabled) - { - asyncDebuggerFlags |= Flags.AsyncDebugger; - } - - lock (s_lock) - { - s_asyncDebuggerActiveFlags = asyncDebuggerFlags; - if (IsInitialized(s_activeFlags)) - { - s_activeFlags = s_asyncProfilerActiveFlags | s_asyncDebuggerActiveFlags; - } + s_activeFlags |= Flags.Synchronize; } } - private static Flags InitializeFlags() + private static Flags SynchronizeFlags() { _ = TplEventSource.Log; // Touch TplEventSource to trigger static constructor which will initialize TPL flags if EventSource is supported. + _ = AsyncProfilerEventSource.Log; // Touch AsyncProfilerEventSource to trigger static constructor which will initialize async profiler flags if EventSource is supported. lock (s_lock) { - if (IsUninitialized(s_activeFlags)) + Flags asyncDebuggerActiveFlags = Flags.Disabled; + if (Task.s_asyncDebuggingEnabled) { - s_activeFlags = s_asyncProfilerActiveFlags | s_asyncDebuggerActiveFlags; + asyncDebuggerActiveFlags = DefaultFlags | Flags.AsyncDebugger; } + s_activeFlags = (s_asyncProfilerActiveFlags | asyncDebuggerActiveFlags) & ~Flags.Synchronize; return s_activeFlags; } } - private static bool IsInitialized(Flags flags) => !IsUninitialized(flags); - - private static bool IsUninitialized(Flags flags) => (flags & Flags.Uninitialized) != 0; - - private static Flags s_activeFlags = Flags.Uninitialized; + private static Flags s_activeFlags = Flags.Synchronize; private static Flags s_asyncProfilerActiveFlags; - private static Flags s_asyncDebuggerActiveFlags; - private static readonly Lock s_lock = new(); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs new file mode 100644 index 00000000000000..65bcd8eb835515 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs @@ -0,0 +1,1144 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Tracing; +using System.Runtime.InteropServices; +using System.Threading; +using static System.Runtime.CompilerServices.AsyncProfilerEventSource; +using Serializer = System.Runtime.CompilerServices.AsyncProfiler.EventBuffer.Serializer; + +namespace System.Runtime.CompilerServices +{ + internal static partial class AsyncProfiler + { + [Flags] + public enum AsyncCallstackType : byte + { + Compiler = 0x1, + Runtime = 0x2, + Cached = 0x80 + } + + internal enum AsyncEventID : byte + { + CreateAsyncContext = 1, + ResumeAsyncContext = 2, + SuspendAsyncContext = 3, + CompleteAsyncContext = 4, + UnwindAsyncException = 5, + CreateAsyncCallstack = 6, + ResumeAsyncCallstack = 7, + SuspendAsyncCallstack = 8, + ResumeAsyncMethod = 9, + CompleteAsyncMethod = 10, + ResetAsyncThreadContext = 11, + ResetAsyncContinuationWrapperIndex = 12, + AsyncProfilerMetadata = 13, + AsyncProfilerSyncClock = 14 + } + + internal ref struct Info + { + public object? Context; + public ref nint ContinuationTable; + public uint ContinuationIndex; + } + + internal static void InitInfo(ref Info info) + { + info.Context = null; + info.ContinuationIndex = 0; + ContinuationWrapper.InitInfo(ref info); + } + + internal static partial class Config + { + public static readonly Lock ConfigLock = new(); + + public static bool Changed(AsyncThreadContext context) => context.ConfigRevision != Revision; + + public static void Update(EventLevel logLevel, EventKeywords eventKeywords) + { + lock (ConfigLock) + { + Revision++; + + ActiveEventKeywords = 0; + if (logLevel == EventLevel.LogAlways || logLevel >= EventLevel.Informational) + { + ActiveEventKeywords = eventKeywords; + } + + string? eventBufferSizeEnv = System.Environment.GetEnvironmentVariable("DOTNET_AsyncProfilerEventSource_EventBufferSize"); + if (eventBufferSizeEnv != null && uint.TryParse(eventBufferSizeEnv, out uint eventBufferSize)) + { + eventBufferSize = Math.Max(eventBufferSize, 1024); + EventBufferSize = Math.Min(eventBufferSize, 64 * 1024 - 256); + } + + if (IsEnabled.AnyAsyncEvents(ActiveEventKeywords)) + { + AsyncThreadContextCache.EnableFlushTimer(); + AsyncThreadContextCache.DisableCleanupTimer(); + } + else + { + AsyncThreadContextCache.DisableFlushTimer(); + AsyncThreadContextCache.EnableCleanupTimer(); + } + + // Writer thread access both ActiveFlags and Revision without explicit acquire/release semantics, + // but Flags will be read before calling AcquireAsyncThreadContext that includes one volatile read + // acting as the load barrier for ActiveFlags and Revision. + Interlocked.MemoryBarrier(); + + UpdateFlags(); + } + } + + public static void EmitAsyncProfilerMetadataIfNeeded(AsyncThreadContext context) + { + if (s_metadataRevision != Revision) + { + lock (s_metadataRevisionLock) + { + if (s_metadataRevision != Revision) + { + long[] wrapperIPs = ContinuationWrapper.GetContinuationWrapperIPs(); + + // Metadata payload: + // [qpcFrequency (compressed uint64)] + // [qpcSync (compressed uint64)] + // [utcSync (compressed uint64)] + // [eventBufferSize (compressed uint32)] + // [wrapperCount byte] + // [wrapperIP0 (compressed uint64)] ... [wrapperIPn (compressed uint64)] + const int MaxStaticEventPayloadSize = Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt32Size + 1; + int maxDynamicEventPayloadSize = wrapperIPs.Length * Serializer.MaxCompressedUInt64Size; + + ref EventBuffer eventBuffer = ref context.EventBuffer; + if (Serializer.AsyncEventHeader(context, ref eventBuffer, AsyncEventID.AsyncProfilerMetadata, MaxStaticEventPayloadSize + maxDynamicEventPayloadSize)) + { + SyncClock(out long utcTimeSync, out long qpcSync); + + Span payloadSpan = eventBuffer.Data.AsSpan(eventBuffer.Index, MaxStaticEventPayloadSize + maxDynamicEventPayloadSize); + int payloadSpanIndex = 0; + + payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan.Slice(payloadSpanIndex), (ulong)Stopwatch.Frequency); + payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan.Slice(payloadSpanIndex), (ulong)qpcSync); + payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan.Slice(payloadSpanIndex), (ulong)utcTimeSync); + payloadSpanIndex += Serializer.WriteCompressedUInt32(payloadSpan.Slice(payloadSpanIndex), EventBufferSize); + + payloadSpan[payloadSpanIndex++] = (byte)wrapperIPs.Length; + + for (int i = 0; i < wrapperIPs.Length; i++) + { + payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan.Slice(payloadSpanIndex), (ulong)wrapperIPs[i]); + } + + eventBuffer.Index += payloadSpanIndex; + + // Force flush to deliver event promptly. + context.Flush(); + + s_metadataRevision = Revision; + s_lastSyncClockEventTimestamp = Stopwatch.GetTimestamp(); + } + } + } + } + } + + public static void EmitSyncClockEventIfNeeded() + { + long currentTimestamp = Stopwatch.GetTimestamp(); + if (s_lastSyncClockEventTimestamp == 0) + { + s_lastSyncClockEventTimestamp = currentTimestamp; + return; + } + + if (currentTimestamp - s_lastSyncClockEventTimestamp < s_intervalBetweenSyncClockEvent) + { + return; + } + + s_lastSyncClockEventTimestamp = currentTimestamp; + + if (IsEnabled.AnyAsyncEvents(ActiveEventKeywords)) + { + AsyncThreadContext transientContext = AsyncThreadContext.AcquireTransient(); + + // SyncClock payload: + // [qpcSync (compressed uint64)] + // [utcSync (compressed uint64)] + const int MaxEventPayloadSize = Serializer.MaxCompressedUInt64Size + Serializer.MaxCompressedUInt64Size; + + ref EventBuffer eventBuffer = ref transientContext.EventBuffer; + if (Serializer.AsyncEventHeader(transientContext, ref eventBuffer, AsyncEventID.AsyncProfilerSyncClock, MaxEventPayloadSize)) + { + SyncClock(out long utcTimeSync, out long qpcSync); + + Span payloadSpan = eventBuffer.Data.AsSpan(eventBuffer.Index, MaxEventPayloadSize); + int payloadSpanIndex = 0; + + payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan.Slice(payloadSpanIndex), (ulong)qpcSync); + payloadSpanIndex += Serializer.WriteCompressedUInt64(payloadSpan.Slice(payloadSpanIndex), (ulong)utcTimeSync); + + eventBuffer.Index += payloadSpanIndex; + + // Force flush to deliver event promptly. + transientContext.Flush(); + } + + AsyncThreadContext.Release(transientContext); + } + } + + private static void SyncClock(out long utcTimeSync, out long qpcSync) + { + long qpcDiff = long.MaxValue; + + utcTimeSync = 0; + qpcSync = 0; + + // Run calibration loop to find the closest QPC timestamp to UTC timestamp. + // This is a best effort to minimize the max error between QPC and UTC timestamps. + for (int i = 0; i < 10; i++) + { + long qpc1 = Stopwatch.GetTimestamp(); + long utcTime = DateTime.UtcNow.ToFileTimeUtc(); + long qpc2 = Stopwatch.GetTimestamp(); + long diff = qpc2 - qpc1; + + if (diff < qpcDiff) + { + utcTimeSync = utcTime; + qpcSync = qpc1; + qpcDiff = diff; + } + } + + // QPC and UTC clocks are not guaranteed to be perfectly linear, so this is a best effort to minimize the max error. + // Both QPC and DateTime.UtcNow should have a 100ns resolution (or better). If latency getting QPC and UTC time is small enough, + // the error introduced by non-linearity should be within 100ns. + qpcSync += qpcDiff / 2; + } + + private static void UpdateFlags() + { + AsyncInstrumentation.Flags flags = AsyncInstrumentation.Flags.Disabled; + flags |= IsEnabled.CreateAsyncContextEvent(ActiveEventKeywords) || IsEnabled.CreateAsyncCallstackEvent(ActiveEventKeywords) ? AsyncInstrumentation.Flags.CreateAsyncContext : AsyncInstrumentation.Flags.Disabled; + flags |= IsEnabled.ResumeAsyncContextEvent(ActiveEventKeywords) || IsEnabled.ResumeAsyncCallstackEvent(ActiveEventKeywords) ? AsyncInstrumentation.Flags.ResumeAsyncContext : AsyncInstrumentation.Flags.Disabled; + flags |= IsEnabled.SuspendAsyncContextEvent(ActiveEventKeywords) || IsEnabled.SuspendAsyncCallstackEvent(ActiveEventKeywords) ? AsyncInstrumentation.Flags.SuspendAsyncContext : AsyncInstrumentation.Flags.Disabled; + flags |= IsEnabled.CompleteAsyncContextEvent(ActiveEventKeywords) ? AsyncInstrumentation.Flags.CompleteAsyncContext : AsyncInstrumentation.Flags.Disabled; + flags |= IsEnabled.UnwindAsyncExceptionEvent(ActiveEventKeywords) ? AsyncInstrumentation.Flags.UnwindAsyncException : AsyncInstrumentation.Flags.Disabled; + flags |= IsEnabled.ResumeAsyncMethodEvent(ActiveEventKeywords) ? AsyncInstrumentation.Flags.ResumeAsyncMethod : AsyncInstrumentation.Flags.Disabled; + flags |= IsEnabled.CompleteAsyncMethodEvent(ActiveEventKeywords) ? AsyncInstrumentation.Flags.CompleteAsyncMethod : AsyncInstrumentation.Flags.Disabled; + + AsyncInstrumentation.UpdateAsyncProfilerFlags(flags); + } + + public static void CaptureState() + { + AsyncThreadContextCache.Flush(true); + } + + public static EventKeywords ActiveEventKeywords { get; private set; } + + public static uint Revision { get; private set; } + + // Use 16KB - 256 event buffer as default. 256 bytes reserved for event header. + // 16KB events pack cleanly into a 64KB ETW/EventPipe/UserEvents buffer. + public static uint EventBufferSize { get; private set; } = 16 * 1024 - 256; + + private static readonly Lock s_metadataRevisionLock = new(); + + private static uint s_metadataRevision; + + private static long s_lastSyncClockEventTimestamp; + + private static readonly long s_intervalBetweenSyncClockEvent = Stopwatch.Frequency * 60; // 1 minute + } + + internal struct EventBuffer + { + public byte[] Data; + + public int Index; + + public uint EventCount; + + public static class Serializer + { + public const int MaxCompressedUInt32Size = 5; + public const int MaxCompressedInt32Size = 5; + public const int MaxCompressedUInt64Size = 10; + public const int MaxCompressedInt64Size = 10; + public const int MaxEventHeaderSize = 37; + public const int MaxAsyncEventHeaderSize = 11; + + public ref struct AsyncEventHeaderRollbackData + { + public int Index; + public uint EventCount; + public long LastEventTimestamp; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteCompressedInt32(Span buffer, int value) + { + return WriteCompressedUInt32(buffer, ZigzagEncodeInt32(value)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteCompressedUInt32(Span buffer, uint value) + { + if (buffer.Length < MaxCompressedUInt32Size) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); + } + + ref byte dst = ref MemoryMarshal.GetReference(buffer); + int index = 0; + + while (value > 0x7Fu) + { + Unsafe.Add(ref dst, index++) = (byte)((uint)value | ~0x7Fu); + value >>= 7; + } + + Unsafe.Add(ref dst, index++) = (byte)value; + return index; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteCompressedInt64(Span buffer, long value) + { + return WriteCompressedUInt64(buffer, ZigzagEncodeInt64(value)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteCompressedUInt64(Span buffer, ulong value) + { + if (buffer.Length < MaxCompressedUInt64Size) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); + } + + ref byte dst = ref MemoryMarshal.GetReference(buffer); + int index = 0; + + while (value > 0x7Fu) + { + Unsafe.Add(ref dst, index++) = (byte)((uint)value | ~0x7Fu); + value >>= 7; + } + + Unsafe.Add(ref dst, index++) = (byte)value; + return index; + } + + public static uint ZigzagEncodeInt32(int value) => (uint)((value << 1) ^ (value >> 31)); + + public static ulong ZigzagEncodeInt64(long value) => (ulong)((value << 1) ^ (value >> 63)); + + public static void Header(AsyncThreadContext context, ref EventBuffer eventBuffer) + { + long currentTimestamp = Stopwatch.GetTimestamp(); + + eventBuffer.Index = 0; + eventBuffer.EventCount = 0; + context.LastEventTimestamp = currentTimestamp; + + Span headerSpan = eventBuffer.Data.AsSpan(0, MaxEventHeaderSize); + int headerSpanIndex = 0; + + // Version + headerSpan[headerSpanIndex++] = 1; + + // Total size in bytes, will be updated on flush. + BinaryPrimitives.WriteUInt32LittleEndian(headerSpan.Slice(headerSpanIndex), 0); + headerSpanIndex += sizeof(uint); + + // Async Thread Context ID + BinaryPrimitives.WriteUInt32LittleEndian(headerSpan.Slice(headerSpanIndex), context.AsyncThreadContextId); + headerSpanIndex += sizeof(uint); + + // OS Thread ID + BinaryPrimitives.WriteUInt64LittleEndian(headerSpan.Slice(headerSpanIndex), context.OsThreadId); + headerSpanIndex += sizeof(ulong); + + // Total event count, will be updated on flush. + BinaryPrimitives.WriteUInt32LittleEndian(headerSpan.Slice(headerSpanIndex), 0); + headerSpanIndex += sizeof(uint); + + // Start timestamp + BinaryPrimitives.WriteUInt64LittleEndian(headerSpan.Slice(headerSpanIndex), (ulong)currentTimestamp); + headerSpanIndex += sizeof(ulong); + + // End timestamp, will be updated on flush. + BinaryPrimitives.WriteUInt64LittleEndian(headerSpan.Slice(headerSpanIndex), 0); + headerSpanIndex += sizeof(ulong); + + eventBuffer.Index = headerSpanIndex; + } + + public static bool AsyncEventHeader(AsyncThreadContext context, ref EventBuffer eventBuffer, AsyncEventID eventID, int maxEventPayloadSize) + { + long currentTimestamp = Stopwatch.GetTimestamp(); + long delta = currentTimestamp - context.LastEventTimestamp; + return AsyncEventHeader(context, ref eventBuffer, currentTimestamp, delta, eventID, maxEventPayloadSize); + } + + public static bool AsyncEventHeader(AsyncThreadContext context, ref EventBuffer eventBuffer, long currentTimestamp, AsyncEventID eventID, int maxEventPayloadSize) + { + long delta = currentTimestamp - context.LastEventTimestamp; + return AsyncEventHeader(context, ref eventBuffer, currentTimestamp, delta, eventID, maxEventPayloadSize); + } + + public static bool AsyncEventHeader(AsyncThreadContext context, ref EventBuffer eventBuffer, long currentTimestamp, long delta, AsyncEventID eventID, int maxEventPayloadSize, out AsyncEventHeaderRollbackData rollbackData) + { + byte[] buffer = eventBuffer.Data; + int index = eventBuffer.Index; + long previousTimestamp = context.LastEventTimestamp; + + if ((index + MaxAsyncEventHeaderSize + maxEventPayloadSize) <= buffer.Length && delta >= 0) + { + context.LastEventTimestamp = currentTimestamp; + } + else + { + // Event is too big for buffer, drop it. + if (MaxAsyncEventHeaderSize + maxEventPayloadSize > buffer.Length) + { + rollbackData = default; + return false; + } + + context.Flush(); + + previousTimestamp = context.LastEventTimestamp; + delta = 0; + index = eventBuffer.Index; + } + + // Capture state after potential flush but before writing the header. + rollbackData = new AsyncEventHeaderRollbackData + { + Index = index, + EventCount = eventBuffer.EventCount, + LastEventTimestamp = previousTimestamp, + }; + + Span headerSpan = buffer.AsSpan(index, MaxAsyncEventHeaderSize); + int headerSpanIndex = 0; + + headerSpan[headerSpanIndex++] = (byte)eventID; // eventID + headerSpanIndex += WriteCompressedUInt64(headerSpan.Slice(headerSpanIndex), (ulong)delta); // Timestamp delta from last event + + eventBuffer.Index += headerSpanIndex; + eventBuffer.EventCount++; + + return true; + } + + public static bool AsyncEventHeader(AsyncThreadContext context, ref EventBuffer eventBuffer, long currentTimestamp, long delta, AsyncEventID eventID, int maxEventPayloadSize) + { + byte[] buffer = eventBuffer.Data; + int index = eventBuffer.Index; + + if ((index + MaxAsyncEventHeaderSize + maxEventPayloadSize) <= buffer.Length && delta >= 0) + { + context.LastEventTimestamp = currentTimestamp; + } + else + { + // Event is too big for buffer, drop it. + if (MaxAsyncEventHeaderSize + maxEventPayloadSize > buffer.Length) + { + return false; + } + + context.Flush(); + + delta = 0; + index = eventBuffer.Index; + } + + Span headerSpan = buffer.AsSpan(index, MaxAsyncEventHeaderSize); + int headerSpanIndex = 0; + + headerSpan[headerSpanIndex++] = (byte)eventID; // eventID + headerSpanIndex += WriteCompressedUInt64(headerSpan.Slice(headerSpanIndex), (ulong)delta); // Timestamp delta from last event + + eventBuffer.Index += headerSpanIndex; + eventBuffer.EventCount++; + + return true; + } + + public static void RollbackAsyncEventHeader(AsyncThreadContext context, in AsyncEventHeaderRollbackData rollbackData) + { + ref EventBuffer eventBuffer = ref context.EventBuffer; + eventBuffer.Index = rollbackData.Index; + eventBuffer.EventCount = rollbackData.EventCount; + context.LastEventTimestamp = rollbackData.LastEventTimestamp; + } + } + } + + internal sealed class AsyncThreadContext + { + private static uint s_nextAsyncThreadContextId; + + public AsyncThreadContext() + { + _eventBuffer.Data = Array.Empty(); + AsyncThreadContextId = Interlocked.Increment(ref s_nextAsyncThreadContextId); + OsThreadId = Thread.CurrentOSThreadId; + } + + private EventBuffer _eventBuffer; + + public long LastEventTimestamp; + + public EventKeywords ActiveEventKeywords; + + public readonly ulong OsThreadId; + + public readonly uint AsyncThreadContextId; + + public uint ConfigRevision; + + public volatile bool InUse; + + public volatile bool BlockContext; + + public ref EventBuffer EventBuffer + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if (_eventBuffer.Data.Length == 0) + { + InitializeBuffer(); + } + + Debug.Assert(InUse || BlockContext); + return ref _eventBuffer; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static AsyncThreadContext Acquire(ref Info info) + { + AsyncThreadContext context = Get(ref info); + Debug.Assert(!context.InUse); + + context.InUse = true; + if (context.BlockContext) + { + WaitOnBlockedAsyncThreadContext(context); + } + + return context; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Release(AsyncThreadContext context) + { + Debug.Assert(context.InUse); + context.InUse = false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static AsyncThreadContext Get() + { + AsyncThreadContext? context = t_asyncThreadContext; + if (context != null) + { + return context; + } + + return CreateAsyncThreadContext(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static AsyncThreadContext Get(ref Info info) + { + Debug.Assert(info.Context == null || info.Context is AsyncThreadContext); + + AsyncThreadContext? context = Unsafe.As(info.Context); + if (context != null) + { + return context; + } + + return GetAsyncThreadContext(ref info); + } + + public static AsyncThreadContext AcquireTransient() + { + AsyncThreadContext? context; + if (t_asyncThreadContext != null) + { + context = Get(); + Debug.Assert(!context.InUse); + } + else + { + context = new AsyncThreadContext(); + context.ConfigRevision = Config.Revision; + context.ActiveEventKeywords = Config.ActiveEventKeywords; + } + + context.InUse = true; + if (context.BlockContext) + { + WaitOnBlockedAsyncThreadContext(context); + } + + return context; + } + + public void Reclaim() + { + Debug.Assert(InUse || BlockContext); + + _eventBuffer.Data = Array.Empty(); + _eventBuffer.Index = 0; + _eventBuffer.EventCount = 0; + } + + public void Flush() + { + Debug.Assert(InUse || BlockContext); + + if (_eventBuffer.EventCount == 0) + { + return; + } + + ref EventBuffer eventBuffer = ref EventBuffer; + + Span headerSpan = eventBuffer.Data.AsSpan(0, Serializer.MaxEventHeaderSize); + + int spanIndex = 1; // Skip version + + // Fill in total size in header before flushing. + BinaryPrimitives.WriteUInt32LittleEndian(headerSpan.Slice(spanIndex), (uint)eventBuffer.Index); + spanIndex += sizeof(uint); + + spanIndex += sizeof(uint) + sizeof(ulong); // Skip AsyncThreadContextId and OSThreadId + + // Fill in event count in header before flushing. + BinaryPrimitives.WriteUInt32LittleEndian(headerSpan.Slice(spanIndex), eventBuffer.EventCount); + spanIndex += sizeof(uint); + + spanIndex += sizeof(ulong); // Skip start timestamp + + // Fill in end timestamp in header before flushing. + BinaryPrimitives.WriteUInt64LittleEndian(headerSpan.Slice(spanIndex), (ulong)LastEventTimestamp); + + try + { + Log.AsyncEvents(eventBuffer.Data.AsSpan(0, eventBuffer.Index)); + } + catch + { + // AsyncProfiler can't throw, ignore exception and lose buffer. + } + + Serializer.Header(this, ref eventBuffer); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void InitializeBuffer() + { + try + { + _eventBuffer.Data = new byte[Config.EventBufferSize]; + Serializer.Header(this, ref _eventBuffer); + } + catch + { + // Async Profiler can't throw, ignore exception and use empty buffer. + // This will cause event to drop and attempt to reallocate buffer on next event. + _eventBuffer.Data = Array.Empty(); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void WaitOnBlockedAsyncThreadContext(AsyncThreadContext context) + { + context.InUse = false; + // Intentionally acquire and release CacheLock to wait for the flush thread + // to finish any work that is currently synchronized on this lock. + lock (AsyncThreadContextCache.CacheLock) { } + context.InUse = true; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static AsyncThreadContext GetAsyncThreadContext(ref Info info) + { + AsyncThreadContext context = Get(); + info.Context = t_asyncThreadContext; + return context; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static AsyncThreadContext CreateAsyncThreadContext() + { + AsyncThreadContext context = new AsyncThreadContext(); + AsyncThreadContextCache.Add(context); + t_asyncThreadContext = context; + return context; + } + + [ThreadStatic] + private static AsyncThreadContext? t_asyncThreadContext; + } + + internal static partial class CreateAsyncContext + { + public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, ulong id) + { + const int MaxEventPayloadSize = Serializer.MaxCompressedUInt64Size; + + ref EventBuffer eventBuffer = ref context.EventBuffer; + if (Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, AsyncEventID.CreateAsyncContext, MaxEventPayloadSize)) + { + eventBuffer.Index += Serializer.WriteCompressedUInt64(eventBuffer.Data.AsSpan(eventBuffer.Index, MaxEventPayloadSize), id); + } + } + } + + internal static partial class ResumeAsyncContext + { + public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, ulong id) + { + const int MaxEventPayloadSize = Serializer.MaxCompressedUInt64Size; + + ref EventBuffer eventBuffer = ref context.EventBuffer; + if (Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, AsyncEventID.ResumeAsyncContext, MaxEventPayloadSize)) + { + eventBuffer.Index += Serializer.WriteCompressedUInt64(eventBuffer.Data.AsSpan(eventBuffer.Index, MaxEventPayloadSize), id); + } + } + } + + internal static partial class SuspendAsyncContext + { + public static void EmitEvent(AsyncThreadContext context, long currentTimestamp) + { + Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.SuspendAsyncContext, 0); + } + } + + internal static partial class CompleteAsyncContext + { + public static void Complete(ref Info info) + { + AsyncThreadContext context = AsyncThreadContext.Acquire(ref info); + + SyncPoint.Check(context); + + if (IsEnabled.CompleteAsyncContextEvent(context.ActiveEventKeywords)) + { + EmitEvent(context, Stopwatch.GetTimestamp()); + } + + AsyncThreadContext.Release(context); + } + + public static void EmitEvent(AsyncThreadContext context, long currentTimestamp) + { + Serializer.AsyncEventHeader(context, ref context.EventBuffer, currentTimestamp, AsyncEventID.CompleteAsyncContext, 0); + } + } + + internal static partial class AsyncMethodException + { + public static void Unhandled(ref Info info, uint unwindedFrames) + { + AsyncThreadContext context = AsyncThreadContext.Acquire(ref info); + + SyncPoint.Check(context); + + EventKeywords activeEventKeywords = context.ActiveEventKeywords; + if (IsEnabled.AnyAsyncEvents(activeEventKeywords)) + { + long currentTimestamp = Stopwatch.GetTimestamp(); + if (IsEnabled.UnwindAsyncExceptionEvent(activeEventKeywords)) + { + EmitEvent(context, currentTimestamp, unwindedFrames); + } + + if (IsEnabled.CompleteAsyncContextEvent(activeEventKeywords)) + { + CompleteAsyncContext.EmitEvent(context, currentTimestamp); + } + } + + AsyncThreadContext.Release(context); + } + + public static void Handled(ref Info info, uint unwindedFrames) + { + AsyncThreadContext context = AsyncThreadContext.Acquire(ref info); + + SyncPoint.Check(context); + if (IsEnabled.UnwindAsyncExceptionEvent(context.ActiveEventKeywords)) + { + EmitEvent(context, Stopwatch.GetTimestamp(), unwindedFrames); + } + + AsyncThreadContext.Release(context); + } + + public static void EmitEvent(AsyncThreadContext context, long currentTimestamp, uint unwindedFrames) + { + // unwinded frames + const int MaxEventPayloadSize = Serializer.MaxCompressedUInt32Size; + + ref EventBuffer eventBuffer = ref context.EventBuffer; + if (Serializer.AsyncEventHeader(context, ref eventBuffer, currentTimestamp, AsyncEventID.UnwindAsyncException, MaxEventPayloadSize)) + { + eventBuffer.Index += Serializer.WriteCompressedUInt32(eventBuffer.Data.AsSpan(eventBuffer.Index, MaxEventPayloadSize), unwindedFrames); + } + } + } + + internal static partial class ResumeAsyncMethod + { + public static void Resume(ref Info info) + { + AsyncThreadContext context = AsyncThreadContext.Acquire(ref info); + + SyncPoint.Check(context); + if (IsEnabled.ResumeAsyncMethodEvent(context.ActiveEventKeywords)) + { + EmitEvent(context); + } + + AsyncThreadContext.Release(context); + } + + public static void EmitEvent(AsyncThreadContext context) + { + Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResumeAsyncMethod, 0); + } + } + + internal static partial class CompleteAsyncMethod + { + public static void Complete(ref Info info) + { + AsyncThreadContext context = AsyncThreadContext.Acquire(ref info); + + SyncPoint.Check(context); + if (IsEnabled.CompleteAsyncMethodEvent(context.ActiveEventKeywords)) + { + EmitEvent(context); + } + + AsyncThreadContext.Release(context); + } + + public static void EmitEvent(AsyncThreadContext context) + { + Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.CompleteAsyncMethod, 0); + } + } + + internal static partial class ContinuationWrapper + { + public const byte COUNT = 32; + public const byte COUNT_MASK = COUNT - 1; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void IncrementIndex(ref Info info) + { + info.ContinuationIndex++; + if ((info.ContinuationIndex & COUNT_MASK) == 0) + { + ResetIndex(ref info); + } + } + + public static void UnwindIndex(ref Info info, uint unwindedFrames) + { + uint oldIndex = info.ContinuationIndex; + info.ContinuationIndex += unwindedFrames; + + if ((oldIndex & ~COUNT_MASK) != (info.ContinuationIndex & ~COUNT_MASK)) + { + ResetIndex(ref info); + } + } + + private static void ResetIndex(ref Info info) + { + AsyncThreadContext context = AsyncThreadContext.Acquire(ref info); + + SyncPoint.Check(context); + if (IsEnabled.AnyAsyncEvents(context.ActiveEventKeywords)) + { + EmitEvent(context); + } + + AsyncThreadContext.Release(context); + } + + private static void EmitEvent(AsyncThreadContext context) + { + Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResetAsyncContinuationWrapperIndex, 0); + } + } + + private static partial class SyncPoint + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Check(AsyncThreadContext context) + { + if (Config.Changed(context)) + { + ResetContext(context); + return true; + } + return false; + } + + private static void ResetContext(AsyncThreadContext context) + { + context.Flush(); + + context.ConfigRevision = Config.Revision; + context.ActiveEventKeywords = Config.ActiveEventKeywords; + + if (IsEnabled.AnyAsyncEvents(context.ActiveEventKeywords)) + { + Config.EmitAsyncProfilerMetadataIfNeeded(context); + EmitEvent(context); + } + + ResumeAsyncCallstacks(context); + } + + private static void EmitEvent(AsyncThreadContext context) + { + Serializer.AsyncEventHeader(context, ref context.EventBuffer, AsyncEventID.ResetAsyncThreadContext, 0); + } + } + + private static class IsEnabled + { + public static bool CreateAsyncContextEvent(EventKeywords eventKeywords) => (eventKeywords & Keywords.CreateAsyncContext) != 0; + public static bool ResumeAsyncContextEvent(EventKeywords eventKeywords) => (eventKeywords & Keywords.ResumeAsyncContext) != 0; + public static bool SuspendAsyncContextEvent(EventKeywords eventKeywords) => (eventKeywords & Keywords.SuspendAsyncContext) != 0; + public static bool CompleteAsyncContextEvent(EventKeywords eventKeywords) => (eventKeywords & Keywords.CompleteAsyncContext) != 0; + public static bool UnwindAsyncExceptionEvent(EventKeywords eventKeywords) => (eventKeywords & Keywords.UnwindAsyncException) != 0; + public static bool CreateAsyncCallstackEvent(EventKeywords eventKeywords) => (eventKeywords & Keywords.CreateAsyncCallstack) != 0; + public static bool ResumeAsyncCallstackEvent(EventKeywords eventKeywords) => (eventKeywords & Keywords.ResumeAsyncCallstack) != 0; + public static bool SuspendAsyncCallstackEvent(EventKeywords eventKeywords) => (eventKeywords & Keywords.SuspendAsyncCallstack) != 0; + public static bool ResumeAsyncMethodEvent(EventKeywords eventKeywords) => (eventKeywords & Keywords.ResumeAsyncMethod) != 0; + public static bool CompleteAsyncMethodEvent(EventKeywords eventKeywords) => (eventKeywords & Keywords.CompleteAsyncMethod) != 0; + public static bool AnyAsyncEvents(EventKeywords eventKeywords) => (eventKeywords & AsyncEventKeywords) != 0; + } + + private static class AsyncThreadContextCache + { + public static Lock CacheLock { get; private set; } = new Lock(); + + public static void Add(AsyncThreadContext context) + { + AsyncThreadContextHolder contextHolder = new AsyncThreadContextHolder(context, Thread.CurrentThread); + lock (CacheLock) + { + s_cache.Add(contextHolder); + } + } + + public static void Flush(bool force) + { + lock (CacheLock) + { + FlushCore(force); + } + } + + public static void EnableFlushTimer() + { + lock (CacheLock) + { + s_flushTimer ??= new Timer(PeriodicFlush, null, Timeout.Infinite, Timeout.Infinite, false); + s_flushTimer.Change(AsyncThreadContextCacheFlushTimerIntervalMs, Timeout.Infinite); + } + } + + public static void DisableFlushTimer() + { + lock (CacheLock) + { + s_flushTimer?.Change(Timeout.Infinite, Timeout.Infinite); + } + } + + public static void EnableCleanupTimer() + { + lock (CacheLock) + { + s_cleanupTimer ??= new Timer(Cleanup, null, Timeout.Infinite, Timeout.Infinite, false); + s_cleanupTimer?.Change(AsyncThreadContextCacheCleanupTimerIntervalMs, Timeout.Infinite); + } + } + + public static void DisableCleanupTimer() + { + lock (CacheLock) + { + s_cleanupTimer?.Change(Timeout.Infinite, Timeout.Infinite); + } + } + + private static void Cleanup(object? state) + { + _ = state; + + lock (CacheLock) + { + FlushCore(true); + + if (s_cache.Count > 0) + { + // Restart cleanup timer. + s_cleanupTimer?.Change(AsyncThreadContextCacheCleanupTimerIntervalMs, Timeout.Infinite); + } + } + } + + private static void PeriodicFlush(object? state) + { + _ = state; + + lock (CacheLock) + { + FlushCore(false); + + if (IsEnabled.AnyAsyncEvents(Config.ActiveEventKeywords)) + { + // Restart flush timer. + s_flushTimer?.Change(AsyncThreadContextCacheFlushTimerIntervalMs, Timeout.Infinite); + } + else + { + // Start cleanup timer. + s_cleanupTimer?.Change(AsyncThreadContextCacheCleanupTimerIntervalMs, Timeout.Infinite); + } + } + } + + private static void FlushCore(bool force) + { + // Make sure all dead threads are flushed and removed from the cache. + for (int i = s_cache.Count - 1; i >= 0; i--) + { + AsyncThreadContextHolder contextHolder = s_cache[i]; + if (!contextHolder.OwnerThread.TryGetTarget(out Thread? target) || !target.IsAlive) + { + // Thread is dead, flush its buffer and remove from cache. + AsyncThreadContext context = contextHolder.Context; + + Debug.Assert(!context.InUse); + context.InUse = true; + + context.Flush(); + + context.Reclaim(); + s_cache.RemoveAt(i); + + context.InUse = false; + } + } + + long frequency = Stopwatch.Frequency; + + // Look at live threads, only flush if forced or contexts that have been idle for 250 milliseconds. + long idleWriteTimestamp = Stopwatch.GetTimestamp() - (frequency / 4); + + // Additionally, reclaim buffers for contexts that have been idle for 30 seconds to avoid keeping + // large buffers around indefinitely for threads that are no longer running async code. + long idleReclaimBufferTimestamp = Stopwatch.GetTimestamp() - frequency * 30; + + // Spin wait timeout, 100 milliseconds. + long spinWaitTimeout = frequency / 10; + + foreach (AsyncThreadContextHolder contextHolder in s_cache) + { + AsyncThreadContext context = contextHolder.Context; + + // Read LastEventTimestamp without atomics, could cause teared reads but not critical. + long lastEventWriteTimestamp = context.LastEventTimestamp; + if (force || lastEventWriteTimestamp < idleWriteTimestamp) + { + context.BlockContext = true; + SpinWait sw = default; + long timeout = Stopwatch.GetTimestamp() + spinWaitTimeout; + while (context.InUse) + { + sw.SpinOnce(); + if (Stopwatch.GetTimestamp() > timeout) + { + // AsyncThreadContext has been busy for too long, skip flushing this time. + // NOTE, this should not happen under normal conditions, contexts are only + // held InUse for a very short time writing events. If this do happen then + // then write probably triggered a flush or thread have been preempted for + // a long time while holding the context. Either way, skipping flush this time + // should be ok, as the next flush will pick it up and flushing is best effort. + break; + } + } + + if (!context.InUse) + { + context.Flush(); + + if (force || lastEventWriteTimestamp < idleReclaimBufferTimestamp) + { + context.Reclaim(); + } + } + + context.BlockContext = false; + } + } + + Config.EmitSyncClockEventIfNeeded(); + } + + private sealed class AsyncThreadContextHolder + { + public AsyncThreadContextHolder(AsyncThreadContext context, Thread ownerThread) + { + Context = context; + OwnerThread = new WeakReference(ownerThread); + } + + public readonly AsyncThreadContext Context; + public readonly WeakReference OwnerThread; + } + + private const int AsyncThreadContextCacheFlushTimerIntervalMs = 1000; + private static Timer? s_flushTimer; + + private const int AsyncThreadContextCacheCleanupTimerIntervalMs = 30000; + private static Timer? s_cleanupTimer; + + private static List s_cache = new List(); + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfilerEventSource.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfilerEventSource.cs new file mode 100644 index 00000000000000..7d61ce2f5cb75f --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfilerEventSource.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Tracing; + +namespace System.Runtime.CompilerServices +{ + /// Provides an event source for tracing async execution. + [EventSource(Name = "System.Runtime.CompilerServices.AsyncProfilerEventSource")] + internal sealed partial class AsyncProfilerEventSource : EventSource + { + private const string EventSourceSuppressMessage = "Parameters to this method are primitive and are trimmer safe"; + + public static readonly AsyncProfilerEventSource Log = new AsyncProfilerEventSource(); + + public static class Keywords // this name is important for EventSource + { + public const EventKeywords CreateAsyncContext = (EventKeywords)0x1; + public const EventKeywords ResumeAsyncContext = (EventKeywords)0x2; + public const EventKeywords SuspendAsyncContext = (EventKeywords)0x4; + public const EventKeywords CompleteAsyncContext = (EventKeywords)0x8; + public const EventKeywords UnwindAsyncException = (EventKeywords)0x10; + public const EventKeywords CreateAsyncCallstack = (EventKeywords)0x20; + public const EventKeywords ResumeAsyncCallstack = (EventKeywords)0x40; + public const EventKeywords SuspendAsyncCallstack = (EventKeywords)0x80; + public const EventKeywords ResumeAsyncMethod = (EventKeywords)0x100; + public const EventKeywords CompleteAsyncMethod = (EventKeywords)0x200; + } + + public const EventKeywords AsyncEventKeywords = + Keywords.CreateAsyncContext | + Keywords.ResumeAsyncContext | + Keywords.SuspendAsyncContext | + Keywords.CompleteAsyncContext | + Keywords.CreateAsyncCallstack | + Keywords.ResumeAsyncCallstack | + Keywords.SuspendAsyncCallstack | + Keywords.UnwindAsyncException | + Keywords.ResumeAsyncMethod | + Keywords.CompleteAsyncMethod; + + public const int FlushCommand = 1; + + //----------------------- Event IDs (must be unique) ----------------------- + public const int ASYNC_EVENTS_ID = 1; + + //----------------------------------------------------------------------------------- + // + // Events + // + [Event( + ASYNC_EVENTS_ID, + Version = 1, + Opcode = EventOpcode.Info, + Level = EventLevel.Informational, + Keywords = AsyncEventKeywords, + Message = "")] + public void AsyncEvents(byte[] buffer) + { + AsyncEvents(buffer.AsSpan()); + } + + [NonEvent] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern", Justification = EventSourceSuppressMessage)] + public void AsyncEvents(ReadOnlySpan buffer) + { + unsafe + { + fixed (byte* pBuffer = buffer) + { + int length = buffer.Length; + EventData* eventPayload = stackalloc EventData[2]; + eventPayload[0].Size = sizeof(int); + eventPayload[0].DataPointer = ((IntPtr)(&length)); + eventPayload[0].Reserved = 0; + eventPayload[1].Size = sizeof(byte) * length; + eventPayload[1].DataPointer = length != 0 ? ((IntPtr)pBuffer) : ((IntPtr)(&length)); + eventPayload[1].Reserved = 0; + WriteEventCore(ASYNC_EVENTS_ID, 2, eventPayload); + } + } + } + + /// + /// Get callbacks when the ETW sends us commands + /// + protected override void OnEventCommand(EventCommandEventArgs command) + { + if (command.Command == (EventCommand)FlushCommand || command.Command == EventCommand.SendManifest) + { + AsyncProfiler.Config.CaptureState(); + return; + } + + AsyncProfiler.Config.Update(m_level, m_matchAnyKeyword); + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TplEventSource.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TplEventSource.cs index 89e537c74e92eb..90267e2eb8d031 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TplEventSource.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TplEventSource.cs @@ -34,26 +34,6 @@ protected override void OnEventCommand(EventCommandEventArgs command) Debug = IsEnabled(EventLevel.Informational, Keywords.Debug); DebugActivityId = IsEnabled(EventLevel.Informational, Keywords.DebugActivityId); - - // Until debugger explicitly set the AsyncInstrumentation keyword, we enable async instrumentation when - // Tasks, AsyncCausalitySynchronousWork, AsyncCausalityOperation and TasksFlowActivityIds keywords are enabled. - bool asyncInstrumentationEnabled = IsEnabled(EventLevel.Informational, Keywords.AsyncInstrumentation); - if (!asyncInstrumentationEnabled) - { - asyncInstrumentationEnabled = IsEnabled(EventLevel.Informational, Keywords.Tasks); - asyncInstrumentationEnabled &= IsEnabled(EventLevel.Informational, Keywords.AsyncCausalityOperation); - asyncInstrumentationEnabled &= IsEnabled(EventLevel.Informational, Keywords.AsyncCausalitySynchronousWork); - asyncInstrumentationEnabled &= IsEnabled(EventLevel.Informational, Keywords.TasksFlowActivityIds); - } - - if (asyncInstrumentationEnabled) - { - AsyncInstrumentation.UpdateAsyncDebuggerFlags(AsyncInstrumentation.DefaultFlags); - } - else - { - AsyncInstrumentation.UpdateAsyncDebuggerFlags(AsyncInstrumentation.Flags.Disabled); - } } /// diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs new file mode 100644 index 00000000000000..b493de4e757978 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs @@ -0,0 +1,2322 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers.Binary; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Tracing; +using System.Reflection; +using System.Linq; +using Microsoft.DotNet.XUnitExtensions; +using Xunit; + +namespace System.Threading.Tasks.Tests +{ + // Mirrors AsyncProfiler.AsyncEventID from the runtime (which is internal and inaccessible from tests). + public enum AsyncEventID : byte + { + CreateAsyncContext = 1, + ResumeAsyncContext = 2, + SuspendAsyncContext = 3, + CompleteAsyncContext = 4, + UnwindAsyncException = 5, + CreateAsyncCallstack = 6, + ResumeAsyncCallstack = 7, + SuspendAsyncCallstack = 8, + ResumeAsyncMethod = 9, + CompleteAsyncMethod = 10, + ResetAsyncThreadContext = 11, + ResetAsyncContinuationWrapperIndex = 12, + AsyncProfilerMetadata = 13, + AsyncProfilerSyncClock = 14, + } + + public class AsyncProfilerTests + { + // The test scenarios drive async work via Task.Run(...).GetAwaiter().GetResult() (see + // RunScenarioAndFlush / RunScenario), which requires synchronous blocking waits. On + // single-threaded WASM this throws PlatformNotSupportedException from + // RuntimeFeature.ThrowIfMultithreadingIsNotSupported(), so gate the tests on both + // runtime async support and threading support. + public static bool IsRuntimeAsyncAndThreadingSupported => + PlatformDetection.IsRuntimeAsyncSupported && PlatformDetection.IsMultithreadingSupported; + + private const string AsyncProfilerEventSourceName = "System.Runtime.CompilerServices.AsyncProfilerEventSource"; + private const int AsyncEventsId = 1; + private const int HeaderSize = 1 + sizeof(uint) + sizeof(uint) + sizeof(ulong) + sizeof(uint) + sizeof(ulong) + sizeof(ulong); + + // AsyncProfilerEventSource Keywords matching the event source definition + private const EventKeywords CreateAsyncContextKeyword = (EventKeywords)0x1; + private const EventKeywords ResumeAsyncContextKeyword = (EventKeywords)0x2; + private const EventKeywords SuspendAsyncContextKeyword = (EventKeywords)0x4; + private const EventKeywords CompleteAsyncContextKeyword = (EventKeywords)0x8; + private const EventKeywords UnwindAsyncExceptionKeyword = (EventKeywords)0x10; + private const EventKeywords CreateAsyncCallstackKeyword = (EventKeywords)0x20; + private const EventKeywords ResumeAsyncCallstackKeyword = (EventKeywords)0x40; + private const EventKeywords SuspendAsyncCallstackKeyword = (EventKeywords)0x80; + private const EventKeywords ResumeAsyncMethodKeyword = (EventKeywords)0x100; + private const EventKeywords CompleteAsyncMethodKeyword = (EventKeywords)0x200; + + private const EventKeywords AllKeywords = + CreateAsyncContextKeyword | ResumeAsyncContextKeyword | SuspendAsyncContextKeyword | + CompleteAsyncContextKeyword | UnwindAsyncExceptionKeyword | + CreateAsyncCallstackKeyword | ResumeAsyncCallstackKeyword | SuspendAsyncCallstackKeyword | + ResumeAsyncMethodKeyword | CompleteAsyncMethodKeyword; + + private const EventKeywords CoreKeywords = + CreateAsyncContextKeyword | ResumeAsyncContextKeyword | SuspendAsyncContextKeyword | CompleteAsyncContextKeyword; + + private const EventKeywords MethodKeywords = + ResumeAsyncMethodKeyword | CompleteAsyncMethodKeyword; + + private const EventKeywords CallstackKeywords = + CreateAsyncContextKeyword | CreateAsyncCallstackKeyword | + ResumeAsyncContextKeyword | ResumeAsyncCallstackKeyword | CompleteAsyncContextKeyword | + CompleteAsyncMethodKeyword | UnwindAsyncExceptionKeyword; + + private static readonly MethodInfo s_getMethodFromNativeIP = + typeof(StackFrame).GetMethod("GetMethodFromNativeIP", BindingFlags.Static | BindingFlags.NonPublic)!; + + private static MethodBase? GetMethodFromNativeIP(ulong nativeIP) + => (MethodBase?)s_getMethodFromNativeIP.Invoke(null, new object[] { (IntPtr)nativeIP }); + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task Func() + { + await Task.Yield(); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task FuncChained() + { + await FuncInner(); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task FuncInner() + { + await Task.Yield(); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task OuterCatches() + { + try + { + await InnerThrows(); + } + catch (InvalidOperationException) + { + } + await Task.Yield(); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task InnerThrows() + { + await Task.Yield(); + throw new InvalidOperationException("inner"); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task DeepOuterCatches() + { + try + { + await DeepMiddle(); + } + catch (InvalidOperationException) + { + } + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task DeepMiddle() + { + await DeepInnerThrows(); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task DeepInnerThrows() + { + await Task.Yield(); + throw new InvalidOperationException("deep inner"); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task DeepUnhandledOuter() + { + await DeepUnhandledMiddle(); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task DeepUnhandledMiddle() + { + await DeepUnhandledInnerThrows(); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task DeepUnhandledInnerThrows() + { + await Task.Yield(); + throw new InvalidOperationException("deep unhandled"); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task RecursiveFunc(int depth) + { + if (depth <= 1) + { + await Task.Yield(); + return; + } + await RecursiveFunc(depth - 1); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task WrapperTestA(List<(string MethodName, int WrapperSlot)> captures) + { + await WrapperTestB(captures); + captures.Add((nameof(WrapperTestA), GetCurrentWrapperSlot(nameof(WrapperTestA)))); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task WrapperTestB(List<(string MethodName, int WrapperSlot)> captures) + { + await WrapperTestC(captures); + captures.Add((nameof(WrapperTestB), GetCurrentWrapperSlot(nameof(WrapperTestB)))); + } + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task WrapperTestC(List<(string MethodName, int WrapperSlot)> captures) + { + await Task.Yield(); + captures.Add((nameof(WrapperTestC), GetCurrentWrapperSlot(nameof(WrapperTestC)))); + } + + private static TestEventListener CreateListener(EventKeywords keywords) + { + var listener = new TestEventListener(); + listener.AddSource(AsyncProfilerEventSourceName, EventLevel.Informational, keywords); + return listener; + } + + private static void SendFlushCommand() + { + const int FlushCommand = 1; + foreach (EventSource source in EventSource.GetSources()) + { + if (source.Name == AsyncProfilerEventSourceName) + { + EventSource.SendCommand(source, (EventCommand)FlushCommand, null); + return; + } + } + } + + private static ulong GetCurrentOSThreadId() + { + return (ulong)typeof(Thread) + .GetProperty("CurrentOSThreadId", BindingFlags.Static | BindingFlags.NonPublic)! + .GetValue(null)!; + } + + private static int GetCurrentWrapperSlot(string resumedMethodName) + { + var st = new StackTrace(); + for (int i = 0; i < st.FrameCount - 1; i++) + { + string? name = st.GetFrame(i)?.GetMethod()?.Name; + if (name is not null && name.Contains(resumedMethodName)) + { + // The next frame should be the Continuation_Wrapper_N that dispatched this method. + string? wrapperName = st.GetFrame(i + 1)?.GetMethod()?.Name; + if (wrapperName is not null && wrapperName.StartsWith("Continuation_Wrapper_", StringComparison.Ordinal)) + { + return int.Parse(wrapperName.Substring("Continuation_Wrapper_".Length)); + } + return -1; + } + } + return -1; + } + + private delegate bool EventVisitor(AsyncEventID eventId, ReadOnlySpan buffer, ref int index); + + private delegate bool EventVisitorWithTimestamp(AsyncEventID eventId, long timestamp, ReadOnlySpan buffer, ref int index); + + private static void ParseEventBuffer(ReadOnlySpan buffer, EventVisitor visitor) + { + ParseEventBuffer(buffer, (AsyncEventID eventId, long _, ReadOnlySpan buf, ref int idx) => + visitor(eventId, buf, ref idx)); + } + + private static void ParseEventBuffer(ReadOnlySpan buffer, EventVisitorWithTimestamp visitor) + { + EventBufferHeader? header = ParseEventBufferHeader(buffer); + if (header is null) + return; + + int index = HeaderSize; + long baseTimestamp = (long)header.Value.StartTimestamp; + + while (index < buffer.Length) + { + if (index + 2 > buffer.Length) + break; + + AsyncEventID eventId = (AsyncEventID)buffer[index++]; + + long delta = (long)ReadCompressedUInt64(buffer, ref index); + baseTimestamp += delta; + + if (!visitor(eventId, baseTimestamp, buffer, ref index)) + break; + } + } + + private static bool SkipEventPayload(AsyncEventID eventId, ReadOnlySpan buffer, ref int index) + { + switch (eventId) + { + case AsyncEventID.CreateAsyncContext: + case AsyncEventID.ResumeAsyncContext: + ReadCompressedUInt64(buffer, ref index); + return true; + case AsyncEventID.SuspendAsyncContext: + case AsyncEventID.CompleteAsyncContext: + case AsyncEventID.ResumeAsyncMethod: + case AsyncEventID.CompleteAsyncMethod: + case AsyncEventID.ResetAsyncThreadContext: + case AsyncEventID.ResetAsyncContinuationWrapperIndex: + return true; + case AsyncEventID.AsyncProfilerMetadata: + SkipMetadataPayload(buffer, ref index); + return true; + case AsyncEventID.AsyncProfilerSyncClock: + ReadCompressedUInt64(buffer, ref index); // qpcSync + ReadCompressedUInt64(buffer, ref index); // utcSync + return true; + case AsyncEventID.UnwindAsyncException: + ReadCompressedUInt32(buffer, ref index); + return true; + case AsyncEventID.CreateAsyncCallstack: + case AsyncEventID.ResumeAsyncCallstack: + case AsyncEventID.SuspendAsyncCallstack: + SkipCallstackPayload(buffer, ref index); + return true; + default: + return false; + } + } + + private static uint ReadCompressedUInt32(ReadOnlySpan buffer, ref int index) + { + EventBuffer.Deserializer.ReadCompressedUInt32(buffer, ref index, out uint value); + return value; + } + + private static ulong ReadCompressedUInt64(ReadOnlySpan buffer, ref int index) + { + EventBuffer.Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong value); + return value; + } + + private static void SkipCallstackPayload(ReadOnlySpan buffer, ref int index) + { + ReadCallstackPayload(buffer, ref index, out _, out _); + } + + private static void ReadCallstackPayload(ReadOnlySpan buffer, ref int index, + out byte frameCount, out List<(ulong NativeIP, int State)> frames) + { + ReadCallstackPayload(buffer, ref index, out _, out frameCount, out frames); + } + + private static void ReadCallstackPayload(ReadOnlySpan buffer, ref int index, + out ulong taskId, out byte frameCount, out List<(ulong NativeIP, int State)> frames) + { + index++; // type + index++; // callstack ID (reserved) + frameCount = buffer[index++]; + taskId = ReadCompressedUInt64(buffer, ref index); + frames = new List<(ulong, int)>(frameCount); + + if (frameCount == 0) + return; + + ulong currentNativeIP = ReadCompressedUInt64(buffer, ref index); + int state = ReadCompressedInt32(buffer, ref index); + frames.Add((currentNativeIP, state)); + + for (int i = 1; i < frameCount; i++) + { + long delta = ReadCompressedInt64(buffer, ref index); + state = ReadCompressedInt32(buffer, ref index); + currentNativeIP = (ulong)((long)currentNativeIP + delta); + frames.Add((currentNativeIP, state)); + } + } + + private static int ReadCompressedInt32(ReadOnlySpan buffer, ref int index) + { + EventBuffer.Deserializer.ReadCompressedInt32(buffer, ref index, out int value); + return value; + } + + private static long ReadCompressedInt64(ReadOnlySpan buffer, ref int index) + { + EventBuffer.Deserializer.ReadCompressedInt64(buffer, ref index, out long value); + return value; + } + + private static void SkipMetadataPayload(ReadOnlySpan buffer, ref int index) + { + ReadMetadataPayload(buffer, ref index, out _, out _, out _, out _, out _); + } + + private static void ReadMetadataPayload(ReadOnlySpan buffer, ref int index, + out ulong qpcFrequency, out ulong qpcSync, out ulong utcSync, out uint eventBufferSize, out long[] wrapperIPs) + { + qpcFrequency = ReadCompressedUInt64(buffer, ref index); + qpcSync = ReadCompressedUInt64(buffer, ref index); + utcSync = ReadCompressedUInt64(buffer, ref index); + eventBufferSize = ReadCompressedUInt32(buffer, ref index); + byte wrapperCount = buffer[index++]; + wrapperIPs = new long[wrapperCount]; + for (int i = 0; i < wrapperCount; i++) + { + wrapperIPs[i] = (long)ReadCompressedUInt64(buffer, ref index); + } + } + + private record struct MetadataFromBuffer(ulong QpcFrequency, ulong QpcSync, ulong UtcSync, uint EventBufferSize, long[] WrapperIPs); + + private static List CollectMetadataFromBuffer(ConcurrentQueue events) + { + var metadataList = new List(); + ForEachEventBufferPayload(events, buffer => + { + ParseEventBuffer(buffer, (AsyncEventID eventId, ReadOnlySpan buf, ref int idx) => + { + if (eventId == AsyncEventID.AsyncProfilerMetadata) + { + ReadMetadataPayload(buf, ref idx, out ulong freq, out ulong qpcSync, out ulong utcSync, out uint bufSize, out long[] ips); + metadataList.Add(new MetadataFromBuffer(freq, qpcSync, utcSync, bufSize, ips)); + return true; + } + return SkipEventPayload(eventId, buf, ref idx); + }); + }); + return metadataList; + } + + private static ulong ParseOsThreadId(ReadOnlySpan buffer) + { + return ParseEventBufferHeader(buffer)?.OsThreadId ?? 0; + } + + private readonly record struct EventBufferHeader(byte Version, uint TotalSize, uint AsyncThreadContextId, ulong OsThreadId, uint EventCount, ulong StartTimestamp, ulong EndTimestamp); + + private static EventBufferHeader? ParseEventBufferHeader(ReadOnlySpan buffer) + { + if (buffer.Length < HeaderSize || buffer[0] != 1) + return null; + + int index = 1; + EventBuffer.Deserializer.ReadUInt32(buffer, ref index, out uint totalSize); + EventBuffer.Deserializer.ReadUInt32(buffer, ref index, out uint contextId); + EventBuffer.Deserializer.ReadUInt64(buffer, ref index, out ulong threadId); + EventBuffer.Deserializer.ReadUInt32(buffer, ref index, out uint eventCount); + EventBuffer.Deserializer.ReadUInt64(buffer, ref index, out ulong startTs); + EventBuffer.Deserializer.ReadUInt64(buffer, ref index, out ulong endTs); + + return new EventBufferHeader(buffer[0], totalSize, contextId, threadId, eventCount, startTs, endTs); + } + + private static List CollectAsyncEventIds(ConcurrentQueue events) + { + var allEventIds = new List(); + ForEachEventBufferPayload(events, buffer => + { + ParseEventBuffer(buffer, (AsyncEventID eventId, ReadOnlySpan buf, ref int idx) => + { + allEventIds.Add(eventId); + return SkipEventPayload(eventId, buf, ref idx); + }); + }); + return allEventIds; + } + + private static List<(AsyncEventID EventId, long Timestamp)> CollectAsyncEventIdsWithTimestamps(ConcurrentQueue events) + { + var allEvents = new List<(AsyncEventID EventId, long Timestamp)>(); + ForEachEventBufferPayload(events, buffer => + { + ParseEventBuffer(buffer, (AsyncEventID eventId, long timestamp, ReadOnlySpan buf, ref int idx) => + { + allEvents.Add((eventId, timestamp)); + return SkipEventPayload(eventId, buf, ref idx); + }); + }); + allEvents.Sort((a, b) => a.Timestamp.CompareTo(b.Timestamp)); + return allEvents; + } + + private static HashSet CollectOsThreadIds(ConcurrentQueue events) + { + var threadIds = new HashSet(); + ForEachEventBufferPayload(events, buffer => + { + ulong tid = ParseOsThreadId(buffer); + if (tid != 0) + threadIds.Add(tid); + }); + return threadIds; + } + + private static List CollectUnwindFrameCounts(ConcurrentQueue events) + { + var frameCounts = new List(); + ForEachEventBufferPayload(events, buffer => + { + ParseEventBuffer(buffer, (AsyncEventID eventId, ReadOnlySpan buf, ref int idx) => + { + if (eventId == AsyncEventID.UnwindAsyncException) + { + frameCounts.Add(ReadCompressedUInt32(buf, ref idx)); + return true; + } + return SkipEventPayload(eventId, buf, ref idx); + }); + }); + return frameCounts; + } + + private static List<(ulong TaskId, byte FrameCount, List<(ulong NativeIP, int State)> Frames)> CollectCallstacks( + ConcurrentQueue events) + { + return CollectCallstacks(events, AsyncEventID.ResumeAsyncCallstack, threadId: null); + } + + private static List<(ulong TaskId, byte FrameCount, List<(ulong NativeIP, int State)> Frames)> CollectCallstacks( + ConcurrentQueue events, ulong? threadId) + { + return CollectCallstacks(events, AsyncEventID.ResumeAsyncCallstack, threadId); + } + + private static List<(ulong TaskId, byte FrameCount, List<(ulong NativeIP, int State)> Frames)> CollectCallstacks( + ConcurrentQueue events, AsyncEventID callstackEventId) + { + return CollectCallstacks(events, callstackEventId, threadId: null); + } + + private static List<(ulong TaskId, byte FrameCount, List<(ulong NativeIP, int State)> Frames)> CollectCallstacks( + ConcurrentQueue events, AsyncEventID callstackEventId, ulong? threadId) + { + var callstacks = new List<(ulong, byte, List<(ulong, int)>)>(); + ForEachEventBufferPayload(events, buffer => + { + if (threadId.HasValue) + { + ulong tid = ParseOsThreadId(buffer); + if (tid != threadId.Value) + return; + } + + ParseEventBuffer(buffer, (AsyncEventID eventId, ReadOnlySpan buf, ref int idx) => + { + if (eventId == callstackEventId) + { + ReadCallstackPayload(buf, ref idx, out ulong taskId, out byte frameCount, out var frames); + callstacks.Add((taskId, frameCount, frames)); + return true; + } + return SkipEventPayload(eventId, buf, ref idx); + }); + }); + return callstacks; + } + + private static (byte FrameCount, List<(ulong NativeIP, int State)> Frames)? FindCallstackAfterTimestamp( + ConcurrentQueue events, ulong threadId, long afterTimestamp) + { + (byte FrameCount, List<(ulong, int)> Frames)? best = null; + long bestTimestamp = long.MaxValue; + + ForEachEventBufferPayload(events, buffer => + { + ulong tid = ParseOsThreadId(buffer); + if (tid != threadId) + return; + + ParseEventBuffer(buffer, (AsyncEventID eventId, long timestamp, ReadOnlySpan buf, ref int idx) => + { + if (eventId == AsyncEventID.ResumeAsyncCallstack) + { + ReadCallstackPayload(buf, ref idx, out byte frameCount, out var frames); + if (timestamp >= afterTimestamp && timestamp < bestTimestamp) + { + bestTimestamp = timestamp; + best = (frameCount, frames); + } + return true; + } + return SkipEventPayload(eventId, buf, ref idx); + }); + }); + + return best; + } + + private delegate void EventBufferPayloadAction(ReadOnlySpan payload); + + private static void ForEachEventBufferPayload(ConcurrentQueue events, EventBufferPayloadAction action) + { + foreach (var e in events) + { + if (e.EventId == AsyncEventsId && e.Payload is { Count: >= 1 } && e.Payload[0] is byte[] rawPayload) + { + action(rawPayload); + } + } + } + + // Uncomment at callsite to dump all collected event buffers to console for diagnostics: + private static void DumpCollectedEvents(ConcurrentQueue events) + { + ForEachEventBufferPayload(events, buffer => EventBuffer.OutputEventBuffer(buffer)); + } + + private static void RunScenarioAndFlush(Func scenario) + { + Task.Run(scenario).GetAwaiter().GetResult(); + SendFlushCommand(); + } + + private static void RunScenario(Func scenario) + { + Task.Run(scenario).GetAwaiter().GetResult(); + } + + private static ConcurrentQueue CollectEvents(EventKeywords keywords, Action callback) + { + return CollectEvents(keywords, (_, _) => callback()); + } + + private static ConcurrentQueue CollectEvents(EventKeywords keywords, Action, EventKeywords> callback) + { + var events = new ConcurrentQueue(); + using (var listener = CreateListener(keywords)) + { + listener.RunWithCallback(events.Enqueue, () => + { + SendFlushCommand(); + events.Clear(); + callback(events, keywords); + }); + } + return events; + } + + private static void AssertCallstackSimulationReachesZero(ConcurrentQueue events) + { + var eventIds = CollectAsyncEventIds(events); + var frameCounts = CollectUnwindFrameCounts(events); + var callstacks = CollectCallstacks(events); + + int stackDepth = 0; + int unwindIdx = 0; + int callstackIdx = 0; + + foreach (AsyncEventID id in eventIds) + { + switch (id) + { + case AsyncEventID.ResumeAsyncCallstack: + if (callstackIdx < callstacks.Count) + stackDepth = callstacks[callstackIdx++].FrameCount; + break; + case AsyncEventID.CompleteAsyncMethod: + if (stackDepth > 0) + stackDepth--; + break; + case AsyncEventID.UnwindAsyncException: + if (unwindIdx < frameCounts.Count) + stackDepth = Math.Max(0, stackDepth - (int)frameCounts[unwindIdx++]); + break; + } + } + + Assert.True(callstackIdx > 0, "Expected at least one ResumeAsyncCallstack event"); + Assert.Equal(0, stackDepth); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_EventBufferHeaderFormat() + { + var events = CollectEvents(CoreKeywords, () => + { + RunScenarioAndFlush(async () => + { + await Func(); + }); + }); + + // DumpCollectedEvents(events); + + int buffersChecked = 0; + ForEachEventBufferPayload(events, buffer => + { + EventBufferHeader? parsed = ParseEventBufferHeader(buffer); + Assert.NotNull(parsed); + EventBufferHeader header = parsed.Value; + + Assert.Equal(1, header.Version); + Assert.Equal((uint)buffer.Length, header.TotalSize); + Assert.True(header.AsyncThreadContextId > 0, "Async thread context ID should be positive"); + Assert.True(header.OsThreadId != 0, "OS thread ID should be non-zero"); + Assert.True(header.StartTimestamp > 0, "Start timestamp should be positive"); + Assert.True(header.EndTimestamp >= header.StartTimestamp, + $"End timestamp ({header.EndTimestamp}) should be >= start timestamp ({header.StartTimestamp})"); + + int eventCount = 0; + ParseEventBuffer(buffer, (AsyncEventID eventId, ReadOnlySpan buf, ref int idx) => + { + eventCount++; + return SkipEventPayload(eventId, buf, ref idx); + }); + + Assert.Equal(header.EventCount, (uint)eventCount); + Assert.True(header.EventCount > 0, "Expected at least one event in buffer"); + + buffersChecked++; + }); + + Assert.True(buffersChecked > 0, "Expected at least one buffer"); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_EventsEmitted() + { + var events = CollectEvents(AllKeywords, () => + { + RunScenarioAndFlush(async () => + { + await Func(); + }); + }); + + // DumpCollectedEvents(events); + + Assert.True(events.Count > 0, "Expected at least one AsyncEvents event to be emitted"); + Assert.Contains(events, e => e.EventId == AsyncEventsId); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_SuspendResumeCompleteEvents() + { + var events = CollectEvents(CoreKeywords, () => + { + RunScenarioAndFlush(async () => + { + // If not Yield here there won't be a SuspendAsyncContext. + // First call is a regular sync invocation (no continuation chain). + // Yield in Func will create an RuntimeAsyncTask with continuation chain + // and schedule on thread pool. When chain is resumed there will be + // ResumeAsyncContext and CompleteAsyncContext since the chain won't suspend again. + // The first Yield fixes that creating and schedule the RuntimeAsyncTask and Func + // will be called from the dispatch loop triggering the expected sequence of events. + await Task.Yield(); + await Func(); + }); + }); + + // DumpCollectedEvents(events); + + var eventIds = CollectAsyncEventIds(events); + + Assert.Contains(AsyncEventID.ResumeAsyncContext, eventIds); + Assert.Contains(AsyncEventID.SuspendAsyncContext, eventIds); + Assert.Contains(AsyncEventID.CompleteAsyncContext, eventIds); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_ContextEventIdLifecycle() + { + var events = CollectEvents(CoreKeywords, () => + { + RunScenarioAndFlush(async () => + { + await Task.Yield(); + await Func(); + }); + }); + + // DumpCollectedEvents(events); + + var createIds = new List(); + var resumeIds = new List(); + + ForEachEventBufferPayload(events, buffer => + { + ParseEventBuffer(buffer, (AsyncEventID eventId, ReadOnlySpan buf, ref int idx) => + { + if (eventId == AsyncEventID.CreateAsyncContext) + { + createIds.Add(ReadCompressedUInt64(buf, ref idx)); + return true; + } + if (eventId == AsyncEventID.ResumeAsyncContext) + { + resumeIds.Add(ReadCompressedUInt64(buf, ref idx)); + return true; + } + return SkipEventPayload(eventId, buf, ref idx); + }); + }); + + Assert.True(createIds.Count > 0, "Expected at least one CreateAsyncContext with id"); + Assert.True(resumeIds.Count > 0, "Expected at least one ResumeAsyncContext with id"); + + Assert.All(createIds, id => Assert.True(id > 0, "CreateAsyncContext id should be non-zero")); + Assert.All(resumeIds, id => Assert.True(id > 0, "ResumeAsyncContext id should be non-zero")); + + foreach (ulong resumeId in resumeIds) + { + Assert.Contains(resumeId, createIds); + } + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_ResumeCompleteMethodEvents() + { + var events = CollectEvents(MethodKeywords, () => + { + RunScenarioAndFlush(async () => + { + await FuncChained(); + }); + }); + + // DumpCollectedEvents(events); + + var eventIds = CollectAsyncEventIds(events); + + Assert.Contains(AsyncEventID.ResumeAsyncMethod, eventIds); + Assert.Contains(AsyncEventID.CompleteAsyncMethod, eventIds); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_EventSequenceOrder() + { + var events = CollectEvents(CoreKeywords, () => + { + // Same scenario as SuspendResumeCompleteEvents; here we verify ordering. + RunScenarioAndFlush(async () => + { + await Task.Yield(); + await Func(); + }); + }); + + // DumpCollectedEvents(events); + + var sortedEvents = CollectAsyncEventIdsWithTimestamps(events); + var coreEvents = sortedEvents.FindAll(e => e.EventId == AsyncEventID.ResumeAsyncContext || e.EventId == AsyncEventID.SuspendAsyncContext || e.EventId == AsyncEventID.CompleteAsyncContext); + + Assert.Equal(AsyncEventID.ResumeAsyncContext, coreEvents[0].EventId); + Assert.Equal(AsyncEventID.SuspendAsyncContext, coreEvents[1].EventId); + Assert.Equal(AsyncEventID.ResumeAsyncContext, coreEvents[2].EventId); + Assert.Equal(AsyncEventID.CompleteAsyncContext, coreEvents[3].EventId); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_CreateAsyncContextEmittedOnFirstAwait() + { + var events = CollectEvents(CreateAsyncContextKeyword | CompleteAsyncContextKeyword, () => + { + RunScenarioAndFlush(async () => + { + await Func(); + }); + }); + + // DumpCollectedEvents(events); + + var eventIds = CollectAsyncEventIds(events); + Assert.Contains(AsyncEventID.CreateAsyncContext, eventIds); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_CreateAsyncCallstackEmittedOnFirstAwait() + { + var events = CollectEvents(CreateAsyncCallstackKeyword | CompleteAsyncContextKeyword, () => + { + RunScenarioAndFlush(async () => + { + await Func(); + }); + }); + + // DumpCollectedEvents(events); + + var callstacks = CollectCallstacks(events, AsyncEventID.CreateAsyncCallstack); + + Assert.NotEmpty(callstacks); + Assert.All(callstacks, cs => + { + Assert.True(cs.FrameCount > 0, "Expected at least one frame in create callstack"); + Assert.True(cs.TaskId != 0, "Expected non-zero task ID in create callstack"); + Assert.True(cs.Frames[0].NativeIP != 0, "Expected non-zero NativeIP in first frame"); + }); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_CreateCallstackDepthMatchesChain() + { + var events = CollectEvents(CreateAsyncCallstackKeyword | CompleteAsyncContextKeyword, () => + { + // FuncChained -> FuncInner -> lambda: create callstack at FuncInner's + // first await should reflect the 3-level chain. + RunScenarioAndFlush(async () => + { + await FuncChained(); + }); + }); + + // DumpCollectedEvents(events); + + var callstacks = CollectCallstacks(events, AsyncEventID.CreateAsyncCallstack); + + Assert.NotEmpty(callstacks); + Assert.Contains(callstacks, cs => cs.FrameCount == 3); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_SuspendAsyncCallstackEmittedOnAwait() + { + var events = CollectEvents(SuspendAsyncCallstackKeyword | CompleteAsyncContextKeyword, () => + { + RunScenarioAndFlush(async () => + { + // First Yield pushes execution into the dispatch loop. + // Then Func()'s Yield triggers a suspend inside the loop + // where the SuspendAsyncCallstack event is emitted. + await Task.Yield(); + await Func(); + }); + }); + + // DumpCollectedEvents(events); + + var callstacks = CollectCallstacks(events, AsyncEventID.SuspendAsyncCallstack); + + Assert.NotEmpty(callstacks); + Assert.All(callstacks, cs => + { + Assert.True(cs.FrameCount > 0, "Expected at least one frame in suspend callstack"); + Assert.True(cs.TaskId != 0, "Expected non-zero task ID in suspend callstack"); + Assert.True(cs.Frames[0].NativeIP != 0, "Expected non-zero NativeIP in first frame"); + }); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_SuspendCallstackDepthMatchesChain() + { + var events = CollectEvents(SuspendAsyncCallstackKeyword | CompleteAsyncContextKeyword, () => + { + // FuncChained -> FuncInner -> lambda: 3 levels deep when FuncInner suspends. + RunScenarioAndFlush(async () => + { + await Task.Yield(); + await FuncChained(); + }); + }); + + // DumpCollectedEvents(events); + + var callstacks = CollectCallstacks(events, AsyncEventID.SuspendAsyncCallstack); + + Assert.NotEmpty(callstacks); + Assert.Contains(callstacks, cs => cs.FrameCount == 3); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_SuspendCallstackPrecedesComplete() + { + // Use a single-level async method so all events belong to the same context. + // This avoids ordering ambiguity from nested async calls. + var events = CollectEvents(SuspendAsyncCallstackKeyword | CompleteAsyncContextKeyword, () => + { + RunScenarioAndFlush(async () => + { + // First Yield pushes into dispatch loop; second Yield triggers suspend. + await Task.Yield(); + await Task.Yield(); + }); + }); + + // DumpCollectedEvents(events); + + var eventIds = CollectAsyncEventIdsWithTimestamps(events); + + int suspendIdx = eventIds.FindIndex(e => e.EventId == AsyncEventID.SuspendAsyncCallstack); + int completeIdx = eventIds.FindIndex(e => e.EventId == AsyncEventID.CompleteAsyncContext); + + Assert.True(suspendIdx >= 0, "Expected SuspendAsyncCallstack event"); + Assert.True(completeIdx >= 0, "Expected CompleteAsyncContext event"); + Assert.True(suspendIdx < completeIdx, + $"SuspendAsyncCallstack (index {suspendIdx}) should precede CompleteAsyncContext (index {completeIdx})"); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_SuspendCallstackDeeperThanInitialResume() + { + // After the initial Yield, the first resume is at the lambda level (depth 1). + // Then FuncChained -> FuncInner builds the full chain and suspends at depth 3. + // The suspend callstack should be deeper than the initial resume. + var events = CollectEvents( + ResumeAsyncCallstackKeyword | SuspendAsyncCallstackKeyword | CompleteAsyncContextKeyword, () => + { + RunScenarioAndFlush(async () => + { + await Task.Yield(); + await FuncChained(); + }); + }); + + // DumpCollectedEvents(events); + + var resumeStacks = CollectCallstacks(events, AsyncEventID.ResumeAsyncCallstack); + var suspendStacks = CollectCallstacks(events, AsyncEventID.SuspendAsyncCallstack); + + Assert.NotEmpty(resumeStacks); + Assert.NotEmpty(suspendStacks); + + // The shallowest resume is after the initial Yield (just the lambda). + // The deepest suspend captures the full chain (FuncInner -> FuncChained -> lambda). + // Use min/max to avoid cross-buffer ordering dependence. + byte minResumeDepth = resumeStacks.Min(cs => cs.FrameCount); + byte maxSuspendDepth = suspendStacks.Max(cs => cs.FrameCount); + + Assert.True(maxSuspendDepth > minResumeDepth, + $"Suspend callstack depth ({maxSuspendDepth}) should be deeper than shallowest resume callstack depth ({minResumeDepth})"); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_CreateCallstackPrecedesResumeCallstack() + { + var events = CollectEvents(CreateAsyncContextKeyword | CreateAsyncCallstackKeyword | ResumeAsyncContextKeyword | ResumeAsyncCallstackKeyword | CompleteAsyncContextKeyword, () => + { + RunScenarioAndFlush(async () => + { + await Func(); + }); + }); + + // DumpCollectedEvents(events); + + // Collect all callstack events with their task IDs sorted by timestamp. + var callstackEvents = new List<(AsyncEventID EventId, ulong TaskId, long Timestamp)>(); + ForEachEventBufferPayload(events, buffer => + { + ParseEventBuffer(buffer, (AsyncEventID eventId, long timestamp, ReadOnlySpan buf, ref int idx) => + { + if (eventId is AsyncEventID.CreateAsyncCallstack or AsyncEventID.ResumeAsyncCallstack) + { + ReadCallstackPayload(buf, ref idx, out ulong taskId, out byte _, out _); + callstackEvents.Add((eventId, taskId, timestamp)); + return true; + } + return SkipEventPayload(eventId, buf, ref idx); + }); + }); + callstackEvents.Sort((a, b) => a.Timestamp.CompareTo(b.Timestamp)); + + // For each task that has both Create and Resume, verify Create comes first. + var taskIds = callstackEvents.Where(e => e.EventId == AsyncEventID.CreateAsyncCallstack).Select(e => e.TaskId).ToHashSet(); + Assert.NotEmpty(taskIds); + + foreach (ulong taskId in taskIds) + { + int createIdx = callstackEvents.FindIndex(e => e.EventId == AsyncEventID.CreateAsyncCallstack && e.TaskId == taskId); + int resumeIdx = callstackEvents.FindIndex(e => e.EventId == AsyncEventID.ResumeAsyncCallstack && e.TaskId == taskId); + + Assert.True(createIdx >= 0, $"Expected CreateAsyncCallstack for task {taskId}"); + Assert.True(resumeIdx >= 0, $"Expected ResumeAsyncCallstack for task {taskId}"); + Assert.True(createIdx < resumeIdx, + $"For task {taskId}: CreateAsyncCallstack (index {createIdx}) should precede ResumeAsyncCallstack (index {resumeIdx})"); + } + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_CreateAndFirstResumeCallstacksMatch() + { + var events = CollectEvents(CreateAsyncCallstackKeyword | ResumeAsyncCallstackKeyword | CompleteAsyncContextKeyword, () => + { + RunScenarioAndFlush(async () => + { + await Func(); + }); + }); + + // DumpCollectedEvents(events); + + var createStacks = CollectCallstacks(events, AsyncEventID.CreateAsyncCallstack); + var resumeStacks = CollectCallstacks(events, AsyncEventID.ResumeAsyncCallstack); + + Assert.NotEmpty(createStacks); + Assert.NotEmpty(resumeStacks); + + foreach (var (taskId, _, createFrames) in createStacks) + { + var matchingResume = resumeStacks.FirstOrDefault(r => r.TaskId == taskId); + Assert.True(matchingResume.Frames is not null, + $"Expected a ResumeAsyncCallstack for task {taskId}"); + + Assert.Equal(createFrames.Count, matchingResume.Frames!.Count); + for (int i = 0; i < createFrames.Count; i++) + { + Assert.Equal(createFrames[i].NativeIP, matchingResume.Frames[i].NativeIP); + } + } + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_CallstackEmittedOnResume() + { + var events = CollectEvents(CallstackKeywords, () => + { + RunScenarioAndFlush(async () => + { + await Func(); + }); + }); + + // DumpCollectedEvents(events); + + var callstacks = CollectCallstacks(events); + + Assert.NotEmpty(callstacks); + Assert.All(callstacks, cs => + { + Assert.True(cs.FrameCount > 0, "Expected at least one frame in callstack"); + Assert.True(cs.TaskId != 0, "Expected non-zero task ID in resume callstack"); + Assert.True(cs.Frames[0].NativeIP != 0, "Expected non-zero NativeIP in first frame"); + }); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_CallstackDepthMatchesChain() + { + var events = CollectEvents(CallstackKeywords, () => + { + // FuncChained -> FuncInner -> lambda: 3 levels deep after FuncInner yields. + RunScenarioAndFlush(async () => + { + await FuncChained(); + }); + }); + + // DumpCollectedEvents(events); + + var callstacks = CollectCallstacks(events); + + Assert.NotEmpty(callstacks); + Assert.Contains(callstacks, cs => cs.FrameCount == 3); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_CallstackSimulation_NormalCompletion() + { + var events = CollectEvents(CallstackKeywords, () => + { + RunScenarioAndFlush(async () => + { + await FuncChained(); + }); + }); + + // DumpCollectedEvents(events); + + AssertCallstackSimulationReachesZero(events); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_CallstackSimulation_HandledException() + { + var events = CollectEvents(CallstackKeywords, () => + { + // DeepOuterCatches -> DeepMiddle -> DeepInnerThrows: exception is caught + // within the chain. Unwind pops 2 frames, execution resumes in outer. + RunScenarioAndFlush(async () => + { + await DeepOuterCatches(); + }); + }); + + // DumpCollectedEvents(events); + + AssertCallstackSimulationReachesZero(events); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_CallstackSimulation_UnhandledException() + { + var events = CollectEvents(CallstackKeywords, () => + { + // DeepUnhandledOuter -> DeepUnhandledMiddle -> DeepUnhandledInnerThrows: + // no catch in the chain. Unwind pops all 3 frames, task faults. + Task task = Task.Run(DeepUnhandledOuter); + try + { + task.GetAwaiter().GetResult(); + } + catch (InvalidOperationException) + { + } + SendFlushCommand(); + }); + + // DumpCollectedEvents(events); + + AssertCallstackSimulationReachesZero(events); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_UnhandledExceptionUnwind() + { + var events = CollectEvents(UnwindAsyncExceptionKeyword | CoreKeywords, () => + { + // lambda -> DeepUnhandledOuter -> DeepUnhandledMiddle -> DeepUnhandledInnerThrows (4 levels). + // No try/catch in the chain — UnwindToPossibleHandler returns null, + // triggering the unhandled exception path which faults the task. + // unwindedFrames starts at 1 (current) + walks 2 more continuations = 3. + try + { + RunScenario(async () => + { + await DeepUnhandledOuter(); + }); + } + catch (InvalidOperationException) + { + } + + SendFlushCommand(); + }); + + // DumpCollectedEvents(events); + + var eventIds = CollectAsyncEventIds(events); + var frameCounts = CollectUnwindFrameCounts(events); + + Assert.Contains(AsyncEventID.ResumeAsyncContext, eventIds); + Assert.Contains(AsyncEventID.UnwindAsyncException, eventIds); + Assert.Contains(AsyncEventID.CompleteAsyncContext, eventIds); + + Assert.NotEmpty(frameCounts); + Assert.All(frameCounts, count => Assert.Equal(4u, count)); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_HandledExceptionUnwind() + { + var events = CollectEvents(UnwindAsyncExceptionKeyword | CoreKeywords, () => + { + // DeepOuterCatches -> DeepMiddle -> DeepInnerThrows (3 levels). + // DeepOuterCatches has try/catch — UnwindToPossibleHandler finds the handler. + // unwindedFrames starts at 1 (current) + walks 1 to find handler = 2. + RunScenarioAndFlush(async () => + { + await DeepOuterCatches(); + }); + }); + + // DumpCollectedEvents(events); + + var eventIds = CollectAsyncEventIds(events); + var frameCounts = CollectUnwindFrameCounts(events); + + Assert.Contains(AsyncEventID.ResumeAsyncContext, eventIds); + Assert.Contains(AsyncEventID.UnwindAsyncException, eventIds); + Assert.Contains(AsyncEventID.CompleteAsyncContext, eventIds); + + Assert.NotEmpty(frameCounts); + Assert.All(frameCounts, count => Assert.Equal(2u, count)); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_WrapperIndexMatchesCallstack() + { + var captures = new List<(string MethodName, int WrapperSlot)>(); + ulong scenarioThreadId = 0; + long scenarioTimestamp = 0; + + var events = CollectEvents(CallstackKeywords, () => + { + // Capture a timestamp just before the scenario runs. + // The callstack event closest after this timestamp on the + // scenario thread is the one we want — simulating how a CPU + // sampler would correlate a sample with a callstack. + scenarioTimestamp = Stopwatch.GetTimestamp(); + + // WrapperTestA -> WrapperTestB -> WrapperTestC. + // Each method captures which Continuation_Wrapper_N dispatched it. + RunScenarioAndFlush(async () => + { + await WrapperTestA(captures); + scenarioThreadId = GetCurrentOSThreadId(); + }); + }); + + // DumpCollectedEvents(events); + + Assert.True(scenarioThreadId != 0, "Failed to capture scenario thread ID"); + Assert.True(captures.Count == 3, $"Expected 3 wrapper captures, got {captures.Count}"); + Assert.All(captures, c => Assert.True(c.WrapperSlot >= 0, $"{c.MethodName} did not find Continuation_Wrapper_N on stack (slot={c.WrapperSlot})")); + + int slotC = captures.First(c => c.MethodName == nameof(WrapperTestC)).WrapperSlot; + int slotB = captures.First(c => c.MethodName == nameof(WrapperTestB)).WrapperSlot; + int slotA = captures.First(c => c.MethodName == nameof(WrapperTestA)).WrapperSlot; + + Assert.Equal(slotC + 1, slotB); + Assert.Equal(slotB + 1, slotA); + + var chainStack = FindCallstackAfterTimestamp(events, scenarioThreadId, scenarioTimestamp); + + Assert.True(chainStack.HasValue, "No callstack found after scenario timestamp on scenario thread"); + Assert.True(chainStack.Value.FrameCount == 4, $"Expected callstack with 4 frames, got {chainStack.Value.FrameCount}"); + + var resolvedNames = new List(); + foreach (var (nativeIP, _) in chainStack.Value.Frames) + { + var method = GetMethodFromNativeIP(nativeIP); + resolvedNames.Add(method?.Name ?? ""); + } + + Assert.Equal(nameof(WrapperTestC), resolvedNames[slotC]); + Assert.Equal(nameof(WrapperTestB), resolvedNames[slotB]); + Assert.Equal(nameof(WrapperTestA), resolvedNames[slotA]); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_WrapperIndexResetEmitted() + { + var events = CollectEvents(AllKeywords, () => + { + // Recursive chain 34 levels deep crosses the 32-slot boundary, + // triggering at least one ResetAsyncContinuationWrapperIndex event. + RunScenarioAndFlush(async () => + { + await RecursiveFunc(34); + }); + }); + + // DumpCollectedEvents(events); + + var eventIds = CollectAsyncEventIds(events); + + Assert.Contains(AsyncEventID.ResetAsyncContinuationWrapperIndex, eventIds); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_WrapperIndexNoResetUnder32() + { + var events = CollectEvents(AllKeywords, () => + { + // A shallow chain stays within the first 32 slots — + // no reset event should be emitted. + RunScenarioAndFlush(async () => + { + await RecursiveFunc(2); + }); + }); + + // DumpCollectedEvents(events); + + var eventIds = CollectAsyncEventIds(events); + + Assert.DoesNotContain(AsyncEventID.ResetAsyncContinuationWrapperIndex, eventIds); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_PeriodicTimerFlush() + { + var events = CollectEvents(CoreKeywords, (collectedEvents, _) => + { + // Run scenario — do NOT flush explicitly afterwards. + RunScenario(async () => + { + await Func(); + }); + + // Wait for the periodic flush timer (1s interval) to detect the idle buffer and flush it automatically. + Thread.Sleep(1000); + + // Poll to make sure the expected buffer got flush. + bool flushed = SpinWait.SpinUntil(() => + { + var ids = CollectAsyncEventIds(collectedEvents); + return ids.Exists(id => id == AsyncEventID.ResumeAsyncContext || id == AsyncEventID.SuspendAsyncContext || id == AsyncEventID.CompleteAsyncContext); + }, TimeSpan.FromSeconds(20)); + + Assert.True(flushed, "Expected periodic timer to flush buffer with core lifecycle events within timeout"); + }); + + // DumpCollectedEvents(events); + + var eventIds = CollectAsyncEventIds(events); + int coreEventCount = eventIds.FindAll(id => id == AsyncEventID.ResumeAsyncContext || id == AsyncEventID.SuspendAsyncContext || id == AsyncEventID.CompleteAsyncContext).Count; + + Assert.True(coreEventCount > 0, "Expected periodic timer to flush buffer with core lifecycle events"); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_PeriodicTimerFlush_PreservesOwnerThreadId() + { + // This test verifies that when the background flush timer flushes a thread's buffer, + // the new header written afterwards preserves the owning thread's OS thread ID + // (not the timer thread's ID). + // + // Strategy: run async work on a dedicated thread so its profiler context gets events. + // Between two batches of work, wait for the flush timer to fire. Both buffer flushes + // from the dedicated thread should carry the same OsThreadId. + + ulong workerOsThreadId = 0; + var workerIdReady = new ManualResetEventSlim(false); + var firstBatchDone = new ManualResetEventSlim(false); + var firstFlushSeen = new ManualResetEventSlim(false); + var workerEvents = new ConcurrentQueue(); + + using (var listener = CreateListener(CoreKeywords)) + { + listener.RunWithCallback(e => + { + if (!workerIdReady.IsSet) + return; + if (e.EventId != AsyncEventsId || e.Payload is null || e.Payload.Count == 0) + return; + if (e.Payload[0] is not byte[] payload) + return; + EventBufferHeader? header = ParseEventBufferHeader(payload); + if (header is not null && header.Value.OsThreadId == workerOsThreadId) + workerEvents.Enqueue(e); + }, () => + { + SendFlushCommand(); + + var thread = new Thread(() => + { + workerOsThreadId = GetCurrentOSThreadId(); + workerIdReady.Set(); + + // First batch: generate events on this thread's profiler context. + Func().GetAwaiter().GetResult(); + firstBatchDone.Set(); + + // Wait for the flush to deliver our first buffer before generating more events. + bool flushed = firstFlushSeen.Wait(TimeSpan.FromSeconds(20)); + Assert.True(flushed, "Expected first flush of core lifecycle events within timeout"); + + // Second batch: generate more events on the same thread's context. + Func().GetAwaiter().GetResult(); + }); + + thread.IsBackground = true; + thread.Start(); + + // Wait for the worker to finish its first batch, then force flush. + firstBatchDone.Wait(TimeSpan.FromSeconds(20)); + SendFlushCommand(); + + // Poll for first buffer from our worker thread. + bool firstFlush = SpinWait.SpinUntil(() => workerEvents.Count >= 1, TimeSpan.FromSeconds(20)); + Assert.True(firstFlush, "Expected periodic timer to flush core lifecycle events within timeout"); + + firstFlushSeen.Set(); + + // Wait for the worker to finish its second batch. + bool joined = thread.Join(TimeSpan.FromSeconds(20)); + Assert.True(joined, "Expected worker thread to terminate within timeout after second batch of work"); + + // Force a flush to deliver the second batch. + SendFlushCommand(); + + // Poll for second buffer from our worker thread. + bool secondFlush = SpinWait.SpinUntil(() => workerEvents.Count >= 2, TimeSpan.FromSeconds(20)); + Assert.True(secondFlush, "Expected periodic timer to flush core lifecycle events within timeout"); + }); + } + + // DumpCollectedEvents(workerEvents); + + Assert.True(workerOsThreadId != 0, "Failed to capture worker OS thread ID"); + + // The key assertion: find buffers that contain CreateAsyncContext events (our work batches). + // There must be at least 2 such buffers (one per Func() call), and ALL of them must + // have the worker's OsThreadId — proving the timer flush didn't corrupt the header. + int workBufferCount = 0; + foreach (EventWrittenEventArgs e in workerEvents) + { + if (e.EventId != AsyncEventsId || e.Payload is null || e.Payload.Count == 0) + continue; + if (e.Payload[0] is not byte[] payload) + continue; + + bool hasCreateEvent = false; + ParseEventBuffer(payload, (AsyncEventID eventId, ReadOnlySpan buf, ref int idx) => + { + if (eventId == AsyncEventID.CreateAsyncContext) + hasCreateEvent = true; + return SkipEventPayload(eventId, buf, ref idx); + }); + + if (hasCreateEvent) + { + workBufferCount++; + EventBufferHeader? header = ParseEventBufferHeader(payload); + Assert.NotNull(header); + Assert.Equal(workerOsThreadId, header.Value.OsThreadId); + } + } + + Assert.True(workBufferCount >= 2, $"Expected at least 2 buffers with CreateAsyncContext from the worker thread, got {workBufferCount}"); + } + + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_DeadThreadFlush() + { + var events = CollectEvents(CoreKeywords, (collectedEvents, _) => + { + // Spawn a dedicated thread that runs async work then exits. + // Its thread-local buffer becomes orphaned when the thread dies. + var thread = new Thread(() => + { + RunScenario(async () => + { + await Func(); + }); + }); + + thread.IsBackground = true; + thread.Start(); + bool joined = thread.Join(TimeSpan.FromSeconds(20)); + Assert.True(joined, "Expected worker thread to terminate within timeout before waiting for orphaned buffer flush"); + + // Do NOT send a flush command. + // Wait for the periodic flush timer to detect the dead thread and flush its orphaned buffer. + Thread.Sleep(1000); + + // Poll to make sure the expected buffer got flush. + bool flushed = SpinWait.SpinUntil(() => + { + var ids = CollectAsyncEventIds(collectedEvents); + return ids.Exists(id => id == AsyncEventID.ResumeAsyncContext || id == AsyncEventID.SuspendAsyncContext || id == AsyncEventID.CompleteAsyncContext); + }, TimeSpan.FromSeconds(20)); + + Assert.True(flushed, "Expected periodic timer to flush buffer with core lifecycle events within timeout"); + }); + + // DumpCollectedEvents(events); + + var eventIds = CollectAsyncEventIds(events); + int coreEventCount = eventIds.FindAll(id => id == AsyncEventID.ResumeAsyncContext || id == AsyncEventID.SuspendAsyncContext || id == AsyncEventID.CompleteAsyncContext).Count; + + Assert.True(coreEventCount > 0, "Expected periodic timer to flush dead thread's buffer"); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_NoSyncClockEventBeforeInterval() + { + var events = CollectEvents(CoreKeywords, () => + { + RunScenarioAndFlush(async () => + { + await Func(); + }); + }); + + // DumpCollectedEvents(events); + + var eventIds = CollectAsyncEventIds(events); + + Assert.DoesNotContain(AsyncEventID.AsyncProfilerSyncClock, eventIds); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_NoEventsWhenDisabled() + { + // Run async work WITHOUT a listener attached + Task.Run(async () => + { + for (int i = 0; i < 50; i++) + { + await Func(); + } + }).GetAwaiter().GetResult(); + + // Now attach listener and verify no stale events are emitted + var events = CollectEvents(CoreKeywords, () => + { + // Don't run any async work - just check nothing comes through from before + Thread.Sleep(100); + }); + + // DumpCollectedEvents(events); + + // There may be a ResetAsyncThreadContext from the SyncPoint when keywords change, + // but there should be no suspend/resume/complete events from the earlier work. + var eventIds = CollectAsyncEventIds(events); + int contextEvents = eventIds.FindAll(id => id == AsyncEventID.ResumeAsyncContext || id == AsyncEventID.SuspendAsyncContext || id == AsyncEventID.CompleteAsyncContext).Count; + + Assert.Equal(0, contextEvents); + } + + public static IEnumerable KeywordGatekeepingData() + { + yield return new object[] { (long)CreateAsyncContextKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.CreateAsyncContext, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)ResumeAsyncContextKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.ResumeAsyncContext, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)SuspendAsyncContextKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.SuspendAsyncContext, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)CompleteAsyncContextKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.CompleteAsyncContext, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)UnwindAsyncExceptionKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.UnwindAsyncException, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)CreateAsyncCallstackKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.CreateAsyncCallstack, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)ResumeAsyncCallstackKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.ResumeAsyncCallstack, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)SuspendAsyncCallstackKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.SuspendAsyncCallstack, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)ResumeAsyncMethodKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.ResumeAsyncMethod, AsyncEventID.AsyncProfilerMetadata } }; + yield return new object[] { (long)CompleteAsyncMethodKeyword, new AsyncEventID[] { AsyncEventID.ResetAsyncThreadContext, AsyncEventID.CompleteAsyncMethod, AsyncEventID.AsyncProfilerMetadata } }; + } + + [ConditionalTheory(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + [MemberData(nameof(KeywordGatekeepingData))] + public void RuntimeAsync_KeywordGatekeeping(long keywordValue, AsyncEventID[] allowedEventIds) + { + EventKeywords kw = (EventKeywords)keywordValue; + var allowed = new HashSet(allowedEventIds); + + var events = CollectEvents(kw, () => + { + // Run a scenario that exercises all event types: resume, suspend, + // complete, method events, callstacks, and exception unwinds. + // Only the events matching the enabled keyword should be emitted. + RunScenarioAndFlush(async () => + { + await OuterCatches(); + await FuncChained(); + }); + }); + + // DumpCollectedEvents(events); + + var eventIds = CollectAsyncEventIds(events); + var unexpected = eventIds.FindAll(id => !allowed.Contains(id)); + + Assert.True(unexpected.Count == 0, + $"Keyword 0x{(long)kw:X}: unexpected event IDs [{string.Join(", ", unexpected)}], " + + $"allowed [{string.Join(", ", allowed)}]"); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_ResetAsyncThreadContextEvent() + { + var events = CollectEvents(CoreKeywords, () => + { + RunScenarioAndFlush(async () => + { + await Func(); + }); + }); + + // DumpCollectedEvents(events); + + var eventIds = CollectAsyncEventIds(events); + + Assert.Contains(AsyncEventID.ResetAsyncThreadContext, eventIds); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_MetadataEventEmittedOnEnable() + { + var events = CollectEvents(AllKeywords, () => + { + RunScenarioAndFlush(async () => + { + await Func(); + }); + }); + + // DumpCollectedEvents(events); + + var metadataList = CollectMetadataFromBuffer(events); + Assert.True(metadataList.Count >= 1, "Expected at least one metadata event in buffer"); + + MetadataFromBuffer meta = metadataList[0]; + Assert.True(meta.QpcFrequency > 0, $"QPC frequency should be positive, got {meta.QpcFrequency}"); + Assert.True(meta.QpcSync > 0, $"QPC sync timestamp should be positive, got {meta.QpcSync}"); + Assert.True(meta.UtcSync > 0, $"UTC sync timestamp should be positive, got {meta.UtcSync}"); + Assert.True(meta.EventBufferSize > 0, $"Event buffer size should be positive, got {meta.EventBufferSize}"); + Assert.True(meta.WrapperIPs.Length > 0, "Wrapper IPs array should not be empty"); + Assert.All(meta.WrapperIPs, ip => Assert.True(ip != 0, "Each wrapper IP should be non-zero")); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_MetadataEventEmittedOnceAcrossThreads() + { + const int threadCount = 8; + + var events = CollectEvents(AllKeywords, () => + { + using var barrier = new Barrier(threadCount); + var tasks = new Task[threadCount]; + for (int i = 0; i < threadCount; i++) + { + tasks[i] = Task.Factory.StartNew(() => + { + barrier.SignalAndWait(); + Func().GetAwaiter().GetResult(); + }, TaskCreationOptions.LongRunning); + } + Task.WaitAll(tasks); + SendFlushCommand(); + }); + + // DumpCollectedEvents(events); + + var metadataList = CollectMetadataFromBuffer(events); + Assert.True(metadataList.Count == 1, $"Expected exactly 1 metadata event across {threadCount} threads, got {metadataList.Count}"); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_CallstackNativeIPDeltaRoundtrip() + { + // Verify that delta-encoded NativeIPs in callstacks roundtrip correctly, + // including both positive and negative deltas. With multiple distinct async + // methods at different JIT-assigned addresses, the deltas between consecutive + // NativeIPs will naturally span both directions. This exercises the full + // zigzag + LEB128 encode/decode path through the production serializer. + var events = CollectEvents(CallstackKeywords, () => + { + RunScenarioAndFlush(async () => + { + // Run several different call chains to maximize address variation. + await FuncChained(); + await DeepOuterCatches(); + await RecursiveFunc(10); + }); + }); + + var callstacks = CollectCallstacks(events); + Assert.NotEmpty(callstacks); + + // Find callstacks with 3+ frames — enough depth for meaningful deltas. + var deepCallstacks = callstacks.Where(cs => cs.FrameCount >= 3).ToList(); + Assert.True(deepCallstacks.Count > 0, + "Expected at least one callstack with 3+ frames for delta verification"); + + bool hasPositiveDelta = false; + bool hasNegativeDelta = false; + + foreach (var cs in deepCallstacks) + { + for (int i = 0; i < cs.Frames.Count; i++) + { + var (nativeIP, _) = cs.Frames[i]; + Assert.True(nativeIP != 0, $"Frame {i} has zero NativeIP"); + var method = GetMethodFromNativeIP(nativeIP); + Assert.True(method is not null, + $"Frame {i}: NativeIP 0x{nativeIP:X} does not resolve to a managed method"); + + if (i > 0) + { + long delta = (long)(cs.Frames[i].NativeIP - cs.Frames[i - 1].NativeIP); + if (delta > 0) + hasPositiveDelta = true; + else if (delta < 0) + hasNegativeDelta = true; + } + } + } + + // With multiple distinct async methods at different addresses, we expect + // both positive and negative deltas. If the JIT happens to lay out all + // methods monotonically (extremely unlikely), at minimum we must see + // non-zero deltas proving the encoding works. + Assert.True(hasPositiveDelta || hasNegativeDelta, + "Expected at least one non-zero NativeIP delta across all callstack frames"); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_CallstackStressWithVaryingDepths() + { + // Stress test: run many async calls with varying callstack depths. + // Varying sizes mean some callstacks will land at buffer boundaries, + // naturally exercising the overflow/rewind path in callstack emission. + // RecursiveFunc(d) produces exactly d frames on the chain. The lambda + // that calls it adds one more frame, so the total callstack depth is d + 1. + const int iterations = 200; + int[] depths = new int[iterations]; + var rng = new Random(42); + for (int i = 0; i < iterations; i++) + depths[i] = rng.Next(1, 120); + + var events = CollectEvents(CallstackKeywords, () => + { + RunScenarioAndFlush(async () => + { + for (int i = 0; i < iterations; i++) + await RecursiveFunc(depths[i]); + }); + }); + + // DumpCollectedEvents(events); + + // Collect all resume callstacks with timestamps, sorted by timestamp. + var callstacksWithTimestamp = new List<(long Timestamp, byte FrameCount, List<(ulong NativeIP, int State)> Frames)>(); + ForEachEventBufferPayload(events, buffer => + { + ParseEventBuffer(buffer, (AsyncEventID eventId, long timestamp, ReadOnlySpan buf, ref int idx) => + { + if (eventId == AsyncEventID.ResumeAsyncCallstack) + { + ReadCallstackPayload(buf, ref idx, out byte frameCount, out var frames); + callstacksWithTimestamp.Add((timestamp, frameCount, frames)); + return true; + } + return SkipEventPayload(eventId, buf, ref idx); + }); + }); + + callstacksWithTimestamp.Sort((a, b) => a.Timestamp.CompareTo(b.Timestamp)); + + // Verify all callstacks have valid frame data that resolves to managed methods. + foreach (var cs in callstacksWithTimestamp) + { + Assert.True(cs.FrameCount > 0, "Callstack has 0 frames"); + Assert.Equal(cs.FrameCount, cs.Frames.Count); + for (int f = 0; f < cs.Frames.Count; f++) + { + var (nativeIP, _) = cs.Frames[f]; + Assert.True(nativeIP != 0, $"Frame {f} has zero NativeIP"); + var method = GetMethodFromNativeIP(nativeIP); + Assert.True(method is not null, + $"Frame {f}: NativeIP 0x{nativeIP:X} does not resolve to a managed method"); + } + } + + // One resume callstack per iteration; find our sequence at the end + // (earlier entries may be from metadata/warmup). + Assert.True(callstacksWithTimestamp.Count >= iterations, + $"Expected at least {iterations} callstacks, got {callstacksWithTimestamp.Count}"); + + int startOffset = callstacksWithTimestamp.Count - iterations; + for (int i = 0; i < iterations; i++) + { + int expected = depths[i] + 1; + int actual = callstacksWithTimestamp[startOffset + i].FrameCount; + Assert.True(actual == expected, + $"Iteration {i}: expected depth {expected} (RecursiveFunc({depths[i]}) + lambda), got {actual}"); + } + + // Verify multiple buffer flushes occurred. + int bufferCount = 0; + ForEachEventBufferPayload(events, _ => bufferCount++); + Assert.True(bufferCount >= 3, $"Expected at least 3 buffer flushes, got {bufferCount}"); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_CallstackOverflowPathProducesValidFrames() + { + // Targeted test: run random-depth callstacks until we detect the overflow + // path was exercised, then validate the affected callstack. + // The overflow path fires when a large callstack doesn't fit inline in the + // remaining buffer space — the code rewinds, flushes, and re-writes the + // callstack as the first event in a fresh buffer. + bool overflowDetected = false; + var rng = new Random(42); + + for (int attempt = 0; attempt < 10 && !overflowDetected; attempt++) + { + int iterations = 500; + int[] depths = new int[iterations]; + for (int i = 0; i < iterations; i++) + depths[i] = rng.Next(50, 250); + + var events = CollectEvents(ResumeAsyncCallstackKeyword, () => + { + RunScenarioAndFlush(async () => + { + for (int i = 0; i < iterations; i++) + await RecursiveFunc(depths[i]); + }); + }); + + // Check each buffer: if the first event is a large ResumeAsyncCallstack, + // the overflow path flushed the previous buffer and re-wrote here. + ForEachEventBufferPayload(events, buffer => + { + if (overflowDetected) + return; + + int index = HeaderSize; + if (index + 2 > buffer.Length) + return; + + AsyncEventID firstEvent = (AsyncEventID)buffer[index++]; + ReadCompressedUInt64(buffer, ref index); + if (firstEvent != AsyncEventID.ResumeAsyncCallstack) + return; + + ReadCallstackPayload(buffer, ref index, out byte frameCount, out var frames); + if (frameCount <= 30) + return; + + overflowDetected = true; + + Assert.Equal(frameCount, frames.Count); + for (int f = 0; f < frames.Count; f++) + { + var (nativeIP, _) = frames[f]; + Assert.True(nativeIP != 0, $"Overflow callstack frame {f} has zero NativeIP"); + var method = GetMethodFromNativeIP(nativeIP); + Assert.True(method is not null, + $"Overflow callstack frame {f}: NativeIP 0x{nativeIP:X} does not resolve to a managed method"); + } + }); + } + + Assert.True(overflowDetected, + "Failed to trigger callstack buffer overflow after 10 attempts"); + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_CallstackDepthCappedAtMaxFrames() + { + // Verify that callstack depth is capped when the continuation chain + // exceeds the maximum frame count (255, limited by byte storage). + // RecursiveFunc(300) produces a 300-deep chain + 1 lambda = 301 frames. + const int requestedDepth = 300; + + var events = CollectEvents(ResumeAsyncCallstackKeyword, () => + { + RunScenarioAndFlush(async () => + { + await RecursiveFunc(requestedDepth); + }); + }); + + // DumpCollectedEvents(events); + + var callstacks = CollectCallstacks(events); + Assert.True(callstacks.Count >= 1, "Expected at least one callstack"); + + // Find the callstack from our deep RecursiveFunc call. + // The max frame count is capped at 255 (byte.MaxValue) since the + // CaptureRuntimeAsyncCallstackState.Count is a byte. + // RecursiveFunc(300) + 1 lambda = 301 frames, capped to 255. + var deepest = callstacks.MaxBy(cs => cs.FrameCount); + Assert.Equal(255, deepest.FrameCount); + Assert.Equal(deepest.FrameCount, deepest.Frames.Count); + + // Verify all frames are valid. + foreach (var (nativeIP, _) in deepest.Frames) + { + Assert.True(nativeIP != 0, "Frame has zero NativeIP"); + var method = GetMethodFromNativeIP(nativeIP); + Assert.True(method is not null, + $"NativeIP 0x{nativeIP:X} does not resolve to a managed method"); + } + } + + [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsRuntimeAsyncAndThreadingSupported))] + public void RuntimeAsync_MetadataWrapperIPsMatchMethods() + { + var events = CollectEvents(AllKeywords, () => + { + RunScenarioAndFlush(async () => + { + await Func(); + }); + }); + + // DumpCollectedEvents(events); + + var metadataList = CollectMetadataFromBuffer(events); + Assert.True(metadataList.Count >= 1, "Expected at least one metadata event in buffer"); + + long[] wrapperIPs = metadataList[0].WrapperIPs; + + Type? cwType = typeof(object).Assembly.GetType("System.Runtime.CompilerServices.AsyncProfiler+ContinuationWrapper"); + Assert.NotNull(cwType); + + for (int i = 0; i < wrapperIPs.Length; i++) + { + string expectedName = $"Continuation_Wrapper_{i}"; + MethodInfo? method = cwType.GetMethod(expectedName, BindingFlags.NonPublic | BindingFlags.Static); + Assert.True(method is not null, $"Expected method '{expectedName}' to exist on ContinuationWrapper type"); + + System.Runtime.CompilerServices.RuntimeHelpers.PrepareMethod(method.MethodHandle); + long expectedIP = method.MethodHandle.GetFunctionPointer().ToInt64(); + + Assert.True(wrapperIPs[i] == expectedIP, + $"Wrapper IP mismatch at index {i}: metadata has 0x{wrapperIPs[i]:X}, " + + $"method '{expectedName}' has 0x{expectedIP:X}"); + } + } + } + + internal static class EventBuffer + { + public static int OutputEventBuffer(ReadOnlySpan buffer) + { + Console.WriteLine("--- AsyncEvents ---"); + + int index = 0; + + if ((uint)buffer.Length < 1) + { + Console.WriteLine("Buffer too small."); + Console.WriteLine("----------------------------------"); + return index; + } + + byte version = buffer[index++]; + Console.WriteLine($"Version: {version}"); + + if (version != 1) + { + Console.WriteLine($"Unsupported version: {version}"); + Console.WriteLine("----------------------------------"); + return index; + } + + Deserializer.ReadUInt32(buffer, ref index, out uint totalSize); + Deserializer.ReadUInt32(buffer, ref index, out uint contextId); + Deserializer.ReadUInt64(buffer, ref index, out ulong osThreadId); + Deserializer.ReadUInt32(buffer, ref index, out uint totalEventCount); + Deserializer.ReadUInt64(buffer, ref index, out ulong startTimestamp); + Deserializer.ReadUInt64(buffer, ref index, out ulong endTimestamp); + + Console.WriteLine($"TotalSize (bytes): {totalSize}"); + Console.WriteLine($"AsyncThreadContextId: {contextId}"); + Console.WriteLine($"OSThreadId: {osThreadId}"); + Console.WriteLine($"TotalEventCount: {totalEventCount}"); + Console.WriteLine($"StartTimestamp: 0x{startTimestamp:X16}"); + Console.WriteLine($"EndTimestamp: 0x{endTimestamp:X16}"); + + int eventCount = 0; + ulong currentTimestamp = startTimestamp; + + while (index < buffer.Length) + { + if (index + 2 > buffer.Length) + { + Console.WriteLine($"Trailing bytes: {buffer.Length - index} (incomplete entry header)."); + break; + } + + AsyncEventID eventId = (AsyncEventID)buffer[index++]; + + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong delta); + currentTimestamp += delta; + + Console.WriteLine($"Entry[{eventCount}]: Timestamp=0x{currentTimestamp:X16}, EventId={eventId}"); + + int payloadStart = index; + try + { + index += eventId switch + { + AsyncEventID.CreateAsyncContext => OutputCreateAsyncContextEvent(buffer.Slice(index)), + AsyncEventID.ResumeAsyncContext => OutputResumeAsyncContextEvent(buffer.Slice(index)), + AsyncEventID.SuspendAsyncContext => OutputSuspendAsyncContextEvent(), + AsyncEventID.CompleteAsyncContext => OutputCompleteAsyncContextEvent(), + AsyncEventID.UnwindAsyncException => OutputUnwindAsyncExceptionEvent(buffer.Slice(index)), + AsyncEventID.CreateAsyncCallstack => OutputAsyncCallstackEvent("CreateAsyncCallstack", buffer.Slice(index)), + AsyncEventID.ResumeAsyncCallstack => OutputAsyncCallstackEvent("ResumeAsyncCallstack", buffer.Slice(index)), + AsyncEventID.SuspendAsyncCallstack => OutputAsyncCallstackEvent("SuspendAsyncCallstack", buffer.Slice(index)), + AsyncEventID.ResumeAsyncMethod => OutputResumeAsyncMethodEvent(), + AsyncEventID.CompleteAsyncMethod => OutputCompleteAsyncMethodEvent(), + AsyncEventID.ResetAsyncThreadContext => OutputResetAsyncThreadContextEvent(), + AsyncEventID.ResetAsyncContinuationWrapperIndex => OutputResetAsyncContinuationWrapperIndexEvent(), + AsyncEventID.AsyncProfilerMetadata => OutputAsyncProfilerMetadataEvent(buffer.Slice(index)), + _ => throw new InvalidOperationException($"Unknown eventId {eventId}."), + }; + } + catch (Exception ex) + { + Console.WriteLine($" Failed decoding entry payload at offset {payloadStart}: {ex.GetType().Name}: {ex.Message}"); + break; + } + + eventCount++; + } + + Console.WriteLine($"TotalEntriesDecoded: {eventCount}"); + Console.WriteLine("----------------------------------"); + + return index; + } + + private static int OutputCreateAsyncContextEvent(ReadOnlySpan buffer) + { + int index = 0; + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong id); + Console.WriteLine("--- CreateAsyncContext ---"); + Console.WriteLine($" ID: {id}"); + Console.WriteLine("----------------------------"); + return index; + } + + private static int OutputResumeAsyncContextEvent(ReadOnlySpan buffer) + { + int index = 0; + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong id); + Console.WriteLine("--- ResumeAsyncContext ---"); + Console.WriteLine($" ID: {id}"); + Console.WriteLine("----------------------------"); + return index; + } + + private static int OutputSuspendAsyncContextEvent() + { + Console.WriteLine("--- SuspendAsyncContext ---"); + Console.WriteLine("----------------------------"); + return 0; + } + + private static int OutputCompleteAsyncContextEvent() + { + Console.WriteLine("--- CompleteAsyncContext ---"); + Console.WriteLine("----------------------------"); + return 0; + } + + private static int OutputUnwindAsyncExceptionEvent(ReadOnlySpan buffer) + { + uint unwindedFrames; + int index = 0; + + Deserializer.ReadCompressedUInt32(buffer, ref index, out unwindedFrames); + index += OutputUnwindAsyncExceptionEvent(unwindedFrames); + + return index; + } + + private static int OutputUnwindAsyncExceptionEvent(uint unwindedFrames) + { + Console.WriteLine("--- UnwindAsyncException ---"); + Console.WriteLine($"Unwinded Frames: {unwindedFrames}"); + Console.WriteLine("----------------------------"); + return 0; + } + + private static int OutputResumeAsyncMethodEvent() + { + Console.WriteLine("--- ResumeAsyncMethod ---"); + Console.WriteLine("----------------------------"); + return 0; + } + + private static int OutputCompleteAsyncMethodEvent() + { + Console.WriteLine("--- CompleteAsyncMethod ---"); + Console.WriteLine("----------------------------"); + return 0; + } + + private static int OutputResetAsyncContinuationWrapperIndexEvent() + { + Console.WriteLine("--- ResetAsyncContinuationWrapperIndex ---"); + Console.WriteLine("----------------------------"); + return 0; + } + + private static int OutputResetAsyncThreadContextEvent() + { + Console.WriteLine("--- ResetAsyncThreadContext ---"); + Console.WriteLine("----------------------------"); + return 0; + } + + private static int OutputAsyncProfilerMetadataEvent(ReadOnlySpan buffer) + { + int index = 0; + Console.WriteLine("--- AsyncProfilerMetadata ---"); + + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong qpcFrequency); + Console.WriteLine($" QPCFrequency: {qpcFrequency}"); + + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong qpcSync); + Console.WriteLine($" QPCSync: {qpcSync}"); + + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong utcSync); + Console.WriteLine($" UTCSync: {utcSync}"); + + Deserializer.ReadCompressedUInt32(buffer, ref index, out uint eventBufferSize); + Console.WriteLine($" EventBufferSize: {eventBufferSize}"); + + byte wrapperCount = buffer[index++]; + Console.WriteLine($" WrapperCount: {wrapperCount}"); + + for (int i = 0; i < wrapperCount; i++) + { + Deserializer.ReadCompressedUInt64(buffer, ref index, out ulong ip); + Console.WriteLine($" Wrapper[{i}]: 0x{ip:X16}"); + } + + Console.WriteLine("----------------------------"); + return index; + } + + private static int OutputAsyncCallstackEvent(string eventName, ReadOnlySpan buffer) + { + ulong id; + byte type; + byte callstackId; + byte asyncCallstackLength; + int index = 0; + + type = buffer[index++]; + callstackId = buffer[index++]; + asyncCallstackLength = buffer[index++]; + Deserializer.ReadCompressedUInt64(buffer, ref index, out id); + + Console.WriteLine($"--- {eventName} ---"); + Console.WriteLine($"ID: {id}"); + Console.WriteLine($"Type: {type}"); + Console.WriteLine($"CallstackId: {callstackId}"); + Console.WriteLine($"Length: {asyncCallstackLength}"); + + if (asyncCallstackLength == 0) + { + return index; + } + + ulong previousNativeIP; + ulong currentNativeIP; + int state; + + Deserializer.ReadCompressedUInt64(buffer, ref index, out currentNativeIP); + Deserializer.ReadCompressedInt32(buffer, ref index, out state); + + OutputAsyncFrame(currentNativeIP, state, 0); + + for (int i = 1; i < asyncCallstackLength; i++) + { + previousNativeIP = currentNativeIP; + Deserializer.ReadCompressedInt64(buffer, ref index, out long nativeIPDelta); + Deserializer.ReadCompressedInt32(buffer, ref index, out state); + currentNativeIP = previousNativeIP + (ulong)nativeIPDelta; + OutputAsyncFrame(currentNativeIP, state, i); + } + + return index; + } + + private static readonly MethodInfo? s_getMethodFromNativeIP = + typeof(StackFrame).GetMethod("GetMethodFromNativeIP", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); + + private static string ResolveAsyncMethodName(nint nativeIP) + { + if (s_getMethodFromNativeIP is not null) + { + try + { + MethodBase? method = s_getMethodFromNativeIP.Invoke(null, [nativeIP]) as MethodBase; + return method?.Name ?? string.Empty; + } + catch + { + } + } + + return string.Empty; + } + + private static void OutputAsyncFrame(ulong nativeIP, int state, int frameIndex) + { + string asyncMethodName = ResolveAsyncMethodName((nint)nativeIP); + asyncMethodName = !string.IsNullOrEmpty(asyncMethodName) ? asyncMethodName : $"??"; + string nativeIPString = $"0x{nativeIP:X}"; + Console.WriteLine($" Frame {frameIndex}: AsyncMethod = {asyncMethodName}, NativeIP = {nativeIPString}, State = {state}"); + } + + internal static class Deserializer + { + public static void ReadInt32(ReadOnlySpan buffer, ref int index, out int value) + { + uint uValue; + ReadUInt32(buffer, ref index, out uValue); + value = (int)uValue; + } + + public static void ReadCompressedInt32(ReadOnlySpan buffer, ref int index, out int value) + { + uint uValue; + ReadCompressedUInt32(buffer, ref index, out uValue); + value = ZigzagDecodeInt32(uValue); + } + + public static void ReadUInt32(ReadOnlySpan buffer, ref int index, out uint value) + { + value = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(index)); + index += 4; + } + + public static void ReadCompressedUInt32(ReadOnlySpan buffer, ref int index, out uint value) + { + int shift = 0; + byte b; + + value = 0; + do + { + b = buffer[index++]; + value |= (uint)(b & 0x7F) << shift; + shift += 7; + } while ((b & 0x80) != 0); + } + + public static void ReadInt64(ReadOnlySpan buffer, ref int index, out long value) + { + ulong uValue; + ReadUInt64(buffer, ref index, out uValue); + value = (long)uValue; + } + + public static void ReadCompressedInt64(ReadOnlySpan buffer, ref int index, out long value) + { + ulong uValue; + ReadCompressedUInt64(buffer, ref index, out uValue); + value = ZigzagDecodeInt64(uValue); + } + + public static void ReadUInt64(ReadOnlySpan buffer, ref int index, out ulong value) + { + value = BinaryPrimitives.ReadUInt64LittleEndian(buffer.Slice(index)); + index += 8; + } + + public static void ReadCompressedUInt64(ReadOnlySpan buffer, ref int index, out ulong value) + { + int shift = 0; + byte b; + + value = 0; + do + { + b = buffer[index++]; + value |= (ulong)(b & 0x7F) << shift; + shift += 7; + } while ((b & 0x80) != 0); + } + + private static int ZigzagDecodeInt32(uint value) => (int)((value >> 1) ^ (~(value & 1) + 1)); + + private static long ZigzagDecodeInt64(ulong value) => (long)((value >> 1) ^ (~(value & 1) + 1)); + } + } +} diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.cs b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.cs index 7e28cbbfd1e1d9..5501e1feca5fba 100644 --- a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.cs +++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.cs @@ -22,25 +22,25 @@ public class RuntimeAsyncTests private static readonly FieldInfo s_activeFlagsField = GetCorLibClassStaticField("System.Runtime.CompilerServices.AsyncInstrumentation", "s_activeFlags"); private static readonly object s_debuggerLock = new object(); - private static TestEventListener? s_debuggerTplInstance; // AsyncDebugger(0x2000000) | all event flags(0x7F) private const uint EnabledInstrumentationFlags = 0x200007F; private const uint DisabledInstrumentationFlags = 0x0; - private const uint UninitializedInstrumentationFlags = 0x80000000; + private const uint SynchronizeInstrumentationFlags = 0x80000000; private static void AttachDebugger() { - // Simulate a debugger attach to process, creating TPL event source session + setting s_asyncDebuggingEnabled. + // Simulate a debugger attach to process by setting s_asyncDebuggingEnabled + // and triggering a flag synchronization via the Synchronize bit. lock (s_debuggerLock) { uint flags = Convert.ToUInt32(s_activeFlagsField.GetValue(null)); - Assert.True(flags == UninitializedInstrumentationFlags || flags == DisabledInstrumentationFlags, $"ActiveFlags equals {flags}, expected {UninitializedInstrumentationFlags} || {DisabledInstrumentationFlags}"); + Assert.True(flags == SynchronizeInstrumentationFlags || flags == DisabledInstrumentationFlags, $"ActiveFlags equals {flags}, expected {SynchronizeInstrumentationFlags} || {DisabledInstrumentationFlags}"); - s_debuggerTplInstance = new TestEventListener("System.Threading.Tasks.TplEventSource", EventLevel.Verbose); s_asyncDebuggingEnabledField.SetValue(null, true); + s_activeFlagsField.SetValue(null, flags | SynchronizeInstrumentationFlags); - // Initialize flags and collections. + // Run an async method to trigger SyncActiveFlags which will pick up the Synchronize bit. Func().GetAwaiter().GetResult(); flags = Convert.ToUInt32(s_activeFlagsField.GetValue(null)); @@ -62,11 +62,15 @@ private static void DetachDebugger() // Simulate a debugger detach from process. lock (s_debuggerLock) { + uint flags = Convert.ToUInt32(s_activeFlagsField.GetValue(null)); s_asyncDebuggingEnabledField.SetValue(null, false); - s_debuggerTplInstance?.Dispose(); - s_debuggerTplInstance = null; + s_activeFlagsField.SetValue(null, flags | SynchronizeInstrumentationFlags); - uint flags = Convert.ToUInt32(s_activeFlagsField.GetValue(null)); + // Run an async method to trigger SyncActiveFlags which will detect + // s_asyncDebuggingEnabled is false and clear the debugger flags. + Func().GetAwaiter().GetResult(); + + flags = Convert.ToUInt32(s_activeFlagsField.GetValue(null)); Assert.True(flags == DisabledInstrumentationFlags, $"ActiveFlags equals {flags}, expected {DisabledInstrumentationFlags}"); } } @@ -181,7 +185,6 @@ static void ValidateTimestampsCleared() } [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_TaskCompleted() { RemoteExecutor.Invoke(async () => @@ -201,7 +204,6 @@ public void RuntimeAsync_TaskCompleted() } [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_ExceptionCleanup() { RemoteExecutor.Invoke(async () => @@ -227,7 +229,6 @@ public void RuntimeAsync_ExceptionCleanup() } [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_DebuggerDetach() { RemoteExecutor.Invoke(async () => @@ -288,7 +289,6 @@ public void RuntimeAsync_DebuggerDetach() } [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_ValueTypeResult() { RemoteExecutor.Invoke(async () => @@ -309,7 +309,6 @@ public void RuntimeAsync_ValueTypeResult() } [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_HandledExceptionPartialUnwind() { RemoteExecutor.Invoke(async () => @@ -329,7 +328,6 @@ public void RuntimeAsync_HandledExceptionPartialUnwind() } [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_CancellationCleanup() { RemoteExecutor.Invoke(async () => @@ -358,7 +356,6 @@ public void RuntimeAsync_CancellationCleanup() } [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_TimestampsTrackedWhileInFlight() { RemoteExecutor.Invoke(async () => @@ -421,7 +418,6 @@ public void RuntimeAsync_TimestampsTrackedWhileInFlight() } [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_ContinuationTimestampObservedDuringResume() { RemoteExecutor.Invoke(async () => @@ -454,7 +450,6 @@ public void RuntimeAsync_ContinuationTimestampObservedDuringResume() } [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_InFlightInstrumentationUpgrade() { RemoteExecutor.Invoke(async () => @@ -504,7 +499,6 @@ public void RuntimeAsync_InFlightInstrumentationUpgrade() } [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_TplEvents() { RemoteExecutor.Invoke(() => @@ -539,7 +533,6 @@ public void RuntimeAsync_TplEvents() } [ConditionalFact(typeof(RuntimeAsyncTests), nameof(IsRemoteExecutorAndRuntimeAsyncSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/124072", typeof(PlatformDetection), nameof(PlatformDetection.IsInterpreter))] public void RuntimeAsync_NoTplEventsWithoutDebugger() { RemoteExecutor.Invoke(() => diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csproj b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csproj index a2298f56f14bd5..a8c936a9e0093c 100644 --- a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csproj @@ -59,6 +59,7 @@ + From 83ebc0092ef5225b1b620c45ecc6c7367d287321 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Mon, 4 May 2026 13:06:08 +0200 Subject: [PATCH 085/115] Restore `ExecutionContext` from dispatcher instead of from interpreter/JIT'ed code (#127511) Remove `RestoreExecutionContext` call from JIT resumption code and from the interpreter. Instead encode the `ExecutionContext`'s location in the continuation and let the dispatcher restore it. This removes a TLS access + helper call from the resumption path (avoiding the TLS and inlining the helper call in the dispatcher). Improves suspension/resumption performance by about 7-8% and is also a good size improvement. Removing the call from the resumption path allows keeping the continuation in a volatile register, which also helps register allocation in a number of cases. --- .../coreclr/botr/runtime-async-codegen.md | 28 +++++-- .../CompilerServices/AsyncHelpers.CoreCLR.cs | 51 ++++++++---- src/coreclr/inc/corinfo.h | 15 ++-- src/coreclr/inc/jiteeversionguid.h | 10 +-- src/coreclr/interpreter/compiler.cpp | 42 +++++----- .../interpreter/inc/interpretershared.h | 2 - src/coreclr/jit/async.cpp | 78 ++++++++----------- src/coreclr/jit/fgbasic.cpp | 2 + src/coreclr/jit/fgopt.cpp | 12 +++ .../tools/Common/JitInterface/CorInfoImpl.cs | 1 - .../tools/Common/JitInterface/CorInfoTypes.cs | 2 - .../IL/ILImporter.Scanner.cs | 1 - .../Compiler/ReadyToRunCodegenCompilation.cs | 1 - .../tools/superpmi/superpmi-shared/agnostic.h | 1 - .../superpmi-shared/methodcontext.cpp | 2 - src/coreclr/vm/corelib.h | 1 - src/coreclr/vm/interpexec.cpp | 23 ++---- src/coreclr/vm/jitinterface.cpp | 1 - src/coreclr/vm/object.h | 14 ++++ 19 files changed, 156 insertions(+), 131 deletions(-) diff --git a/docs/design/coreclr/botr/runtime-async-codegen.md b/docs/design/coreclr/botr/runtime-async-codegen.md index a914526628e411..44bf31be7437c4 100644 --- a/docs/design/coreclr/botr/runtime-async-codegen.md +++ b/docs/design/coreclr/botr/runtime-async-codegen.md @@ -12,7 +12,7 @@ The general responsibilities of the runtime-async code generator 2. Allow the async thunk logic to work. -3. Generate Async Debug info (Not yet described in this document)f +3. Generate Async Debug info (Not yet described in this document) @@ -37,7 +37,7 @@ call One of the functions which matches NI_System_Runtime_Compiler A search for this sequence is done if Method is known to be async. -The dispatch to async functions save the `ExecutionContext` on suspension and restore it on resumption via `AsyncHelpers.CaptureExecutionContext` and `AsyncHelpers.RestoreExecutionContext` respectively +The dispatch to async functions saves the `ExecutionContext` on suspension to be restored before resumption by `DispatchContinuations`. If PREFIX_TASK_AWAIT_CONTINUE_ON_CAPTURED_CONTEXT, then continuation mode shall be ContinuationContextHandling::ContinueOnCapturedContext otherwise ContinuationContextHandling::ContinueOnThreadPool. @@ -59,9 +59,21 @@ The dispatch to these functions will save and restore the execution context only When encountered, triggers the function to suspend immediately, and return the passed in Continuation. +# Accessing known continuation fields + +The continuation flags encode how to access several well-known fields if they are present in the continuation. +- The execution context +- The continuation context +- The exception object +- The return value + +Each field has a pair of (first bit, number of bits) used to indicate where its details are encoded in the flags. +When the field is present the index is non-zero and the offset is computed as (DataStart + (index - 1) * PointerSize). +If a field is not present the index is zero. + # Saving and restoring of contexts -Capture the execution context before the suspension, and when the function resumes, call `AsyncHelpers.RestoreExecutionContext`. The context should be stored into the Continuation. The context may be captured by calling `AsyncHelpers.CaptureExecutionContext`. +Capture the execution context before the suspension. The context should be stored into the Continuation and its offset encoded via the scheme above. The context may be captured by calling `AsyncHelpers.CaptureExecutionContext`. # ABI for async function handling @@ -87,8 +99,8 @@ if (continuation != NULL) // Resumption point // Copy values out of continuation (including captured sync context and execution context locals) - // If the continuation may have an exception, check to see if its there, and if it is, throw it. Do this if CORINFO\_CONTINUATION\_HAS\_EXCEPTION is set. - // If the continuation has a return value, copy it out of the continuation. (CORINFO\_CONTINUATION\_HAS\_RESULT is set) + // If the continuation may have an exception, check to see if its there, and if it is, throw it. Do this if the flags indicate an exception object is present. + // If the continuation has a return value, copy it out of the continuation. Do this if the flags indicate a result is present. } ``` @@ -109,16 +121,16 @@ This only applies to calls which where ContinuationContextHandling is not Contin If set to ContinuationContextHandling::ContinueOnCapturedContext -- The Continuation shall have an allocated data member for the captured context, and the CORINFO_CONTINUATION_HAS_CONTINUATION_CONTEXT flag shall be set on the continuation. +- The Continuation shall have an allocated data member for the captured context, and its offset is encoded in the flags based on the scheme above. -- The Continuation will store the captured synchronization context. This is done by calling `AsyncHelpers.CaptureContinuationContext(ref newContinuation.ContinuationContext, ref newContinuation.Flags)` while filling in the `Continuation`. +- The Continuation will store the captured synchronization context. This can be done by calling `AsyncHelpers.CaptureContinuationContext(ref newContinuation.ContinuationContext, ref newContinuation.Flags)` while filling in the `Continuation`. If set to ContinuationContextHandling::ContinueOnThreadPool - The Continuation shall have the CORINFO_CONTINUATION_CONTINUE_ON_THREAD_POOL flag set # Exception handling behavior -If an async function is called within a try block (In the jit hasTryIndex return true), set the CORINFO\_CONTINUATION\_HAS\_EXCEPTION bit on the Continuation and make it large enough. +If an async function is called within a try block (In the jit hasTryIndex return true), allocate space for an exception in the continuation and encode its offset in the flags based on the scheme above. # Locals handling diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs index b74b5ddef1eaa3..3b6c123504e869 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs @@ -28,17 +28,20 @@ internal enum ContinuationFlags // Otherwise the exact offset of the member is computed as // DataOffset + (index - 1) * PointerSize // - ExceptionIndexFirstBit = 3, - ExceptionIndexNumBits = 2, + ExecutionContextIndexFirstBit = 3, + ExecutionContextIndexNumBits = 2, ContinuationContextIndexFirstBit = 5, ContinuationContextIndexNumBits = 2, + ExceptionIndexFirstBit = 7, + ExceptionIndexNumBits = 3, + // For JIT, the continuation stores space for every possible type of // async callee's result. We need to represent the offset to each of // these, so we allocate the rest of the bits for this. - ResultIndexFirstBit = 7, - ResultIndexNumBits = 25, + ResultIndexFirstBit = 10, + ResultIndexNumBits = 22, } // Keep in sync with CORINFO_AsyncResumeInfo in corinfo.h @@ -92,6 +95,16 @@ public bool HasException() return ((uint)Flags & (mask << (int)ContinuationFlags.ExceptionIndexFirstBit)) != 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe ExecutionContext? GetExecutionContext() + { + const uint mask = (1u << (int)ContinuationFlags.ExecutionContextIndexNumBits) - 1; + uint index = ((uint)Flags >> (int)ContinuationFlags.ExecutionContextIndexFirstBit) & mask; + Debug.Assert(index != 0); + ref byte data = ref RuntimeHelpers.GetRawData(this); + return Unsafe.As(ref Unsafe.Add(ref data, (DataOffset - PointerSize) + index * PointerSize)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetException(Exception ex) { @@ -234,7 +247,7 @@ private unsafe struct RuntimeAsyncAwaitState public Continuation? SentinelContinuation; // We cache the thread here to avoid unnecessary repeated TLS lookups. - public Thread CurrentThread; + public Thread? CurrentThread; public RuntimeAsyncStackState* StackState; @@ -243,7 +256,7 @@ public void CaptureContexts() // CaptureContext is called from leaf await helpers. We either just started a runtime async chain // (from a thunk), or we came from DispatchContinuations (on resumption). // Both cases have already initialized CurrentThread. - Thread curThread = CurrentThread; + Thread? curThread = CurrentThread; Debug.Assert(curThread != null); Debug.Assert(StackState != null); // Here we get the execution context for presenting to the notifier, @@ -401,7 +414,7 @@ private void SetContinuationState(Continuation value) internal unsafe bool HandleSuspended(ref RuntimeAsyncAwaitState state) { - Thread currentThread = state.CurrentThread; + Thread? currentThread = state.CurrentThread; Debug.Assert(currentThread != null); RuntimeAsyncStackState* stackState = state.StackState; @@ -541,7 +554,10 @@ private unsafe void DispatchContinuations() } } - RuntimeAsyncStackState stackState = default; + // Intentionally skip initialization for this state; the Push + // call below will initialize non-GC refs, and GC refs will be + // zeroed by prolog. + RuntimeAsyncStackState stackState; ref RuntimeAsyncAwaitState awaitState = ref t_runtimeAsyncAwaitState; awaitState.Push(&stackState); @@ -562,6 +578,8 @@ private unsafe void DispatchContinuations() Continuation? nextContinuation = curContinuation.Next; asyncDispatcherInfo.NextContinuation = nextContinuation; + Debug.Assert(awaitState.CurrentThread != null); + RestoreExecutionContext(awaitState.CurrentThread, curContinuation.GetExecutionContext()); ref byte resultLoc = ref nextContinuation != null ? ref nextContinuation.GetResultStorageOrNull() : ref GetResultStorage(); Continuation? newContinuation = curContinuation.ResumeInfo->Resume(curContinuation, ref resultLoc); @@ -640,7 +658,10 @@ private unsafe void DispatchContinuations() [StackTraceHidden] private unsafe void InstrumentedDispatchContinuations(AsyncInstrumentation.Flags flags) { - RuntimeAsyncStackState stackState = default; + // Intentionally skip initialization for this state; the Push + // call below will initialize non-GC refs, and GC refs will be + // zeroed by prolog. + RuntimeAsyncStackState stackState; ref RuntimeAsyncAwaitState awaitState = ref t_runtimeAsyncAwaitState; awaitState.Push(&stackState); @@ -663,6 +684,8 @@ private unsafe void InstrumentedDispatchContinuations(AsyncInstrumentation.Flags Continuation? nextContinuation = curContinuation.Next; asyncDispatcherInfo.NextContinuation = nextContinuation; + Debug.Assert(awaitState.CurrentThread != null); + RestoreExecutionContext(awaitState.CurrentThread, curContinuation.GetExecutionContext()); ref byte resultLoc = ref nextContinuation != null ? ref nextContinuation.GetResultStorageOrNull() : ref GetResultStorage(); RuntimeAsyncInstrumentationHelpers.ResumeRuntimeAsyncMethod(ref asyncDispatcherInfo, flags, curContinuation); @@ -954,16 +977,10 @@ private static ValueTask ValueTaskFromException(Exception ex) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void RestoreExecutionContext(ExecutionContext? previousExecCtx) + private static void RestoreExecutionContext(Thread thread, ExecutionContext? previousExecCtx) { - if (previousExecCtx == ExecutionContext.DefaultFlowSuppressed) - { - return; - } - - Thread thread = Thread.CurrentThreadAssumedInitialized; ExecutionContext? currentExecCtx = thread._executionContext; - if (previousExecCtx != currentExecCtx) + if (previousExecCtx != currentExecCtx && previousExecCtx != ExecutionContext.DefaultFlowSuppressed) { ExecutionContext.RestoreChangedContextToThread(thread, previousExecCtx, currentExecCtx); } diff --git a/src/coreclr/inc/corinfo.h b/src/coreclr/inc/corinfo.h index a7d1726a7614b4..5aa999e2745fa8 100644 --- a/src/coreclr/inc/corinfo.h +++ b/src/coreclr/inc/corinfo.h @@ -1803,18 +1803,21 @@ enum CorInfoContinuationFlags // If the encoded index is 0, it means no such member is present. // Otherwise the exact offset of the member is computed as // OFFSETOF__CORINFO_Continuation__data + (index - 1) * PointerSize - // - CORINFO_CONTINUATION_EXCEPTION_INDEX_FIRST_BIT = 3, - CORINFO_CONTINUATION_EXCEPTION_INDEX_NUM_BITS = 2, + + CORINFO_CONTINUATION_EXECUTION_CONTEXT_INDEX_FIRST_BIT = 3, + CORINFO_CONTINUATION_EXECUTION_CONTEXT_INDEX_NUM_BITS = 2, CORINFO_CONTINUATION_CONTEXT_INDEX_FIRST_BIT = 5, CORINFO_CONTINUATION_CONTEXT_INDEX_NUM_BITS = 2, + CORINFO_CONTINUATION_EXCEPTION_INDEX_FIRST_BIT = 7, + CORINFO_CONTINUATION_EXCEPTION_INDEX_NUM_BITS = 3, + // For JIT, the continuation stores space for every possible type of // async callee's result. We need to represent the offset to each of // these, so we allocate the rest of the bits for this. - CORINFO_CONTINUATION_RESULT_INDEX_FIRST_BIT = 7, - CORINFO_CONTINUATION_RESULT_INDEX_NUM_BITS = 25, + CORINFO_CONTINUATION_RESULT_INDEX_FIRST_BIT = 10, + CORINFO_CONTINUATION_RESULT_INDEX_NUM_BITS = 22, }; struct CORINFO_ASYNC_INFO @@ -1831,8 +1834,6 @@ struct CORINFO_ASYNC_INFO CORINFO_FIELD_HANDLE continuationFlagsFldHnd; // Method handle for AsyncHelpers.CaptureExecutionContext, used during suspension CORINFO_METHOD_HANDLE captureExecutionContextMethHnd; - // Method handle for AsyncHelpers.RestoreExecutionContext, used during resumption - CORINFO_METHOD_HANDLE restoreExecutionContextMethHnd; // Method handle for AsyncHelpers.CaptureContinuationContext, used during suspension CORINFO_METHOD_HANDLE captureContinuationContextMethHnd; // Method handle for AsyncHelpers.CaptureContexts, used at the beginning of async methods diff --git a/src/coreclr/inc/jiteeversionguid.h b/src/coreclr/inc/jiteeversionguid.h index c18e0ddca37998..fe5749d03fdda1 100644 --- a/src/coreclr/inc/jiteeversionguid.h +++ b/src/coreclr/inc/jiteeversionguid.h @@ -37,11 +37,11 @@ #include -constexpr GUID JITEEVersionIdentifier = { /* 1516acb8-ac41-4dcb-9840-f39ee25ffa73 */ - 0x1516acb8, - 0xac41, - 0x4dcb, - {0x98, 0x40, 0xf3, 0x9e, 0xe2, 0x5f, 0xfa, 0x73} +constexpr GUID JITEEVersionIdentifier = { /* e92fbf65-4856-4729-ab9e-f66f7adcecf9 */ + 0xe92fbf65, + 0x4856, + 0x4729, + {0xab, 0x9e, 0xf6, 0x6f, 0x7a, 0xdc, 0xec, 0xf9} }; #endif // JIT_EE_VERSIONING_GUID_H diff --git a/src/coreclr/interpreter/compiler.cpp b/src/coreclr/interpreter/compiler.cpp index 6f10b84a6ea6f2..bb5ee520be236c 100644 --- a/src/coreclr/interpreter/compiler.cpp +++ b/src/coreclr/interpreter/compiler.cpp @@ -5879,21 +5879,19 @@ void InterpCompiler::EmitSuspend(const CORINFO_CALL_INFO &callInfo, Continuation flags |= index << firstBit; }; - for (int32_t i = -3; i < liveVars.GetSize(); i++) + for (int32_t i = -4; i < liveVars.GetSize(); i++) { int32_t var; - if (i == -3) + if (i == -4) { - if (!needsEHHandling) - continue; - INTERP_DUMP("Allocate EH at offset %d\n", currentOffset); + INTERP_DUMP("Allocate ExecutionContext at offset %d\n", currentOffset); SetSlotToTrue(objRefSlots, currentOffset); - encodeIndex(currentOffset, CORINFO_CONTINUATION_EXCEPTION_INDEX_FIRST_BIT, CORINFO_CONTINUATION_EXCEPTION_INDEX_NUM_BITS); + encodeIndex(currentOffset, CORINFO_CONTINUATION_EXECUTION_CONTEXT_INDEX_FIRST_BIT, CORINFO_CONTINUATION_EXECUTION_CONTEXT_INDEX_NUM_BITS); currentOffset += sizeof(void*); // Align to pointer size to match the expected layout continue; } - if (i == -2) + if (i == -3) { if (!captureContinuationContext) continue; @@ -5903,6 +5901,16 @@ void InterpCompiler::EmitSuspend(const CORINFO_CALL_INFO &callInfo, Continuation currentOffset += sizeof(void*); // Align to pointer size to match the expected layout continue; } + if (i == -2) + { + if (!needsEHHandling) + continue; + INTERP_DUMP("Allocate EH at offset %d\n", currentOffset); + SetSlotToTrue(objRefSlots, currentOffset); + encodeIndex(currentOffset, CORINFO_CONTINUATION_EXCEPTION_INDEX_FIRST_BIT, CORINFO_CONTINUATION_EXCEPTION_INDEX_NUM_BITS); + currentOffset += sizeof(void*); // Align to pointer size to match the expected layout + continue; + } if (i == -1) { returnValueDataStartOffset = currentOffset; @@ -5955,15 +5963,6 @@ void InterpCompiler::EmitSuspend(const CORINFO_CALL_INFO &callInfo, Continuation currentOffset += size; } - int32_t execContextOffset = 0; - { - // Mark ExecContext pointer as a GC reference - execContextOffset = currentOffset; - INTERP_DUMP("Allocate ExecutableContext at offset %d\n", currentOffset); - SetSlotToTrue(objRefSlots, currentOffset); - currentOffset += sizeof(void*); - } - int32_t keepAliveOffset = 0; if (needsKeepAlive) { @@ -5974,8 +5973,14 @@ void InterpCompiler::EmitSuspend(const CORINFO_CALL_INFO &callInfo, Continuation currentOffset += sizeof(void*); } + // Tail of the data may not have had any object refs to grow objRefSlots, finish growing it now + int32_t numSlotsExpected = currentOffset / sizeof(void*); + if (objRefSlots.GetSize() < numSlotsExpected) + { + objRefSlots.GrowBy(numSlotsExpected - objRefSlots.GetSize()); + } + // Step 5: Get continuation type handle - assert((int32_t)(currentOffset / sizeof(void*)) <= objRefSlots.GetSize()); CORINFO_CLASS_HANDLE continuationTypeHnd = m_compHnd->getContinuationType( currentOffset, objRefSlots.GetUnderlyingArray(), @@ -6006,10 +6011,8 @@ void InterpCompiler::EmitSuspend(const CORINFO_CALL_INFO &callInfo, Continuation suspendData->flags = (CorInfoContinuationFlags)flags; - suspendData->offsetIntoContinuationTypeForExecutionContext = execContextOffset + OFFSETOF__CORINFO_Continuation__data; suspendData->keepAliveOffset = keepAliveOffset + OFFSETOF__CORINFO_Continuation__data; suspendData->captureSyncContextMethod = asyncInfo.captureContinuationContextMethHnd; - suspendData->restoreExecutionContextMethod = asyncInfo.restoreExecutionContextMethHnd; suspendData->restoreContextsOnSuspensionMethod = asyncInfo.restoreContextsOnSuspensionMethHnd; suspendData->resumeInfo.Resume = (size_t)m_asyncResumeFuncPtr; suspendData->resumeInfo.DiagnosticIP = (size_t)NULL; @@ -11503,7 +11506,6 @@ static void DumpInterpAsyncSuspendData(InterpAsyncSuspendData* pSuspendInfo) printf(" AsyncSuspendData["); printf("continuationTypeHnd=%p", pSuspendInfo->continuationTypeHnd); printf(", flags=%d", pSuspendInfo->flags); - printf(", offsetIntoContinuationTypeForExecutionContext=%d", pSuspendInfo->offsetIntoContinuationTypeForExecutionContext); printf(", liveLocalsIntervals="); PrintLocalIntervals(pSuspendInfo->liveLocalsIntervals); printf(", zeroedLocalsIntervals="); diff --git a/src/coreclr/interpreter/inc/interpretershared.h b/src/coreclr/interpreter/inc/interpretershared.h index c696140b97b073..24b081e73ba7cc 100644 --- a/src/coreclr/interpreter/inc/interpretershared.h +++ b/src/coreclr/interpreter/inc/interpretershared.h @@ -208,7 +208,6 @@ struct InterpAsyncSuspendData InterpIntervalMapEntry* zeroedLocalsIntervals; // This will be used for the locals we need to keep live. InterpIntervalMapEntry* liveLocalsIntervals; // Following the end of this struct is the array of InterpIntervalMapEntry for live locals CorInfoContinuationFlags flags; - int32_t offsetIntoContinuationTypeForExecutionContext; int32_t keepAliveOffset; // Only needed if we have a generic context to keep alive InterpByteCodeStart* methodStartIP; COMPILER_SHARED_TYPE(CORINFO_CLASS_HANDLE, DPTR(MethodTable), asyncMethodReturnType); @@ -216,7 +215,6 @@ struct InterpAsyncSuspendData int32_t continuationArgOffset; COMPILER_SHARED_TYPE(CORINFO_METHOD_HANDLE, DPTR(MethodDesc), captureSyncContextMethod); - COMPILER_SHARED_TYPE(CORINFO_METHOD_HANDLE, DPTR(MethodDesc), restoreExecutionContextMethod); COMPILER_SHARED_TYPE(CORINFO_METHOD_HANDLE, DPTR(MethodDesc), restoreContextsOnSuspensionMethod); }; diff --git a/src/coreclr/jit/async.cpp b/src/coreclr/jit/async.cpp index dc5e45ffa8d43e..cd4bb35514149e 100644 --- a/src/coreclr/jit/async.cpp +++ b/src/coreclr/jit/async.cpp @@ -1264,9 +1264,9 @@ void ContinuationLayout::Dump(int indent) printf("%*s +%03u OSR address\n", indent, "", OSRAddressOffset); } - if (ExceptionOffset != UINT_MAX) + if (ExecutionContextOffset != UINT_MAX) { - printf("%*s +%03u Exception\n", indent, "", ExceptionOffset); + printf("%*s +%03u Execution context\n", indent, "", ExecutionContextOffset); } if (ContinuationContextOffset != UINT_MAX) @@ -1274,14 +1274,14 @@ void ContinuationLayout::Dump(int indent) printf("%*s +%03u Continuation context\n", indent, "", ContinuationContextOffset); } - if (KeepAliveOffset != UINT_MAX) + if (ExceptionOffset != UINT_MAX) { - printf("%*s +%03u Keep alive object\n", indent, "", KeepAliveOffset); + printf("%*s +%03u Exception\n", indent, "", ExceptionOffset); } - if (ExecutionContextOffset != UINT_MAX) + if (KeepAliveOffset != UINT_MAX) { - printf("%*s +%03u Execution context\n", indent, "", ExecutionContextOffset); + printf("%*s +%03u Keep alive object\n", indent, "", KeepAliveOffset); } for (const LiveLocalInfo& inf : Locals) @@ -1428,9 +1428,9 @@ ContinuationLayout* ContinuationLayoutBuilder::Create() layout->OSRAddressOffset = allocLayout(TARGET_POINTER_SIZE, TARGET_POINTER_SIZE); } - if (m_needsException) + if (m_needsExecutionContext) { - layout->ExceptionOffset = allocLayout(TARGET_POINTER_SIZE, TARGET_POINTER_SIZE); + layout->ExecutionContextOffset = allocLayout(TARGET_POINTER_SIZE, TARGET_POINTER_SIZE); } if (m_needsContinuationContext) @@ -1438,6 +1438,11 @@ ContinuationLayout* ContinuationLayoutBuilder::Create() layout->ContinuationContextOffset = allocLayout(TARGET_POINTER_SIZE, TARGET_POINTER_SIZE); } + if (m_needsException) + { + layout->ExceptionOffset = allocLayout(TARGET_POINTER_SIZE, TARGET_POINTER_SIZE); + } + // Now allocate all returns for (ReturnInfo& ret : layout->Returns) { @@ -1451,11 +1456,6 @@ ContinuationLayout* ContinuationLayoutBuilder::Create() layout->KeepAliveOffset = allocLayout(TARGET_POINTER_SIZE, TARGET_POINTER_SIZE); } - if (m_needsExecutionContext) - { - layout->ExecutionContextOffset = allocLayout(TARGET_POINTER_SIZE, TARGET_POINTER_SIZE); - } - // Then all locals for (LiveLocalInfo& inf : layout->Locals) { @@ -1471,10 +1471,10 @@ ContinuationLayout* ContinuationLayoutBuilder::Create() : new (m_compiler, CMK_Async) bool[layout->Size / TARGET_POINTER_SIZE]{}; GCPointerBitMapBuilder bitmapBuilder(objRefs, layout->Size); - bitmapBuilder.SetIfNotMax(layout->ExceptionOffset); + bitmapBuilder.SetIfNotMax(layout->ExecutionContextOffset); bitmapBuilder.SetIfNotMax(layout->ContinuationContextOffset); + bitmapBuilder.SetIfNotMax(layout->ExceptionOffset); bitmapBuilder.SetIfNotMax(layout->KeepAliveOffset); - bitmapBuilder.SetIfNotMax(layout->ExecutionContextOffset); for (LiveLocalInfo& inf : layout->Locals) { @@ -1814,12 +1814,24 @@ void AsyncTransformation::CreateSuspension(BasicBlock* call continuationFlags |= index << firstBit; }; - if (subLayout.NeedsException()) - encodeIndex(layout.ExceptionOffset, CORINFO_CONTINUATION_EXCEPTION_INDEX_FIRST_BIT, - CORINFO_CONTINUATION_EXCEPTION_INDEX_NUM_BITS); + if (subLayout.NeedsExecutionContext()) + { + encodeIndex(layout.ExecutionContextOffset, CORINFO_CONTINUATION_EXECUTION_CONTEXT_INDEX_FIRST_BIT, + CORINFO_CONTINUATION_EXECUTION_CONTEXT_INDEX_NUM_BITS); + } + if (subLayout.NeedsContinuationContext()) + { encodeIndex(layout.ContinuationContextOffset, CORINFO_CONTINUATION_CONTEXT_INDEX_FIRST_BIT, CORINFO_CONTINUATION_CONTEXT_INDEX_NUM_BITS); + } + + if (subLayout.NeedsException()) + { + encodeIndex(layout.ExceptionOffset, CORINFO_CONTINUATION_EXCEPTION_INDEX_FIRST_BIT, + CORINFO_CONTINUATION_EXCEPTION_INDEX_NUM_BITS); + } + if (call->gtReturnType != TYP_VOID) { const ReturnInfo* returnInfo = layout.FindReturn(m_compiler, call); @@ -1827,8 +1839,11 @@ void AsyncTransformation::CreateSuspension(BasicBlock* call encodeIndex(returnInfo->Offset, CORINFO_CONTINUATION_RESULT_INDEX_FIRST_BIT, CORINFO_CONTINUATION_RESULT_INDEX_NUM_BITS); } + if (callInfo.ContinuationContextHandling == ContinuationContextHandling::ContinueOnThreadPool) + { continuationFlags |= CORINFO_CONTINUATION_CONTINUE_ON_THREAD_POOL; + } newContinuation = m_compiler->gtNewLclvNode(newContinuationVar, TYP_REF); unsigned flagsOffset = m_compiler->info.compCompHnd->getFieldOffset(m_asyncInfo->continuationFlagsFldHnd); @@ -2514,33 +2529,6 @@ void AsyncTransformation::RestoreFromDataOnResumption(const ContinuationLayout& const ContinuationLayoutBuilder& subLayout, BasicBlock* resumeBB) { - if (subLayout.NeedsExecutionContext()) - { - GenTree* valuePlaceholder = m_compiler->gtNewZeroConNode(TYP_REF); - GenTreeCall* restoreCall = - m_compiler->gtNewCallNode(CT_USER_FUNC, m_asyncInfo->restoreExecutionContextMethHnd, TYP_VOID); - SetCallEntrypointForR2R(restoreCall, m_compiler, m_asyncInfo->restoreExecutionContextMethHnd); - restoreCall->gtArgs.PushFront(m_compiler, NewCallArg::Primitive(valuePlaceholder)); - - m_compiler->compCurBB = resumeBB; - m_compiler->fgMorphTree(restoreCall); - - LIR::AsRange(resumeBB).InsertAtEnd(LIR::SeqTree(m_compiler, restoreCall)); - - LIR::Use valueUse; - bool gotUse = LIR::AsRange(resumeBB).TryGetUse(valuePlaceholder, &valueUse); - assert(gotUse); - - GenTree* continuation = m_compiler->gtNewLclvNode(m_compiler->lvaAsyncContinuationArg, TYP_REF); - unsigned execContextOffset = OFFSETOF__CORINFO_Continuation__data + layout.ExecutionContextOffset; - GenTree* execContextValue = LoadFromOffset(continuation, execContextOffset, TYP_REF); - - LIR::AsRange(resumeBB).InsertBefore(valuePlaceholder, LIR::SeqTree(m_compiler, execContextValue)); - valueUse.ReplaceWith(execContextValue); - - LIR::AsRange(resumeBB).Remove(valuePlaceholder); - } - // Copy data for (const LiveLocalInfo& inf : layout.Locals) { diff --git a/src/coreclr/jit/fgbasic.cpp b/src/coreclr/jit/fgbasic.cpp index b208a2ab990c96..1f76441347e59e 100644 --- a/src/coreclr/jit/fgbasic.cpp +++ b/src/coreclr/jit/fgbasic.cpp @@ -5252,6 +5252,8 @@ BasicBlock* Compiler::fgRemoveBlock(BasicBlock* block, bool unreachable) for (BasicBlock* const predBlock : block->PredBlocksEditing()) { + // Inherit affordances + predBlock->CopyFlags(block, BBF_ASYNC_RESUMPTION); // change all jumps/refs to the removed block fgReplaceJumpTarget(predBlock, block, succBlock); } diff --git a/src/coreclr/jit/fgopt.cpp b/src/coreclr/jit/fgopt.cpp index 5e1756aae995e6..ffa3d88cba33eb 100644 --- a/src/coreclr/jit/fgopt.cpp +++ b/src/coreclr/jit/fgopt.cpp @@ -1097,6 +1097,9 @@ void Compiler::fgCompactBlock(BasicBlock* block) block->CopyFlags(target, BBF_COMPACT_UPD); + // If target was a resumption block, block now plays that role. + block->CopyFlags(target, BBF_ASYNC_RESUMPTION); + // mark target as removed target->SetFlags(BBF_REMOVED); @@ -1384,6 +1387,9 @@ bool Compiler::fgOptimizeBranchToEmptyUnconditional(BasicBlock* block, BasicBloc unreached(); } + // Inherit some special affordances. + block->CopyFlags(bDest, BBF_ASYNC_RESUMPTION); + // // When we optimize a branch to branch we need to update the profile weight // of bDest by subtracting out the weight of the path that is being optimized. @@ -1647,6 +1653,9 @@ bool Compiler::fgOptimizeSwitchBranches(BasicBlock* block) bDest->decreaseBBProfileWeight(edge->getLikelyWeight()); } + // Inherit affordances + block->CopyFlags(bDest, BBF_ASYNC_RESUMPTION); + // Redirect the jump to the new target // fgReplaceJumpTarget(block, bDest, bNewDest); @@ -4633,6 +4642,9 @@ bool Compiler::fgUpdateFlowGraph(bool doTailDuplication /* = false */, bool isPh std::swap(block->TrueEdgeRef(), block->FalseEdgeRef()); fgRedirectEdge(block->TrueEdgeRef(), bNext->GetTarget()); + // Inherit some affordances + block->CopyFlags(bNext, BBF_ASYNC_RESUMPTION); + // bNext no longer flows to target // fgRemoveRefPred(bNext->GetTargetEdge()); diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs index dcc065e8303292..6c44f4e6ebfdbc 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs @@ -3483,7 +3483,6 @@ private void getAsyncInfo(ref CORINFO_ASYNC_INFO pAsyncInfoOut) pAsyncInfoOut.continuationFlagsFldHnd = ObjectToHandle(continuation.GetKnownField("Flags"u8)); DefType asyncHelpers = _compilation.TypeSystemContext.SystemModule.GetKnownType("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8); pAsyncInfoOut.captureExecutionContextMethHnd = ObjectToHandle(asyncHelpers.GetKnownMethod("CaptureExecutionContext"u8, null)); - pAsyncInfoOut.restoreExecutionContextMethHnd = ObjectToHandle(asyncHelpers.GetKnownMethod("RestoreExecutionContext"u8, null)); pAsyncInfoOut.captureContinuationContextMethHnd = ObjectToHandle(asyncHelpers.GetKnownMethod("CaptureContinuationContext"u8, null)); pAsyncInfoOut.captureContextsMethHnd = ObjectToHandle(asyncHelpers.GetKnownMethod("CaptureContexts"u8, null)); pAsyncInfoOut.restoreContextsMethHnd = ObjectToHandle(asyncHelpers.GetKnownMethod("RestoreContexts"u8, null)); diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs b/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs index a921abd0406ab1..8bba89b5e829fb 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs @@ -971,8 +971,6 @@ public unsafe struct CORINFO_ASYNC_INFO public CORINFO_FIELD_STRUCT_* continuationFlagsFldHnd; // Method handle for AsyncHelpers.CaptureExecutionContext public CORINFO_METHOD_STRUCT_* captureExecutionContextMethHnd; - // Method handle for AsyncHelpers.RestoreExecutionContext - public CORINFO_METHOD_STRUCT_* restoreExecutionContextMethHnd; public CORINFO_METHOD_STRUCT_* captureContinuationContextMethHnd; public CORINFO_METHOD_STRUCT_* captureContextsMethHnd; public CORINFO_METHOD_STRUCT_* restoreContextsMethHnd; diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs index a22040709eca7d..5fb5421e58cc7e 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs @@ -469,7 +469,6 @@ private void ImportCall(ILOpcode opcode, int token) _dependencies.Add(_compilation.GetHelperEntrypoint(ReadyToRunHelper.AllocContinuation), asyncReason); _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("CaptureExecutionContext"u8, null)), asyncReason); - _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("RestoreExecutionContext"u8, null)), asyncReason); _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("CaptureContinuationContext"u8, null)), asyncReason); _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("RestoreContextsOnSuspension"u8, null)), asyncReason); _dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("FinishSuspensionNoContinuationContext"u8, null)), asyncReason); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs index c1b858a839db05..e0376defcbf35b 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs @@ -1022,7 +1022,6 @@ private void AddNecessaryAsyncReferences(MethodDesc method) [ // For CorInfoImpl.getAsyncInfo asyncHelpers.GetKnownMethod("CaptureExecutionContext"u8, null), - asyncHelpers.GetKnownMethod("RestoreExecutionContext"u8, null), asyncHelpers.GetKnownMethod("CaptureContinuationContext"u8, null), asyncHelpers.GetKnownMethod("CaptureContexts"u8, null), asyncHelpers.GetKnownMethod("RestoreContexts"u8, null), diff --git a/src/coreclr/tools/superpmi/superpmi-shared/agnostic.h b/src/coreclr/tools/superpmi/superpmi-shared/agnostic.h index f929bb75035cf7..91b26c9caaa256 100644 --- a/src/coreclr/tools/superpmi/superpmi-shared/agnostic.h +++ b/src/coreclr/tools/superpmi/superpmi-shared/agnostic.h @@ -201,7 +201,6 @@ struct Agnostic_CORINFO_ASYNC_INFO DWORDLONG continuationStateFldHnd; DWORDLONG continuationFlagsFldHnd; DWORDLONG captureExecutionContextMethHnd; - DWORDLONG restoreExecutionContextMethHnd; DWORDLONG captureContinuationContextMethHnd; DWORDLONG captureContextsMethHnd; DWORDLONG restoreContextsMethHnd; diff --git a/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp b/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp index 605cdfce3c4758..64775fb58cfe52 100644 --- a/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp +++ b/src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp @@ -4490,7 +4490,6 @@ void MethodContext::recGetAsyncInfo(const CORINFO_ASYNC_INFO* pAsyncInfo) value.continuationStateFldHnd = CastHandle(pAsyncInfo->continuationStateFldHnd); value.continuationFlagsFldHnd = CastHandle(pAsyncInfo->continuationFlagsFldHnd); value.captureExecutionContextMethHnd = CastHandle(pAsyncInfo->captureExecutionContextMethHnd); - value.restoreExecutionContextMethHnd = CastHandle(pAsyncInfo->restoreExecutionContextMethHnd); value.captureContinuationContextMethHnd = CastHandle(pAsyncInfo->captureContinuationContextMethHnd); value.captureContextsMethHnd = CastHandle(pAsyncInfo->captureContextsMethHnd); value.restoreContextsMethHnd = CastHandle(pAsyncInfo->restoreContextsMethHnd); @@ -4517,7 +4516,6 @@ void MethodContext::repGetAsyncInfo(CORINFO_ASYNC_INFO* pAsyncInfoOut) pAsyncInfoOut->continuationStateFldHnd = (CORINFO_FIELD_HANDLE)value.continuationStateFldHnd; pAsyncInfoOut->continuationFlagsFldHnd = (CORINFO_FIELD_HANDLE)value.continuationFlagsFldHnd; pAsyncInfoOut->captureExecutionContextMethHnd = (CORINFO_METHOD_HANDLE)value.captureExecutionContextMethHnd; - pAsyncInfoOut->restoreExecutionContextMethHnd = (CORINFO_METHOD_HANDLE)value.restoreExecutionContextMethHnd; pAsyncInfoOut->captureContinuationContextMethHnd = (CORINFO_METHOD_HANDLE)value.captureContinuationContextMethHnd; pAsyncInfoOut->captureContextsMethHnd = (CORINFO_METHOD_HANDLE)value.captureContextsMethHnd; pAsyncInfoOut->restoreContextsMethHnd = (CORINFO_METHOD_HANDLE)value.restoreContextsMethHnd; diff --git a/src/coreclr/vm/corelib.h b/src/coreclr/vm/corelib.h index 512320b3ca99b2..485792072ae4d5 100644 --- a/src/coreclr/vm/corelib.h +++ b/src/coreclr/vm/corelib.h @@ -721,7 +721,6 @@ DEFINE_METHOD(ASYNC_HELPERS, TRANSPARENT_AWAIT, TransparentAwait, N DEFINE_METHOD(ASYNC_HELPERS, COMPLETED_TASK_RESULT, CompletedTaskResult, NoSig) DEFINE_METHOD(ASYNC_HELPERS, COMPLETED_TASK, CompletedTask, NoSig) DEFINE_METHOD(ASYNC_HELPERS, CAPTURE_EXECUTION_CONTEXT, CaptureExecutionContext, NoSig) -DEFINE_METHOD(ASYNC_HELPERS, RESTORE_EXECUTION_CONTEXT, RestoreExecutionContext, NoSig) DEFINE_METHOD(ASYNC_HELPERS, CAPTURE_CONTINUATION_CONTEXT, CaptureContinuationContext, NoSig) DEFINE_METHOD(ASYNC_HELPERS, CAPTURE_CONTEXTS, CaptureContexts, NoSig) DEFINE_METHOD(ASYNC_HELPERS, RESTORE_CONTEXTS, RestoreContexts, NoSig) diff --git a/src/coreclr/vm/interpexec.cpp b/src/coreclr/vm/interpexec.cpp index e5d99b4c501b04..e3bd37659a2d57 100644 --- a/src/coreclr/vm/interpexec.cpp +++ b/src/coreclr/vm/interpexec.cpp @@ -4399,11 +4399,14 @@ do \ ); } continuation = LOCAL_VAR(ip[2], CONTINUATIONREF); - SetObjectReference((OBJECTREF *)((uint8_t*)(OBJECTREFToObject(continuation)) + pAsyncSuspendData->offsetIntoContinuationTypeForExecutionContext), executionContext); continuation->SetFlags(pAsyncSuspendData->flags); + PTR_OBJECTREF pExecutionContext = continuation->GetExecutionContextObjectStorageOrNull(); + _ASSERTE(pExecutionContext != NULL); + SetObjectReference(pExecutionContext, executionContext); + PTR_OBJECTREF pContinuationContext = continuation->GetContinuationContextObjectStorageOrNull(); - if (pContinuationContext != nullptr) + if (pContinuationContext != NULL) { MethodDesc *captureSyncContextMethod = pAsyncSuspendData->captureSyncContextMethod; int32_t *flagsAddress = continuation->GetFlagsAddress(); @@ -4557,9 +4560,6 @@ do \ CONTINUATIONREF continuation = (CONTINUATIONREF)ObjectToOBJECTREF(*(Object**)(stack + pAsyncSuspendData->continuationArgOffset)); _ASSERTE(pInterpreterFrame->GetContinuation() == NULL); - // The INTOP_CHECK_FOR_CONTINUATION opcode will have called to restore the execution context already - // Now copy the locals - // copy locals that need to move from the continuation object uint8_t *pContinuationData = continuation->GetResultStorage(); InterpIntervalMapEntry *pCopyEntry = pAsyncSuspendData->liveLocalsIntervals; @@ -4602,18 +4602,7 @@ do \ // And before it should be an INTOP_HANDLE_CONTINUATION_SUSPEND opcode _ASSERTE(*ip == INTOP_HANDLE_CONTINUATION_RESUME); _ASSERTE(*(ip-3) == INTOP_HANDLE_CONTINUATION_SUSPEND); - InterpAsyncSuspendData *pAsyncSuspendData = (InterpAsyncSuspendData*)pMethod->pDataItems[ip[1]]; - - returnOffset = pMethod->allocaSize; - callArgsOffset = pMethod->allocaSize; - - OBJECTREF execContext = ObjectToOBJECTREF(*(Object**)(((uint8_t*)OBJECTREFToObject(continuation)) + pAsyncSuspendData->offsetIntoContinuationTypeForExecutionContext)); - - // We need to call the RestoreExecutionContext helper method - LOCAL_VAR(callArgsOffset, OBJECTREF) = execContext; - - targetMethod = pAsyncSuspendData->restoreExecutionContextMethod; - goto CALL_INTERP_METHOD; + break; } ip += 3; break; diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index ebf0533da70904..d06b57ed47c5b8 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -10379,7 +10379,6 @@ void CEEInfo::getAsyncInfo(CORINFO_ASYNC_INFO* pAsyncInfoOut) pAsyncInfoOut->continuationStateFldHnd = CORINFO_FIELD_HANDLE(CoreLibBinder::GetField(FIELD__CONTINUATION__STATE)); pAsyncInfoOut->continuationFlagsFldHnd = CORINFO_FIELD_HANDLE(CoreLibBinder::GetField(FIELD__CONTINUATION__FLAGS)); pAsyncInfoOut->captureExecutionContextMethHnd = CORINFO_METHOD_HANDLE(CoreLibBinder::GetMethod(METHOD__ASYNC_HELPERS__CAPTURE_EXECUTION_CONTEXT)); - pAsyncInfoOut->restoreExecutionContextMethHnd = CORINFO_METHOD_HANDLE(CoreLibBinder::GetMethod(METHOD__ASYNC_HELPERS__RESTORE_EXECUTION_CONTEXT)); pAsyncInfoOut->captureContinuationContextMethHnd = CORINFO_METHOD_HANDLE(CoreLibBinder::GetMethod(METHOD__ASYNC_HELPERS__CAPTURE_CONTINUATION_CONTEXT)); pAsyncInfoOut->captureContextsMethHnd = CORINFO_METHOD_HANDLE(CoreLibBinder::GetMethod(METHOD__ASYNC_HELPERS__CAPTURE_CONTEXTS)); pAsyncInfoOut->restoreContextsMethHnd = CORINFO_METHOD_HANDLE(CoreLibBinder::GetMethod(METHOD__ASYNC_HELPERS__RESTORE_CONTEXTS)); diff --git a/src/coreclr/vm/object.h b/src/coreclr/vm/object.h index e0efadc3d9dd96..9060454fc2deaf 100644 --- a/src/coreclr/vm/object.h +++ b/src/coreclr/vm/object.h @@ -2168,6 +2168,20 @@ class ContinuationObject : public Object return dac_cast(dataAddress); } + PTR_OBJECTREF GetExecutionContextObjectStorageOrNull() + { + LIMITED_METHOD_CONTRACT; + + uint32_t mask = (1u << CORINFO_CONTINUATION_EXECUTION_CONTEXT_INDEX_NUM_BITS) - 1; + uint32_t index = ((uint32_t)Flags >> CORINFO_CONTINUATION_EXECUTION_CONTEXT_INDEX_FIRST_BIT) & mask; + if (index == 0) + return NULL; + + uint32_t offset = OFFSETOF__CORINFO_Continuation__data + (index - 1) * TARGET_POINTER_SIZE; + PTR_BYTE address = dac_cast(dac_cast(this) + offset); + return dac_cast(address); + } + PTR_OBJECTREF GetContinuationContextObjectStorageOrNull() { LIMITED_METHOD_CONTRACT; From 7a98c250ca1f5d86cfbaff5e4f0347e1847e613a Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Mon, 4 May 2026 13:06:42 +0200 Subject: [PATCH 086/115] JIT: Use `VisitLocalDefNodes` from `gtTreeHasLocalStore` (#127555) `gtTreeHasLocalStore` did not properly handle all definitions, skipping for example retbuf definitions. We have a low-level helper for this so use that. --- src/coreclr/jit/gentree.cpp | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/coreclr/jit/gentree.cpp b/src/coreclr/jit/gentree.cpp index 1b846460e89c8b..f9365400bd7f09 100644 --- a/src/coreclr/jit/gentree.cpp +++ b/src/coreclr/jit/gentree.cpp @@ -7890,7 +7890,7 @@ bool Compiler::gtTreeHasLocalRead(GenTree* tree, unsigned lclNum) // lclNum - The local to look for. // // Returns: -// True if there is any GT_STORE_LCL_VAR or GT_STORE_LCL_FLD that affects "lclNum". +// True if there is any definition that affects "lclNum". // bool Compiler::gtTreeHasLocalStore(GenTree* tree, unsigned lclNum) { @@ -7899,8 +7899,7 @@ bool Compiler::gtTreeHasLocalStore(GenTree* tree, unsigned lclNum) public: enum { - DoPreOrder = true, - DoLclVarsOnly = true, + DoPreOrder = true, }; unsigned m_lclNum; @@ -7922,25 +7921,26 @@ bool Compiler::gtTreeHasLocalStore(GenTree* tree, unsigned lclNum) return WALK_SKIP_SUBTREES; } - if (node->OperIsLocalStore()) - { - if (node->AsLclVarCommon()->GetLclNum() == m_lclNum) + auto visit = [&](GenTreeLclVarCommon* lclVar) { + if (lclVar->GetLclNum() == m_lclNum) { - return WALK_ABORT; + return GenTree::VisitResult::Abort; } - - if (m_lclDsc->lvIsStructField && (node->AsLclVarCommon()->GetLclNum() == m_lclDsc->lvParentLcl)) + if (m_lclDsc->lvIsStructField && (lclVar->GetLclNum() == m_lclDsc->lvParentLcl)) { - // Store to parent local also affects the field - return WALK_ABORT; + return GenTree::VisitResult::Abort; } - - if (m_lclDsc->lvPromoted && (node->AsLclVarCommon()->GetLclNum() >= m_lclDsc->lvFieldLclStart) && - (node->AsLclVarCommon()->GetLclNum() < m_lclDsc->lvFieldLclStart + m_lclDsc->lvFieldCnt)) + if (m_lclDsc->lvPromoted && (lclVar->GetLclNum() >= m_lclDsc->lvFieldLclStart) && + (lclVar->GetLclNum() < m_lclDsc->lvFieldLclStart + m_lclDsc->lvFieldCnt)) { - // Store to field also affects the parent - return WALK_ABORT; + return GenTree::VisitResult::Abort; } + return GenTree::VisitResult::Continue; + }; + + if (node->VisitLocalDefNodes(m_compiler, visit) == GenTree::VisitResult::Abort) + { + return WALK_ABORT; } return WALK_CONTINUE; From aa2d3706b716e90228f885bc8827f130c4c0e971 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Mon, 4 May 2026 14:11:38 +0200 Subject: [PATCH 087/115] Fix INT_MIN overflow in RangeCheck::BetweenBounds (#127648) fixes #127639 --------- Co-authored-by: EgorBo Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/rangecheck.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/coreclr/jit/rangecheck.cpp b/src/coreclr/jit/rangecheck.cpp index 85953433c066aa..55a2c5ab7e3545 100644 --- a/src/coreclr/jit/rangecheck.cpp +++ b/src/coreclr/jit/rangecheck.cpp @@ -208,7 +208,10 @@ bool RangeCheck::BetweenBounds(Range& range, GenTree* upper, int arrSize) if (range.LowerLimit().IsBinOpArray()) { int lcns = range.LowerLimit().GetConstant(); - if (lcns >= 0 || -lcns > arrSize) + // Use "lcns < -arrSize" rather than "-lcns > arrSize" to avoid signed + // overflow when lcns == INT_MIN. + assert(arrSize > 0); + if (lcns >= 0 || lcns < -arrSize) { return false; } @@ -236,8 +239,11 @@ bool RangeCheck::BetweenBounds(Range& range, GenTree* upper, int arrSize) if (range.LowerLimit().IsBinOpArray()) { int lcns = range.LowerLimit().GetConstant(); - // len + lcns, make sure we don't subtract too much from len. - if (lcns >= 0 || -lcns > arrSize) + // len + lcns, make sure we don't subtract too much from len. Use + // "lcns < -arrSize" rather than "-lcns > arrSize" to avoid signed + // overflow when lcns == INT_MIN. + assert(arrSize > 0); + if (lcns >= 0 || lcns < -arrSize) { return false; } From 2260be1f5583bed1b0d2dc7ebb97d6b68a128597 Mon Sep 17 00:00:00 2001 From: Ahmed Waleed Date: Mon, 4 May 2026 15:15:52 +0300 Subject: [PATCH 088/115] Fix ImmutableHashSet.SetEquals correctness for mismatched comparers (#127633) ## Summary This PR fixes a correctness issue in `SetEquals` introduced in my previous PR #126309. The `Count` check was moved inside the `Comparer` equality block. This ensures that when comparers differ, we don't return a false negative and instead fall back to the safe path. ### Example of the issue fixed: ```csharp // This should return true, but was returning false var main = ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase, "a"); var other = ImmutableHashSet.Create("a", "A"); Console.WriteLine(main.SetEquals(other)); ``` ## Changes - Moved `if (count != origin.Count)` inside the `EqualityComparer>.Default.Equals` check. - This ensures mismatched comparers safely bypass the fast-path and proceed to a proper set comparison. ## Testing In addition to the fix, I have added comprehensive unit tests covering various scenarios to ensure correctness: - **Mismatched Comparers (Ordinal vs. OrdinalIgnoreCase):** Verified that `SetEquals` returns `true` when logically equal but with different comparers, and `false` when logically different. - **ICollection with Duplicates:** Verified the fallback path correctly handles collections like `List` with duplicate elements. - **Count Optimizations:** - Verified that mismatched comparers correctly bypass the fast-path count check. - Verified that `SetEquals` still performs early-exit when `other.Count < origin.Count`. - **Fast-Path Validation:** Ensured that when comparers match, the optimized count-based comparison still works as expected. - **Edge Cases:** Included tests for empty sets with different comparers and content-specific mismatches. **Related to:** #126309 --- .../Immutable/ImmutableHashSet_1.cs | 29 +++--- .../tests/ImmutableHashSetTest.cs | 90 +++++++++++++++++++ 2 files changed, 105 insertions(+), 14 deletions(-) diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableHashSet_1.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableHashSet_1.cs index 4c9974fadd5b0b..55f7e7e34f2ea6 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableHashSet_1.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableHashSet_1.cs @@ -743,39 +743,40 @@ private static bool SetEquals(IEnumerable other, MutationInput origin) switch (other) { case ImmutableHashSet otherAsImmutableHashSet: - if (otherAsImmutableHashSet.Count != origin.Count) - { - return false; - } - if (EqualityComparer>.Default.Equals(origin.EqualityComparer, otherAsImmutableHashSet.KeyComparer)) { + if (otherAsImmutableHashSet.Count != origin.Count) + { + return false; + } return SetEqualsWithImmutableHashset(otherAsImmutableHashSet, origin); } - break; - case HashSet otherAsHashset: - if (otherAsHashset.Count != origin.Count) + if (otherAsImmutableHashSet.Count < origin.Count) { return false; } + break; + case HashSet otherAsHashset: if (EqualityComparer>.Default.Equals(origin.EqualityComparer, otherAsHashset.Comparer)) { + if (otherAsHashset.Count != origin.Count) + { + return false; + } return SetEqualsWithHashset(otherAsHashset, origin); } - break; - case ICollection otherAsICollectionGeneric: - // We check for < instead of != because other is not guaranteed to be a set, it could be a collection with duplicates. - if (otherAsICollectionGeneric.Count < origin.Count) + if (otherAsHashset.Count < origin.Count) { return false; } break; - case ICollection otherAsICollection: - if (otherAsICollection.Count < origin.Count) + case ICollection otherAsICollectionGeneric: + // We check for < instead of != because other is not guaranteed to be a set, it could be a collection with duplicates. + if (otherAsICollectionGeneric.Count < origin.Count) { return false; } diff --git a/src/libraries/System.Collections.Immutable/tests/ImmutableHashSetTest.cs b/src/libraries/System.Collections.Immutable/tests/ImmutableHashSetTest.cs index d6a01aee099a26..12203d957186b7 100644 --- a/src/libraries/System.Collections.Immutable/tests/ImmutableHashSetTest.cs +++ b/src/libraries/System.Collections.Immutable/tests/ImmutableHashSetTest.cs @@ -31,6 +31,96 @@ public void CustomSort() new[] { "apple" }); } + [Fact] + public void SetEqualsMismatchedComparersOriginInsensitiveOtherSensitive() + { + var ignoreCaseSet = ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase, "a"); + var sensitiveSet = ImmutableHashSet.Create(StringComparer.Ordinal, "a", "A"); + + Assert.True(ignoreCaseSet.SetEquals(sensitiveSet)); + } + + [Fact] + public void SetEqualsMismatchedComparersOriginSensitiveOtherInsensitive() + { + var sensitiveSetMain = ImmutableHashSet.Create(StringComparer.Ordinal, "a"); + var insensitiveMutable = new HashSet(StringComparer.OrdinalIgnoreCase) { "a", "A" }; + + Assert.True(sensitiveSetMain.SetEquals(insensitiveMutable)); + } + + [Fact] + public void SetEqualsICollectionWithDuplicatesValidatesCorrectness() + { + var ignoreCaseSet = ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase, "a"); + var listWithDupes = new List { "a", "a", "a", "a" }; + + Assert.True(ignoreCaseSet.SetEquals(listWithDupes)); + } + + [Fact] + public void SetEqualsDifferentContent() + { + var ignoreCaseSet = ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase, "a"); + var setB = ImmutableHashSet.Create(StringComparer.Ordinal, "b"); + + Assert.False(ignoreCaseSet.SetEquals(setB)); + } + + [Fact] + public void SetEqualsMismatchedComparersOtherCountSmaller() + { + var originTwoElements = ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase, "a", "b"); + var otherOneElement = ImmutableHashSet.Create(StringComparer.Ordinal, "a"); + + Assert.False(originTwoElements.SetEquals(otherOneElement)); + } + + [Fact] + public void SetEqualsMatchedComparersDifferentCounts() + { + var matchedSet1 = ImmutableHashSet.Create(StringComparer.Ordinal, "a", "b"); + var matchedSet2 = ImmutableHashSet.Create(StringComparer.Ordinal, "a"); + + Assert.False(matchedSet1.SetEquals(matchedSet2)); + } + + [Fact] + public void SetEqualsMatchedComparersSameContent() + { + var matchedSet1 = ImmutableHashSet.Create(StringComparer.Ordinal, "a", "b"); + var matchedSet2 = ImmutableHashSet.Create(StringComparer.Ordinal, "a", "b"); + + Assert.True(matchedSet1.SetEquals(matchedSet2)); + } + + [Fact] + public void SetEqualsEmptySetsDifferentComparers() + { + var empty1 = ImmutableHashSet.Empty.WithComparer(StringComparer.Ordinal); + var empty2 = ImmutableHashSet.Empty.WithComparer(StringComparer.OrdinalIgnoreCase); + + Assert.True(empty1.SetEquals(empty2)); + } + + [Fact] + public void SetEqualsMismatchedComparersOriginSensitiveOtherInsensitiveSameCount() + { + var sensitiveSet = ImmutableHashSet.Create(StringComparer.Ordinal, "a", "A"); + var insensitiveSet = ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase, "a", "b"); + + Assert.False(sensitiveSet.SetEquals(insensitiveSet)); + } + + [Fact] + public void SetEqualsMismatchedComparersOtherIsLarger() + { + var origin = ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase, "a"); + var other = ImmutableHashSet.Create(StringComparer.Ordinal, "a", "b"); + + Assert.False(origin.SetEquals(other)); + } + [Fact] public void ChangeUnorderedEqualityComparer() { From f0620eaa0ab4c3786288e976e84eb7da1674c1ac Mon Sep 17 00:00:00 2001 From: Vlad Brezae Date: Mon, 4 May 2026 15:45:11 +0300 Subject: [PATCH 089/115] Fix handling of TypedReference returns in call stubs (#127603) We special case it as consisting of 2 I8s. Fixes `System.Resources.Extensions.BinaryFormat.Tests` crashes when a method returning `TypedReference` gets called from interpreter via JIT. --- src/coreclr/vm/callstubgenerator.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/coreclr/vm/callstubgenerator.cpp b/src/coreclr/vm/callstubgenerator.cpp index fe8d37fb29bceb..1dba38a4b6ac81 100644 --- a/src/coreclr/vm/callstubgenerator.cpp +++ b/src/coreclr/vm/callstubgenerator.cpp @@ -2682,7 +2682,6 @@ CallStubGenerator::ReturnType CallStubGenerator::GetReturnType(ArgIteratorType * case ELEMENT_TYPE_STRING: case ELEMENT_TYPE_PTR: case ELEMENT_TYPE_BYREF: - case ELEMENT_TYPE_TYPEDBYREF: case ELEMENT_TYPE_ARRAY: case ELEMENT_TYPE_SZARRAY: case ELEMENT_TYPE_FNPTR: @@ -2704,6 +2703,15 @@ CallStubGenerator::ReturnType CallStubGenerator::GetReturnType(ArgIteratorType * case ELEMENT_TYPE_VOID: return ReturnTypeVoid; break; + case ELEMENT_TYPE_TYPEDBYREF: +#if defined(UNIX_AMD64_ABI) + return ReturnTypeI8I8; +#elif defined(TARGET_ARM64) || defined(TARGET_RISCV64) || defined(TARGET_LOONGARCH64) + return ReturnType2I8; +#else + _ASSERTE(!"Implement TypedByRef return support for this platform"); + break; +#endif case ELEMENT_TYPE_VALUETYPE: #ifdef TARGET_AMD64 #ifdef TARGET_WINDOWS From 3b810523b4c4314c6f1e4439d68084a862bdb7b1 Mon Sep 17 00:00:00 2001 From: Jan Kotas Date: Mon, 4 May 2026 06:15:46 -0700 Subject: [PATCH 090/115] Hide Environment.CallEntryPoint in unhandled exception message (#127677) The idea in #126222 was to hide CallEntryPoint helper in the unhandled exception stacktraces, but it did not actually happen. --- src/coreclr/vm/excep.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/coreclr/vm/excep.cpp b/src/coreclr/vm/excep.cpp index a841feafc3aca4..5e87f606da0be9 100644 --- a/src/coreclr/vm/excep.cpp +++ b/src/coreclr/vm/excep.cpp @@ -2577,8 +2577,10 @@ void StackTraceInfo::AppendElement(OBJECTREF pThrowable, UINT_PTR currentIP, UIN return; } - if ((pFunc != NULL && pFunc->IsDiagnosticsHidden())) + if (pFunc != NULL && (pFunc->IsDiagnosticsHidden() || pFunc == g_pEnvironmentCallEntryPointMethodDesc)) + { return; + } struct { From ec42692fe2a0d2404151841b25353d8922cb5101 Mon Sep 17 00:00:00 2001 From: Milos Kotlar Date: Mon, 4 May 2026 15:29:39 +0200 Subject: [PATCH 091/115] Improve clarity in mobile scanner workflow issues and PRs (#127568) ## Description The mobile platform failure scanner has been failing with permission-denied errors and produced a mix of PRs, issues, and repeated comments on the same issues across runs. This change makes the workflow simpler and more predictable. Now every mobile failure either becomes a draft PR (when there's a per-test fix like `[SkipOnPlatform]`) or a tracking issue (for product bugs, native crashes, multi-assembly regressions, or infra problems that need an owner). The workflow no longer files comments and no longer ends a run with `noop`. Other changes: - Cap raised to 5 PRs and 3 issues per run. - PRs now only touch test files (`src/libraries/**/tests/**` and matching `.csproj`); anything else fails PR creation. - PR and issue bodies use a fixed structure: Reasoning, Impact on platforms, Errors log, First build it occurred (issues add a Recommended action section). - Removed broken references in the prompt (`pwsh Get-CIStatus.ps1`, `gh search prs`) and dropped `pwsh` from the allowlist. Validated with one workflow_dispatch run on this branch where it produced three issues #127563, #127564, #127565. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/mobile-scan.lock.yml | 95 +++++-------- .github/workflows/mobile-scan.md | 176 ++++++------------------- 2 files changed, 77 insertions(+), 194 deletions(-) diff --git a/.github/workflows/mobile-scan.lock.yml b/.github/workflows/mobile-scan.lock.yml index a71bec9e418889..23d4822e27064b 100644 --- a/.github/workflows/mobile-scan.lock.yml +++ b/.github/workflows/mobile-scan.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"705293dc67f0708eb58d9c6e9843ae3ea36346e9e116286707fe0dcaccfbbda1","compiler_version":"v0.68.1","strict":true,"agent_id":"copilot","agent_model":"claude-sonnet-4.5"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_PAT_0","COPILOT_PAT_1","COPILOT_PAT_2","COPILOT_PAT_3","COPILOT_PAT_4","COPILOT_PAT_5","COPILOT_PAT_6","COPILOT_PAT_7","COPILOT_PAT_8","COPILOT_PAT_9","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9"},{"repo":"actions/upload-artifact","sha":"bbbca2ddaa5d8feaa63e36b76fdaad77386f024f","version":"v7"},{"repo":"github/gh-aw-actions/setup","sha":"2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc","version":"v0.68.1"}]} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"f77456f33b61a0a920cfcf29a2ff065f923c2d75ef2c2d46909816fd0df9776e","compiler_version":"v0.68.1","strict":true,"agent_id":"copilot","agent_model":"claude-sonnet-4.5"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","COPILOT_PAT_0","COPILOT_PAT_1","COPILOT_PAT_2","COPILOT_PAT_3","COPILOT_PAT_4","COPILOT_PAT_5","COPILOT_PAT_6","COPILOT_PAT_7","COPILOT_PAT_8","COPILOT_PAT_9","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9"},{"repo":"actions/upload-artifact","sha":"bbbca2ddaa5d8feaa63e36b76fdaad77386f024f","version":"v7"},{"repo":"github/gh-aw-actions/setup","sha":"2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc","version":"v0.68.1"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -22,7 +22,7 @@ # # For more information: https://github.github.com/gh-aw/introduction/overview/ # -# Daily scan of the runtime-extra-platforms pipeline for Apple mobile and Android failures. Investigates and proposes fixes. +# Daily scan of the runtime-extra-platforms pipeline for Apple mobile and Android failures. Fixes per-test failures via PR; files an actionable tracking issue otherwise. # # Secrets used: # - COPILOT_GITHUB_TOKEN @@ -44,7 +44,6 @@ # Custom actions used: # - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 -# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 # - actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 # - github/gh-aw-actions/setup@2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc # v0.68.1 @@ -198,19 +197,19 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_4285ed15b837fbd0_EOF' + cat << 'GH_AW_PROMPT_5b32ebfb2ce4676c_EOF' - GH_AW_PROMPT_4285ed15b837fbd0_EOF + GH_AW_PROMPT_5b32ebfb2ce4676c_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_4285ed15b837fbd0_EOF' + cat << 'GH_AW_PROMPT_5b32ebfb2ce4676c_EOF' - Tools: add_comment(max:5), create_issue(max:2), create_pull_request(max:2), missing_tool, missing_data, noop - GH_AW_PROMPT_4285ed15b837fbd0_EOF + Tools: create_issue(max:3), create_pull_request(max:5), missing_tool, missing_data, noop + GH_AW_PROMPT_5b32ebfb2ce4676c_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_4285ed15b837fbd0_EOF' + cat << 'GH_AW_PROMPT_5b32ebfb2ce4676c_EOF' The following GitHub context information is available for this workflow: @@ -243,12 +242,12 @@ jobs: - **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it without proper authentication. If the branch is required and not available, exit with an error and ask the user to add it to the `fetch:` option of the `checkout:` configuration (e.g., `fetch: ["refs/pulls/open/*"]` for all open PR refs, or `fetch: ["main", "feature/my-branch"]` for specific branches). - GH_AW_PROMPT_4285ed15b837fbd0_EOF + GH_AW_PROMPT_5b32ebfb2ce4676c_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_4285ed15b837fbd0_EOF' + cat << 'GH_AW_PROMPT_5b32ebfb2ce4676c_EOF' {{#runtime-import .github/workflows/mobile-scan.md}} - GH_AW_PROMPT_4285ed15b837fbd0_EOF + GH_AW_PROMPT_5b32ebfb2ce4676c_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 @@ -400,16 +399,13 @@ jobs: GH_HOST: github.com - name: Install AWF binary run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.18 - - name: Determine automatic lockdown mode for GitHub MCP Server - id: determine-automatic-lockdown - uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + - name: Parse integrity filter lists + id: parse-guard-vars env: - GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - with: - script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); - await determineAutomaticLockdown(github, context, core); + GH_AW_BLOCKED_USERS_VAR: ${{ vars.GH_AW_GITHUB_BLOCKED_USERS || '' }} + GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }} + GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh" - name: Download container images run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.18 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18 ghcr.io/github/gh-aw-firewall/squid:0.25.18 ghcr.io/github/gh-aw-mcpg:v0.2.17 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine - name: Write Safe Outputs Config @@ -417,41 +413,22 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_00abe6b02dd0d453_EOF' - {"add_comment":{"max":5,"target":"*"},"create_issue":{"labels":["agentic-workflows","untriaged"],"max":2},"create_pull_request":{"draft":true,"labels":["agentic-workflows"],"max":2,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[mobile] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_00abe6b02dd0d453_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_9c9d9c81bf97b513_EOF' + {"create_issue":{"labels":["agentic-workflows"],"max":3},"create_pull_request":{"allowed_files":["src/libraries/**/tests/**","src/libraries/Common/tests/**"],"draft":true,"labels":["agentic-workflows"],"max":5,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"blocked","protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[mobile] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_9c9d9c81bf97b513_EOF - name: Write Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { - "add_comment": " CONSTRAINTS: Maximum 5 comment(s) can be added. Target: *.", - "create_issue": " CONSTRAINTS: Maximum 2 issue(s) can be created. Labels [\"agentic-workflows\" \"untriaged\"] will be automatically added.", - "create_pull_request": " CONSTRAINTS: Maximum 2 pull request(s) can be created. Title will be prefixed with \"[mobile] \". Labels [\"agentic-workflows\"] will be automatically added. PRs will be created as drafts." + "create_issue": " CONSTRAINTS: Maximum 3 issue(s) can be created. Labels [\"agentic-workflows\"] will be automatically added.", + "create_pull_request": " CONSTRAINTS: Maximum 5 pull request(s) can be created. Title will be prefixed with \"[mobile] \". Labels [\"agentic-workflows\"] will be automatically added. PRs will be created as drafts." }, "repo_params": {}, "dynamic_tools": [] } GH_AW_VALIDATION_JSON: | { - "add_comment": { - "defaultMax": 1, - "fields": { - "body": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 - }, - "item_number": { - "issueOrPRNumber": true - }, - "repo": { - "type": "string", - "maxLength": 256 - } - } - }, "create_issue": { "defaultMax": 1, "fields": { @@ -648,8 +625,6 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} - GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} - GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} run: | set -eo pipefail @@ -670,7 +645,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.17' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_bf684fc7da0b3d83_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + cat << GH_AW_MCP_CONFIG_17f3452edca3354e_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" { "mcpServers": { "github": { @@ -684,8 +659,11 @@ jobs: }, "guard-policies": { "allow-only": { - "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", - "repos": "$GITHUB_MCP_GUARD_REPOS" + "approval-labels": ${{ steps.parse-guard-vars.outputs.approval_labels }}, + "blocked-users": ${{ steps.parse-guard-vars.outputs.blocked_users }}, + "min-integrity": "approved", + "repos": "all", + "trusted-users": ${{ steps.parse-guard-vars.outputs.trusted_users }} } } }, @@ -711,7 +689,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_bf684fc7da0b3d83_EOF + GH_AW_MCP_CONFIG_17f3452edca3354e_EOF - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -753,7 +731,6 @@ jobs: # --allow-tool shell(ls) # --allow-tool shell(mkdir) # --allow-tool shell(pwd) - # --allow-tool shell(pwsh) # --allow-tool shell(sed) # --allow-tool shell(sh) # --allow-tool shell(sort) @@ -773,7 +750,7 @@ jobs: (umask 177 && touch /tmp/gh-aw/agent-stdio.log) # shellcheck disable=SC1003 sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --allow-domains '*.blob.core.windows.net,*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dev.azure.com,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,helix.dot.net,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.18 --skip-pull --enable-api-proxy \ - -- /bin/bash -c 'node ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-tool github --allow-tool safeoutputs --allow-tool '\''shell(awk)'\'' --allow-tool '\''shell(basename)'\'' --allow-tool '\''shell(bash)'\'' --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(chmod)'\'' --allow-tool '\''shell(curl:*)'\'' --allow-tool '\''shell(cut)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(dirname)'\'' --allow-tool '\''shell(dotnet:*)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(env)'\'' --allow-tool '\''shell(find)'\'' --allow-tool '\''shell(git add:*)'\'' --allow-tool '\''shell(git branch:*)'\'' --allow-tool '\''shell(git checkout:*)'\'' --allow-tool '\''shell(git commit:*)'\'' --allow-tool '\''shell(git merge:*)'\'' --allow-tool '\''shell(git rm:*)'\'' --allow-tool '\''shell(git status)'\'' --allow-tool '\''shell(git switch:*)'\'' --allow-tool '\''shell(git:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(jq)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(mkdir)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(pwsh)'\'' --allow-tool '\''shell(sed)'\'' --allow-tool '\''shell(sh)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(tee)'\'' --allow-tool '\''shell(test)'\'' --allow-tool '\''shell(tr)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(xargs)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'node ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-tool github --allow-tool safeoutputs --allow-tool '\''shell(awk)'\'' --allow-tool '\''shell(basename)'\'' --allow-tool '\''shell(bash)'\'' --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(chmod)'\'' --allow-tool '\''shell(curl:*)'\'' --allow-tool '\''shell(cut)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(dirname)'\'' --allow-tool '\''shell(dotnet:*)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(env)'\'' --allow-tool '\''shell(find)'\'' --allow-tool '\''shell(git add:*)'\'' --allow-tool '\''shell(git branch:*)'\'' --allow-tool '\''shell(git checkout:*)'\'' --allow-tool '\''shell(git commit:*)'\'' --allow-tool '\''shell(git merge:*)'\'' --allow-tool '\''shell(git rm:*)'\'' --allow-tool '\''shell(git status)'\'' --allow-tool '\''shell(git switch:*)'\'' --allow-tool '\''shell(git:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(jq)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(mkdir)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(sed)'\'' --allow-tool '\''shell(sh)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(tee)'\'' --allow-tool '\''shell(test)'\'' --allow-tool '\''shell(tr)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(xargs)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_GITHUB_TOKEN: ${{ case(needs.pre_activation.outputs.copilot_pat_number == '0', secrets.COPILOT_PAT_0, needs.pre_activation.outputs.copilot_pat_number == '1', secrets.COPILOT_PAT_1, needs.pre_activation.outputs.copilot_pat_number == '2', secrets.COPILOT_PAT_2, needs.pre_activation.outputs.copilot_pat_number == '3', secrets.COPILOT_PAT_3, needs.pre_activation.outputs.copilot_pat_number == '4', secrets.COPILOT_PAT_4, needs.pre_activation.outputs.copilot_pat_number == '5', secrets.COPILOT_PAT_5, needs.pre_activation.outputs.copilot_pat_number == '6', secrets.COPILOT_PAT_6, needs.pre_activation.outputs.copilot_pat_number == '7', secrets.COPILOT_PAT_7, needs.pre_activation.outputs.copilot_pat_number == '8', secrets.COPILOT_PAT_8, needs.pre_activation.outputs.copilot_pat_number == '9', secrets.COPILOT_PAT_9, secrets.COPILOT_GITHUB_TOKEN) }} @@ -940,6 +917,8 @@ jobs: /tmp/gh-aw/sandbox/agent/logs/ /tmp/gh-aw/redacted-urls.log /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/proxy-logs/ + !/tmp/gh-aw/proxy-logs/proxy-tls/ /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent-stdio.log /tmp/gh-aw/agent/ @@ -972,7 +951,6 @@ jobs: runs-on: ubuntu-slim permissions: contents: write - discussions: write issues: write pull-requests: write concurrency: @@ -1160,7 +1138,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 env: WORKFLOW_NAME: "Mobile Platform Failure Scanner" - WORKFLOW_DESCRIPTION: "Daily scan of the runtime-extra-platforms pipeline for Apple mobile and Android failures. Investigates and proposes fixes." + WORKFLOW_DESCRIPTION: "Daily scan of the runtime-extra-platforms pipeline for Apple mobile and Android failures. Fixes per-test failures via PR; files an actionable tracking issue otherwise." HAS_PATCH: ${{ needs.agent.outputs.has_patch }} with: script: | @@ -1288,7 +1266,6 @@ jobs: runs-on: ubuntu-slim permissions: contents: write - discussions: write issues: write pull-requests: write timeout-minutes: 15 @@ -1302,8 +1279,6 @@ jobs: outputs: code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} - comment_id: ${{ steps.process_safe_outputs.outputs.comment_id }} - comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} created_issue_number: ${{ steps.process_safe_outputs.outputs.created_issue_number }} @@ -1379,7 +1354,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "*.blob.core.windows.net,*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dev.azure.com,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,helix.dot.net,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":5,\"target\":\"*\"},\"create_issue\":{\"labels\":[\"agentic-workflows\",\"untriaged\"],\"max\":2},\"create_pull_request\":{\"draft\":true,\"labels\":[\"agentic-workflows\"],\"max\":2,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[mobile] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"labels\":[\"agentic-workflows\"],\"max\":3},\"create_pull_request\":{\"allowed_files\":[\"src/libraries/**/tests/**\",\"src/libraries/Common/tests/**\"],\"draft\":true,\"labels\":[\"agentic-workflows\"],\"max\":5,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"blocked\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[mobile] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/mobile-scan.md b/.github/workflows/mobile-scan.md index c79faf47e7e959..3598c13b5a636e 100644 --- a/.github/workflows/mobile-scan.md +++ b/.github/workflows/mobile-scan.md @@ -1,6 +1,6 @@ --- name: "Mobile Platform Failure Scanner" -description: "Daily scan of the runtime-extra-platforms pipeline for Apple mobile and Android failures. Investigates and proposes fixes." +description: "Daily scan of the runtime-extra-platforms pipeline for Apple mobile and Android failures. Fixes per-test failures via PR; files an actionable tracking issue otherwise." permissions: contents: read @@ -70,8 +70,9 @@ concurrency: tools: github: toolsets: [pull_requests, repos, issues, search] + min-integrity: approved edit: - bash: ["dotnet", "git", "find", "ls", "cat", "grep", "head", "tail", "wc", "curl", "jq", "pwsh", "tee", "sed", "awk", "tr", "cut", "sort", "uniq", "xargs", "echo", "date", "mkdir", "test", "env", "basename", "dirname", "bash", "sh", "chmod"] + bash: ["dotnet", "git", "find", "ls", "cat", "grep", "head", "tail", "wc", "curl", "jq", "tee", "sed", "awk", "tr", "cut", "sort", "uniq", "xargs", "echo", "date", "mkdir", "test", "env", "basename", "dirname", "bash", "sh", "chmod"] checkout: fetch-depth: 50 @@ -80,15 +81,15 @@ safe-outputs: create-pull-request: title-prefix: "[mobile] " draft: true - max: 2 - protected-files: fallback-to-issue + max: 5 + protected-files: blocked + allowed-files: + - "src/libraries/**/tests/**" + - "src/libraries/Common/tests/**" labels: [agentic-workflows] create-issue: - max: 2 - labels: [agentic-workflows, untriaged] - add-comment: - max: 5 - target: "*" + max: 3 + labels: [agentic-workflows] timeout-minutes: 60 @@ -103,150 +104,57 @@ network: # Mobile Platform Failure Scanner -You scan the `runtime-extra-platforms` pipeline (AzDO definition 154, org `dnceng-public`, project `public`) for Apple mobile and Android failures on `main`, triage them, and propose fixes. - -**Data safety:** CI logs can contain user paths, environment variables with secrets, and authentication headers. Sanitize log excerpts before posting in PR descriptions, issue comments, or commit messages by redacting these elements. - -## Step 1: Load domain knowledge - -Read `.github/skills/mobile-platforms/SKILL.md` for mobile platform triage criteria. - -For deeper Helix investigation patterns (console log analysis, pass/fail comparison, machine-specific diagnosis, XHarness false failure detection), fetch and read the helix-investigation skill from arcade-skills: - -```bash -curl -sL "https://raw.githubusercontent.com/dotnet/arcade-skills/f866c30a5b58e76492c90fd089082eb5f7e81a87/plugins/dotnet-dnceng/skills/helix-investigation/SKILL.md" -o /tmp/gh-aw/agent/helix-investigation-skill.md -cat /tmp/gh-aw/agent/helix-investigation-skill.md -``` - -Use the helix-investigation workflow (especially Steps 3-6: console log download, failure pattern matching, pass/fail comparison, root cause categorization) when drilling into individual Helix work item failures in Step 5. - -## Step 2: Get the latest build ID - -**Important conventions for this workflow environment:** - -- Each shell tool call runs in a fresh subshell -- environment variables do NOT persist across calls. Store intermediate values in files under `/tmp/gh-aw/agent/`. -- Command substitution like `$(cat file)` and parameter expansion like `${var@P}` are blocked by the agent's shell guard. Instead: either write the full command to a script file and `bash` it, or use `xargs -I{}` to inject file contents. -- **The shell guard also blocks `-o` and `>` output redirection in direct curl/command calls.** Always use `| tee /path/to/file` instead of `-o file` or `> file` for saving output. -- OData query params that start with `$` (e.g. `$top`) must be URL-encoded as `%24top` in curl URLs to avoid the shell guard. - -```bash -mkdir -p /tmp/gh-aw/agent -curl -sL "https://dev.azure.com/dnceng-public/public/_apis/build/builds?definitions=154&branchName=refs/heads/main&statusFilter=completed&%24top=1&api-version=7.1" | tee /tmp/gh-aw/agent/build.json | jq -r '.value[0] | "id=\(.id) result=\(.result)"' -``` - -Then extract the build ID and result (use `tee` instead of `>`): - -```bash -jq -r '.value[0].id' /tmp/gh-aw/agent/build.json | tee /tmp/gh-aw/agent/build_id.txt -jq -r '.value[0].result' /tmp/gh-aw/agent/build.json | tee /tmp/gh-aw/agent/build_result.txt -``` - -If `build_result.txt` contains `succeeded`, stop -- nothing to fix. - -To use the build id in a later command, write a small script that reads the file and run it: - -```bash -cat > /tmp/gh-aw/agent/run-ci-analysis.sh <<'SH' -#!/bin/bash -set -e -BUILD_ID=$(cat /tmp/gh-aw/agent/build_id.txt) -pwsh .github/skills/ci-analysis/scripts/Get-CIStatus.ps1 -BuildId "$BUILD_ID" -ShowLogs > /tmp/gh-aw/agent/ci-analysis.txt 2>&1 -echo "ci-analysis.txt size: $(wc -c < /tmp/gh-aw/agent/ci-analysis.txt)" -SH -bash /tmp/gh-aw/agent/run-ci-analysis.sh -``` - -The script is run via `bash scriptpath` (which is allowed), so the `$(...)` inside the script file is not flagged by the top-level shell guard. - -## Step 3: Analyze failures with ci-analysis - -`ci-analysis.txt` was written by the Step 2 helper script. Extract the JSON summary: - -```bash -sed -n '/\[CI_ANALYSIS_SUMMARY\]/,/^$/p' /tmp/gh-aw/agent/ci-analysis.txt > /tmp/gh-aw/agent/ci-summary.json -head -c 4000 /tmp/gh-aw/agent/ci-summary.json -``` - -Parse the `[CI_ANALYSIS_SUMMARY]` JSON to get `errorCategory`, `errorSnippet`, and `helixWorkItems` per failed job. - -## Step 4: Filter to mobile failures - -From the ci-analysis output, keep only failures whose job names match mobile platforms: - -- Apple mobile: `ios`, `tvos`, `maccatalyst`, `ioslike`, `ioslikesimulator` -- Android: `android` - -Ignore failures in non-mobile jobs. If no mobile jobs failed, stop. - -## Step 5: Drill into Helix failures +Scan the latest completed build of the `runtime-extra-platforms` pipeline (AzDO definition `154`, org `dnceng-public`, project `public`, branch `main`) for Apple mobile and Android failures. Every actionable failure becomes either a draft PR (per-test fix) or a tracking issue (everything else). Read `.github/skills/mobile-platforms/SKILL.md` first for the pipeline layout, platform helpers, and code-path map. -**You MUST drill into Helix console logs before classifying any failure.** Follow the helix-investigation skill workflow (loaded in Step 1) for each failed mobile work item: +## Outcome -1. Enumerate Helix work items from ci-analysis output (job ID, work item name, exit code, machine) -2. Download console logs and test result files per the skill's Step 3 -3. Analyze failure patterns per the skill's Step 4 (XHarness exit codes, false failure detection, timeout signatures) -4. Compare passing vs failing runs per the skill's Step 5 for intermittent failures +For each failed mobile work item in the latest completed build: -**Network note:** The `/console` endpoint on `helix.dot.net` redirects to Azure Blob Storage (`helixr*.blob.core.windows.net`, allowed by the network policy). Pass `-L` to `curl` to follow the redirect. Use `| tee /path/to/file` to save output (the shell guard blocks `-o` and `>` redirection). For complex commands with `$(...)`, write them to a script file and run with `bash`. +- **Per-test platform incompatibility** → open a draft PR. Use a per-test attribute change: `[SkipOnPlatform(...)]`, a narrowed `[ConditionalFact]` predicate built from existing `PlatformDetection.*` helpers, or `[ActiveIssue("https://github.com/dotnet/runtime/issues/", TestPlatforms.)]` referencing an **existing** issue. Touch only files matching the `allowed-files` policy (`src/libraries/**/tests/**`, including test `.csproj`). +- **Anything else** — product regression, native crash, multi-assembly cluster, infrastructure (including queue exhaustion / dead-letter / device-lost) — file a tracking issue. The issue is the deliverable; do not paper over a product bug with `SkipOnPlatform`. Group all dead-letter / queue exhaustion / device-lost failures from one run into a single infrastructure issue. Before filing, `search_issues` for an open issue with the matching `area-Infrastructure` + `os-*` label and update its description in place rather than creating a duplicate. -Capture for each failure: (a) the failing test FQN, (b) the assertion or exception, (c) the platform/arch, (d) whether the same work item repeats across jobs/runs. +Do not emit `noop`. Either a PR or an issue must come out of every actionable failure. -## Step 6: Triage each failure +Cap: **5 PRs and 3 issues per run.** Group failures that share one fix into a single PR. Group failures with the same root cause into a single issue. -Before classifying, search for existing open PRs that already fix these failures: +## Data sources -``` -gh search prs "[mobile]" --repo dotnet/runtime --state open --limit 10 -``` +- AzDO REST: `https://dev.azure.com/dnceng-public/public/_apis/build/...` — list completed builds (definition 154, branch main), get a build's timeline, download per-job AzDO logs. Mobile job names match the regex `(ios|tvos|maccatalyst|android)` (case-insensitive). +- Helix REST: `https://helix.dot.net/api/jobs/{jobId}/workitems?api-version=2019-06-17` — Helix job IDs appear in AzDO logs as `Job on `. Each work item has `Name`, `State`, `ExitCode`, `ConsoleOutputUri`. Failed: `ExitCode != 0` or `State == "Failed"`. Console URIs containing `helix-workitem-deadletter` are dead-lettered (queue had no agent) and are pure infra — drop them. -Also search for PRs referencing the specific test name or library. If a fix PR already exists, reference it in your comment instead of creating a duplicate. +Look back through roughly the last 20 completed builds to compute a "first seen in scanned window" timestamp and occurrence count per `(work_item, queue)` signature. -Classify each mobile failure using the criteria from `.github/skills/mobile-platforms/SKILL.md` and the console log content you fetched: +Drill into one representative console log per signature to confirm the failure shape (`[FAIL]` markers, assertion text) before classifying. -1. **Known build error** (ci-analysis already matched it): add a comment on that issue with the new build link and the work item name. If the root cause has an actionable code fix, proceed to Step 7. If it is purely infrastructure, stop here for this failure. -2. **Infrastructure**: provisioning/timeout/device-lost/network/Helix agent errors. Report on an existing tracking issue (or create one) with labels `area-Infrastructure` + the mobile `os-*` label. Do not attempt a code fix. -3. **Code regression**: a test that was passing started failing after a recent commit on `main`. Start with `git log --oneline --since='3 days ago' -- ` and inspect diffs. If nothing matches, widen the window or check for intermittent patterns. -4. **Platform-unsupported test**: a test that depends on behavior mobile platforms cannot support (process spawning, dynamic code emit where AOT-only, filesystem semantics, desktop JIT). The test was previously passing only because the platform was not exercised. +## PR body -## Step 7: Apply auto-fixes (do not emit noop) +Four H2 sections, in this exact order: -You are authorized -- and expected -- to open a draft PR directly for the following well-bounded patterns. Do **not** emit `noop` or only file an issue for these; commit the minimal change and open a draft PR. +1. **Reasoning** — why the test fails on the affected mobile platforms; why the chosen attribute is the right fix. +2. **Impact on platforms** — bullet list of `(platform/arch + Helix queue + exit code)` per affected occurrence. +3. **Errors log** — sanitized excerpt from the Helix console log (the `[FAIL]` line, the assertion or exception, and the `Failed tests:` summary). Strip JWTs, bearer tokens, `ApplicationGatewayAffinity*=`, and per-user paths. +4. **First build it occurred** — first build (in the scanned window) where this signature appeared: build link, finish time, commit SHA, occurrences-in-window count. State explicitly that this is computed within the scanned window and may not be the true origin. -**Auto-fixable patterns:** +Branch from `origin/main`. Stage only the files you intend to change with `git add `; never `git add -A`. Verify with `git diff --name-only --cached` before committing. Labels: one or more `os-*` (`os-android`, `os-ios`, `os-tvos`, `os-maccatalyst`) plus the test's `area-*` label. -- **Platform-unsupported test**: add `[SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.MacCatalyst | TestPlatforms.Android, "")]` to the specific `[Fact]`/`[Theory]`, or narrow an existing `[ConditionalFact]` predicate. Prefer per-test attributes over disabling the whole class. -- **Test that requires reflection/dynamic-code on AOT mobile**: guard with `[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsReflectionEmitSupported))]` or `IsNotBuiltWithAggressiveTrimming` as appropriate. -- **Flaky test with a clear retry/timing fix**: increase the timeout or add a retry only if the existing pattern in the same file already uses one; otherwise file an issue with `[ActiveIssue("https://github.com/dotnet/runtime/issues/NNN", TestPlatforms.)]` referencing a newly-created tracking issue. -- **Test project that should exclude a mobile TFM**: adjust `TargetFrameworks`, `TestRuntime`, or the `` in the `.csproj` to exclude the unsupported platform, matching conventions already used in sibling projects. +## Issue body -For each auto-fix: +Use this when a PR is not the right tool — product regression, native crash, multi-assembly cluster, infra requiring an owner. Same four sections as a PR (Reasoning, Impact on platforms, Errors log, First build it occurred), plus a fifth: -1. **Branch from `main`, not from the workflow branch.** The safe-outputs patch is computed as `branch HEAD vs main`, so if you branch from the current checkout you will inadvertently pull unrelated `.github/` diffs into the PR and trigger the protected-file fallback. Use: - ```bash - git fetch origin main - git switch -c mobile-fix- origin/main - ``` - Then make the edit. -2. **Touch only `src/` test files and their `.csproj`.** Never stage anything under `.github/`, `eng/`, `docs/`, `global.json`, or the repo root. Before committing, run `git diff --name-only --cached` and abort if any path starts with `.github/`. -3. Use `git add ` -- never `git add -A` or `git add .`. -4. Verify the edit syntactically with `grep`/`cat`. Do not attempt `./build.sh` -- it is too heavy for the agent and CI will validate. -5. Open a draft PR with title `[mobile] `. The PR body must include: the build link, the failing test name, the Helix job+work item, the console log excerpt (sanitized), and the rationale for the fix class. -6. **Set `labels` on the PR/issue** (pass them in the `create_pull_request` / `create_issue` safeoutputs call). Required labels: - - **One or more OS labels** matching the affected platforms: `os-ios`, `os-tvos`, `os-maccatalyst`, `os-android`. If a fix applies to all Apple mobile, include `os-ios`, `os-tvos`, `os-maccatalyst`. If it affects all mobile, also include `os-android`. - - **One `area-*` label** matching the test's library (e.g., `area-System.IO.Compression`, `area-System.Runtime.Loader`, `area-Infrastructure` for build/infra). Pick from the existing repo labels -- do not invent new ones. - - Optional architecture label (`arch-arm64`, `arch-x64`) only if the failure is architecture-specific. -7. Post a comment on any related existing issue linking the PR. +5. **Recommended action** — concrete next step: which area owner, which file likely needs the fix, or what investigation would localize the root cause. Reference any related PR or issue you found via `search_issues`. The issue must be actionable — a checkbox-ready task list, not just "FYI". -**Do NOT auto-fix (open a tracking issue instead):** +Same `os-*` and `area-*` labels. -- Native crashes (SIGSEGV/SIGBUS/SIGABRT) in the runtime itself. -- Failures in >3 unrelated test assemblies suggesting a product regression -- file one issue linking all failures and ping the area owners via label, not via @mention. -- Anything touching files under `protected-files` or `protected-path-prefixes` (safe-outputs will auto-fallback to an issue). +## Hard environment constraints -## Step 8: Submit +These look like permission errors but are physical: -If you found an existing fix PR in Step 6, add a comment on the tracking issue linking it instead of creating a duplicate. +- `curl` URLs containing `?` or `&` MUST be **single-quoted**. Double-quoted URLs trigger `Permission denied and could not request permission from user`. +- `>` and `-o` redirection at the agent's command line is blocked. Use `| tee /path/to/file`. +- `$(...)` and `${var@P}` are blocked at the command line. Compose values via `xargs -I{}` or by reading files inline. +- OData `$top` must be encoded as `%24top` in URLs. +- Bash allowlist: `dotnet`, `git`, `find`, `ls`, `cat`, `grep`, `head`, `tail`, `wc`, `curl`, `jq`, `tee`, `sed`, `awk`, `tr`, `cut`, `sort`, `uniq`, `xargs`, `echo`, `date`, `mkdir`, `test`, `env`, `basename`, `dirname`, `bash`, `sh`, `chmod`. No `gh`, no `pwsh`, no `python`. Each call runs in a fresh subshell — persist intermediate state to files under `/tmp/gh-aw/agent/` (just files; you do not need to author a helper script). -Only emit `noop` if, after Step 5 drill-down, the failure falls into none of the categories above **and** you have already filed or commented on an appropriate issue. A `noop` with "manual investigation required" is not acceptable -- in that case, file a tracking issue with the console log excerpt. +## Submit -If you learned something generalizable during investigation, add it as a comment on the relevant issue so the team can later fold it into `.github/skills/mobile-platforms/SKILL.md`. +Search existing issues and PRs (`search_issues`, `search_pull_requests`) before creating anything new — never duplicate. When using `search_pull_requests`, filter to `is:merged OR review:approved` so the integrity filter does not silently drop low-trust results. If an issue already tracks the failure, **prefer opening a PR that references it via `[ActiveIssue("https://github.com/dotnet/runtime/issues/")]`** rather than filing another issue. If `search_issues` returns no matches, proceed to file the issue. From 20fe1891e01b87e4958d5e8d4426543464c28ccc Mon Sep 17 00:00:00 2001 From: Vitek Karas <10670590+vitek-karas@users.noreply.github.com> Date: Mon, 4 May 2026 15:31:07 +0200 Subject: [PATCH 092/115] Disable Android arm32 test runs (#127599) > [!NOTE] > This pull request description was generated by GitHub Copilot. ## Summary - Disable the `android_arm` Mono library test matrix entry in the default runtime pipeline. - Disable the same `android_arm` entry in the Android-only extra-platforms pipeline. - Leave Android arm64 coverage enabled. References dotnet/runtime#125440. ## Testing - `git diff --check -- eng\pipelines\runtime.yml eng\pipelines\extra-platforms\runtime-extra-platforms-android.yml` Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../extra-platforms/runtime-extra-platforms-android.yml | 3 ++- eng/pipelines/runtime.yml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/eng/pipelines/extra-platforms/runtime-extra-platforms-android.yml b/eng/pipelines/extra-platforms/runtime-extra-platforms-android.yml index 39c3a88c58993d..e9fedea2ff9135 100644 --- a/eng/pipelines/extra-platforms/runtime-extra-platforms-android.yml +++ b/eng/pipelines/extra-platforms/runtime-extra-platforms-android.yml @@ -63,7 +63,8 @@ jobs: buildConfig: Release runtimeFlavor: mono platforms: - - android_arm + # Disabled until https://github.com/dotnet/runtime/issues/125440 is resolved. + # - android_arm - android_arm64 variables: # map dependencies variables to local variables diff --git a/eng/pipelines/runtime.yml b/eng/pipelines/runtime.yml index 9c5b24c8ceb3ee..a1eb3eec103e5d 100644 --- a/eng/pipelines/runtime.yml +++ b/eng/pipelines/runtime.yml @@ -1067,7 +1067,8 @@ extends: buildConfig: Release runtimeFlavor: mono platforms: - - android_arm + # Disabled until https://github.com/dotnet/runtime/issues/125440 is resolved. + # - android_arm - android_arm64 variables: # map dependencies variables to local variables From f1ca590eaa790d84d8d9eebf3185bff65865a65d Mon Sep 17 00:00:00 2001 From: Katelyn Gadd Date: Mon, 4 May 2026 06:43:36 -0700 Subject: [PATCH 093/115] [Wasm RyuJIT] Wire up GC info encoding/decoding for Wasm (#126932) * Wire up basic gc info encoding in the wasm jit * Build gcinfo_universal_wasm and link it into the wasm version of the jit * Move gcdecode/gcencode into JIT_SOURCES * Remove the TARGET_WASM exclusion for the gc info encoder and decoder * Define a Wasm32 GcInfoEncoding * Allow building with EMIT_GENERATE_GCINFO on targets without a fixed register set like Wasm, and set EMIT_GENERATE_GCINFO on Wasm * Never generate tracked gc slots on Wasm * Update wasm regalloc to never enregister gc refs * Consume temporary regs for the operand of GT_NULLCHECK nodes to handle the case where a dead block store leaves behind an orphaned indirection that turns into a null check --- src/coreclr/gcinfo/CMakeLists.txt | 1 + src/coreclr/gcinfo/gcinfoencoder.cpp | 2 -- src/coreclr/inc/gcinfotypes.h | 53 ++++++++++++++++++++++++++-- src/coreclr/jit/CMakeLists.txt | 6 ++-- src/coreclr/jit/codegencommon.cpp | 20 +++++++---- src/coreclr/jit/codegenlinear.cpp | 12 +++---- src/coreclr/jit/codegenwasm.cpp | 37 ++++++++++++++++++- src/coreclr/jit/compiler.cpp | 5 +++ src/coreclr/jit/compiler.h | 2 ++ src/coreclr/jit/emit.cpp | 16 ++++----- src/coreclr/jit/gcencode.cpp | 7 ++++ src/coreclr/jit/gcinfo.cpp | 4 +-- src/coreclr/jit/lclvars.cpp | 11 +++--- src/coreclr/jit/regalloc.cpp | 11 ------ src/coreclr/jit/regallocwasm.cpp | 28 ++++++++++++++- src/coreclr/jit/targetwasm.h | 2 +- src/coreclr/vm/eetwain.cpp | 5 +-- src/coreclr/vm/gcinfodecoder.cpp | 2 -- 18 files changed, 172 insertions(+), 52 deletions(-) diff --git a/src/coreclr/gcinfo/CMakeLists.txt b/src/coreclr/gcinfo/CMakeLists.txt index a652648f32a689..05521608a09a73 100644 --- a/src/coreclr/gcinfo/CMakeLists.txt +++ b/src/coreclr/gcinfo/CMakeLists.txt @@ -66,6 +66,7 @@ endif() if (CLR_CMAKE_TARGET_ARCH_ARM64 OR CLR_CMAKE_TARGET_ARCH_AMD64) create_gcinfo_lib(TARGET gcinfo_universal_arm64 OS universal ARCH arm64) create_gcinfo_lib(TARGET gcinfo_unix_x64 OS unix ARCH x64) + create_gcinfo_lib(TARGET gcinfo_universal_wasm OS universal ARCH wasm) if (CLR_CMAKE_BUILD_COMMUNITY_ALTJITS EQUAL 1) create_gcinfo_lib(TARGET gcinfo_unix_loongarch64 OS unix ARCH loongarch64) create_gcinfo_lib(TARGET gcinfo_unix_riscv64 OS unix ARCH riscv64) diff --git a/src/coreclr/gcinfo/gcinfoencoder.cpp b/src/coreclr/gcinfo/gcinfoencoder.cpp index e877fb534cb163..2a0636e11dbb7e 100644 --- a/src/coreclr/gcinfo/gcinfoencoder.cpp +++ b/src/coreclr/gcinfo/gcinfoencoder.cpp @@ -2634,10 +2634,8 @@ int BitStreamWriter::EncodeVarLengthSigned( SSIZE_T n, UINT32 base ) } } -#ifndef TARGET_WASM // Instantiate the encoder so other files can use it template class TGcInfoEncoder; -#endif // !TARGET_WASM #ifdef FEATURE_INTERPRETER template class TGcInfoEncoder; diff --git a/src/coreclr/inc/gcinfotypes.h b/src/coreclr/inc/gcinfotypes.h index dc56c94477db0e..8419ecd011f9e5 100644 --- a/src/coreclr/inc/gcinfotypes.h +++ b/src/coreclr/inc/gcinfotypes.h @@ -909,13 +909,62 @@ struct X86GcInfoEncoding { static const bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA = true; }; -#elif defined(TARGET_WASM) +#elif defined(TARGET_WASM) && !defined(TARGET_64BIT) #ifndef TARGET_POINTER_SIZE #define TARGET_POINTER_SIZE 4 // equal to sizeof(void*) and the managed pointer size in bytes for this target #endif -#define TargetGcInfoEncoding InterpreterGcInfoEncoding +#define TargetGcInfoEncoding Wasm32GcInfoEncoding + +// TODO-WASM: Investigate normalizing stack slots to save space based on wasm stack alignment + +struct Wasm32GcInfoEncoding { + static const uint32_t NUM_NORM_CODE_OFFSETS_PER_CHUNK = (64); + static const uint32_t NUM_NORM_CODE_OFFSETS_PER_CHUNK_LOG2 = (6); + static inline constexpr int32_t NORMALIZE_STACK_SLOT (int32_t x) { return (x); } + static inline constexpr int32_t DENORMALIZE_STACK_SLOT (int32_t x) { return (x); } + static inline constexpr uint32_t NORMALIZE_CODE_LENGTH (uint32_t x) { return (x); } + static inline constexpr uint32_t DENORMALIZE_CODE_LENGTH (uint32_t x) { return (x); } + static inline constexpr uint32_t NORMALIZE_STACK_BASE_REGISTER (uint32_t x) { return (x); } + static inline constexpr uint32_t DENORMALIZE_STACK_BASE_REGISTER (uint32_t x) { return (x); } + static inline constexpr uint32_t NORMALIZE_SIZE_OF_STACK_AREA (uint32_t x) { return (x); } + static inline constexpr uint32_t DENORMALIZE_SIZE_OF_STACK_AREA (uint32_t x) { return (x); } + static const bool CODE_OFFSETS_NEED_NORMALIZATION = false; + static inline constexpr uint32_t NORMALIZE_CODE_OFFSET (uint32_t x) { return (x); } + static inline constexpr uint32_t DENORMALIZE_CODE_OFFSET (uint32_t x) { return (x); } + + static const int PSP_SYM_STACK_SLOT_ENCBASE = 6; + static const int GENERICS_INST_CONTEXT_STACK_SLOT_ENCBASE = 6; + static const int SECURITY_OBJECT_STACK_SLOT_ENCBASE = 6; + static const int GS_COOKIE_STACK_SLOT_ENCBASE = 6; + static const int CODE_LENGTH_ENCBASE = 6; + static const int SIZE_OF_RETURN_KIND_IN_SLIM_HEADER = 2; + static const int SIZE_OF_RETURN_KIND_IN_FAT_HEADER = 2; + static const int STACK_BASE_REGISTER_ENCBASE = 3; + static const int SIZE_OF_STACK_AREA_ENCBASE = 6; + static const int SIZE_OF_EDIT_AND_CONTINUE_PRESERVED_AREA_ENCBASE = 3; + static const int REVERSE_PINVOKE_FRAME_ENCBASE = 6; + static const int NUM_REGISTERS_ENCBASE = 3; + static const int NUM_STACK_SLOTS_ENCBASE = 5; + static const int NUM_UNTRACKED_SLOTS_ENCBASE = 5; + static const int NORM_PROLOG_SIZE_ENCBASE = 4; + static const int NORM_EPILOG_SIZE_ENCBASE = 3; + static const int NORM_CODE_OFFSET_DELTA_ENCBASE = 3; + static const int INTERRUPTIBLE_RANGE_DELTA1_ENCBASE = 5; + static const int INTERRUPTIBLE_RANGE_DELTA2_ENCBASE = 5; + static const int REGISTER_ENCBASE = 3; + static const int REGISTER_DELTA_ENCBASE = REGISTER_ENCBASE; + static const int STACK_SLOT_ENCBASE = 6; + static const int STACK_SLOT_DELTA_ENCBASE = 4; + static const int NUM_SAFE_POINTS_ENCBASE = 4; + static const int NUM_INTERRUPTIBLE_RANGES_ENCBASE = 1; + static const int NUM_EH_CLAUSES_ENCBASE = 2; + static const int POINTER_SIZE_ENCBASE = 3; + static const int LIVESTATE_RLE_RUN_ENCBASE = 2; + static const int LIVESTATE_RLE_SKIP_ENCBASE = 4; + static const bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA = false; +}; #else // No target defined diff --git a/src/coreclr/jit/CMakeLists.txt b/src/coreclr/jit/CMakeLists.txt index 2efb2b841bb04d..dd83c6891518fe 100644 --- a/src/coreclr/jit/CMakeLists.txt +++ b/src/coreclr/jit/CMakeLists.txt @@ -23,7 +23,7 @@ function(create_standalone_jit) if(TARGETDETAILS_OS STREQUAL "unix_osx" OR TARGETDETAILS_OS STREQUAL "unix_anyos") set(JIT_ARCH_LINK_LIBRARIES gcinfo_unix_${TARGETDETAILS_ARCH}) - elseif(NOT ${TARGETDETAILS_ARCH} MATCHES "wasm") + else() set(JIT_ARCH_LINK_LIBRARIES gcinfo_${TARGETDETAILS_OS}_${TARGETDETAILS_ARCH}) endif() @@ -120,6 +120,8 @@ set( JIT_SOURCES fgstmt.cpp flowgraph.cpp forwardsub.cpp + gcdecode.cpp + gcencode.cpp gcinfo.cpp gentree.cpp gschecks.cpp @@ -186,8 +188,6 @@ set ( JIT_NATIVE_TARGET_SOURCES lsra.cpp lsrabuild.cpp regMaskTPOps.cpp - gcdecode.cpp - gcencode.cpp unwind.cpp ) diff --git a/src/coreclr/jit/codegencommon.cpp b/src/coreclr/jit/codegencommon.cpp index 5ebd0dd3a83a59..2fe77b1a3294b0 100644 --- a/src/coreclr/jit/codegencommon.cpp +++ b/src/coreclr/jit/codegencommon.cpp @@ -854,7 +854,9 @@ bool CodeGen::genIsSameLocalVar(GenTree* op1, GenTree* op2) // inline void CodeGenInterface::genUpdateRegLife(const LclVarDsc* varDsc, bool isBorn, bool isDying DEBUGARG(GenTree* tree)) { -#if EMIT_GENERATE_GCINFO // The regset being updated here is only needed for codegen-level GCness tracking + // Targets like Wasm do not have a fixed set of registers so the regset logic in this method is unnecessary. +#if EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET + // The regset being updated here is only needed for codegen-level GCness tracking. regMaskTP regMask = genGetRegMask(varDsc); #ifdef DEBUG @@ -884,7 +886,7 @@ void CodeGenInterface::genUpdateRegLife(const LclVarDsc* varDsc, bool isBorn, bo assert(varDsc->IsAlwaysAliveInMemory() || ((regSet.GetMaskVars() & regMask) == 0)); regSet.AddMaskVars(regMask); } -#endif // EMIT_GENERATE_GCINFO +#endif // EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET } #ifndef TARGET_WASM @@ -1032,6 +1034,7 @@ void Compiler::compChangeLife(VARSET_VALARG_TP newLife) bool isByRef = varDsc->TypeIs(TYP_BYREF); bool isInReg = varDsc->lvIsInReg(); bool isInMemory = !isInReg || varDsc->IsAlwaysAliveInMemory(); +#ifndef TARGET_WASM if (isInReg) { // TODO-Cleanup: Move the code from compUpdateLifeVar to genUpdateRegLife that updates the @@ -1047,7 +1050,8 @@ void Compiler::compChangeLife(VARSET_VALARG_TP newLife) } codeGen->genUpdateRegLife(varDsc, false /*isBorn*/, true /*isDying*/ DEBUGARG(nullptr)); } - // Update the gcVarPtrSetCur if it is in memory. +#endif // !TARGET_WASM + // Update the gcVarPtrSetCur if it is in memory. if (isInMemory && (isGCRef || isByRef)) { VarSetOps::RemoveElemD(this, codeGen->gcInfo.gcVarPtrSetCur, deadVarIndex); @@ -1070,6 +1074,7 @@ void Compiler::compChangeLife(VARSET_VALARG_TP newLife) bool isByRef = varDsc->TypeIs(TYP_BYREF); if (varDsc->lvIsInReg()) { +#ifndef TARGET_WASM // If this variable is going live in a register, it is no longer live on the stack, // unless it is an EH/"spill at single-def" var, which always remains live on the stack. if (!varDsc->IsAlwaysAliveInMemory()) @@ -1092,6 +1097,7 @@ void Compiler::compChangeLife(VARSET_VALARG_TP newLife) { codeGen->gcInfo.gcRegByrefSetCur |= regMask; } +#endif // !TARGET_WASM } else if (lvaIsGCTracked(varDsc)) { @@ -1769,7 +1775,7 @@ void CodeGen::genExitCode(BasicBlock* block) genIPmappingAdd(IPmappingDscKind::Epilog, DebugInfo(), true); -#if EMIT_GENERATE_GCINFO && defined(DEBUG) +#if EMIT_GENERATE_GCINFO && defined(DEBUG) && !defined(TARGET_WASM) // For returnining epilogs do some validation that the GC info looks right. if (!block->HasFlag(BBF_HAS_JMP)) { @@ -1791,7 +1797,7 @@ void CodeGen::genExitCode(BasicBlock* block) } } } -#endif // EMIT_GENERATE_GCINFO && defined(DEBUG) +#endif // EMIT_GENERATE_GCINFO && defined(DEBUG) && !defined(TARGET_WASM) if (m_compiler->getNeedsGSSecurityCookie()) { @@ -7202,12 +7208,12 @@ void CodeGen::genReturn(GenTree* treeNode) } } -#if EMIT_GENERATE_GCINFO +#if EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET if (treeNode->OperIs(GT_RETURN, GT_SWIFT_ERROR_RET)) { genMarkReturnGCInfo(); } -#endif // EMIT_GENERATE_GCINFO +#endif // EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET #ifdef PROFILING_SUPPORTED diff --git a/src/coreclr/jit/codegenlinear.cpp b/src/coreclr/jit/codegenlinear.cpp index 5386aa65a5d74b..b93ca108bb29b4 100644 --- a/src/coreclr/jit/codegenlinear.cpp +++ b/src/coreclr/jit/codegenlinear.cpp @@ -259,7 +259,7 @@ void CodeGen::genCodeForBlock(BasicBlock* block) // and before first of the current block is emitted genUpdateLife(block->bbLiveIn); -#if EMIT_GENERATE_GCINFO +#if EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET // Even if liveness didn't change, we need to update the registers containing GC references. // genUpdateLife will update the registers live due to liveness changes. But what about registers that didn't // change? We cleared them out above. Maybe we should just not clear them out, but update the ones that change @@ -353,7 +353,7 @@ void CodeGen::genCodeForBlock(BasicBlock* block) } } } -#endif // EMIT_GENERATE_GCINFO +#endif // EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET /* Start a new code output block */ @@ -569,7 +569,7 @@ void CodeGen::genCodeForBlock(BasicBlock* block) regSet.rsSpillChk(); -#if EMIT_GENERATE_GCINFO +#if EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET // Make sure we didn't bungle pointer register tracking regMaskTP ptrRegs = gcInfo.gcRegGCrefSetCur | gcInfo.gcRegByrefSetCur; regMaskTP nonVarPtrRegs = ptrRegs & ~regSet.GetMaskVars(); @@ -618,7 +618,7 @@ void CodeGen::genCodeForBlock(BasicBlock* block) } noway_assert(nonVarPtrRegs == RBM_NONE); -#endif // EMIT_GENERATE_GCINFO +#endif // EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET #endif // DEBUG #if defined(DEBUG) @@ -1601,7 +1601,7 @@ regNumber CodeGen::genConsumeReg(GenTree* tree) // genUpdateLife() will also spill local var if marked as GTF_SPILL by calling CodeGen::genSpillVar genUpdateLife(tree); -#if EMIT_GENERATE_GCINFO +#if EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET // there are three cases where consuming a reg means clearing the bit in the live mask // 1. it was not produced by a local // 2. it was produced by a local that is going dead @@ -1659,7 +1659,7 @@ regNumber CodeGen::genConsumeReg(GenTree* tree) { gcInfo.gcMarkRegSetNpt(tree->gtGetRegMask()); } -#endif // EMIT_GENERATE_GCINFO +#endif // EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET genCheckConsumeNode(tree); return tree->GetRegNum(); diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index caaec17c160446..4df06a6e5d22b0 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -9,6 +9,8 @@ #include "codegen.h" #include "regallocwasm.h" #include "fgwasm.h" +#include "gcinfo.h" +#include "gcinfoencoder.h" static const int LINEAR_MEMORY_INDEX = 0; @@ -3351,7 +3353,40 @@ void CodeGen::inst_JMP(emitJumpKind jmp, BasicBlock* tgtBlock) void CodeGen::genCreateAndStoreGCInfo(unsigned codeSize, unsigned prologSize, unsigned epilogSize DEBUGARG(void* code)) { - // GCInfo not captured/created by codegen. + IAllocator* allowZeroAlloc = new (m_compiler, CMK_GC) CompIAllocator(m_compiler->getAllocatorGC()); + GcInfoEncoder* gcInfoEncoder = new (m_compiler, CMK_GC) + GcInfoEncoder(m_compiler->info.compCompHnd, m_compiler->info.compMethodInfo, allowZeroAlloc, NOMEM); + assert(gcInfoEncoder != nullptr); + + // Follow the code pattern of the x86 gc info encoder (genCreateAndStoreGCInfoJIT32). + gcInfo.gcInfoBlockHdrSave(gcInfoEncoder, codeSize, prologSize); + + // We keep the call count for the second call to gcMakeRegPtrTable() below. + unsigned callCnt = 0; + + // First we figure out the encoder ID's for the stack slots and registers. + gcInfo.gcMakeRegPtrTable(gcInfoEncoder, codeSize, prologSize, GCInfo::MAKE_REG_PTR_MODE_ASSIGN_SLOTS, &callCnt); + + // Now we've requested all the slots we'll need; "finalize" these (make more compact data structures for them). + gcInfoEncoder->FinalizeSlotIds(); + + // Now we can actually use those slot ID's to declare live ranges. + gcInfo.gcMakeRegPtrTable(gcInfoEncoder, codeSize, prologSize, GCInfo::MAKE_REG_PTR_MODE_DO_WORK, &callCnt); + + if (m_compiler->opts.IsReversePInvoke()) + { + unsigned reversePInvokeFrameVarNumber = m_compiler->lvaReversePInvokeFrameVar; + assert(reversePInvokeFrameVarNumber != BAD_VAR_NUM); + const LclVarDsc* reversePInvokeFrameVar = m_compiler->lvaGetDesc(reversePInvokeFrameVarNumber); + gcInfoEncoder->SetReversePInvokeFrameSlot(reversePInvokeFrameVar->GetStackOffset()); + } + + gcInfoEncoder->Build(); + + // GC Encoder automatically puts the GC info in the right spot using ICorJitInfo::allocGCInfo(size_t) + // let's save the values anyway for debugging purposes + m_compiler->compInfoBlkAddr = gcInfoEncoder->Emit(); + m_compiler->compInfoBlkSize = gcInfoEncoder->GetEncodedGCInfoSize(); } //--------------------------------------------------------------------- diff --git a/src/coreclr/jit/compiler.cpp b/src/coreclr/jit/compiler.cpp index daf91aaf30e983..24634a2fc4d823 100644 --- a/src/coreclr/jit/compiler.cpp +++ b/src/coreclr/jit/compiler.cpp @@ -10738,6 +10738,10 @@ void Compiler::EnregisterStats::RecordLocal(const LclVarDsc* varDsc) m_simdUserForcesDep++; break; + case DoNotEnregisterReason::WasmGCVisibility: + m_wasmGcVisibility++; + break; + default: unreached(); break; @@ -10866,6 +10870,7 @@ void Compiler::EnregisterStats::Dump(FILE* fout) const PRINT_STATS(m_swizzleArg, notEnreg); PRINT_STATS(m_blockOpRet, notEnreg); PRINT_STATS(m_returnSpCheck, notEnreg); + PRINT_STATS(m_wasmGcVisibility, notEnreg); PRINT_STATS(m_callSpCheck, notEnreg); PRINT_STATS(m_simdUserForcesDep, notEnreg); diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index 74ebf2548df805..7f6426b6b7bf31 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -488,6 +488,7 @@ enum class DoNotEnregisterReason CallSpCheck, // the local is used to do SP check on every call SimdUserForcesDep, // a promoted struct was used by a SIMD/HWI node; it must be dependently promoted HiddenBufferStructArg, // the argument is a hidden return buffer passed to a method. + WasmGCVisibility, }; enum class AddressExposedReason @@ -11761,6 +11762,7 @@ class Compiler unsigned m_liveInOutHndlr; unsigned m_depField; unsigned m_noRegVars; + unsigned m_wasmGcVisibility; #ifdef JIT32_GCENCODER unsigned m_PinningRef; #endif // JIT32_GCENCODER diff --git a/src/coreclr/jit/emit.cpp b/src/coreclr/jit/emit.cpp index 8f36252fb769bc..bb1794404c67b7 100644 --- a/src/coreclr/jit/emit.cpp +++ b/src/coreclr/jit/emit.cpp @@ -9076,7 +9076,7 @@ void emitter::emitUpdateLiveGCregs(GCtype gcType, regMaskTP regs, BYTE* addr) return; } -#if EMIT_GENERATE_GCINFO +#if EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET regMaskTP life; regMaskTP dead; regMaskTP chg; @@ -9131,7 +9131,7 @@ void emitter::emitUpdateLiveGCregs(GCtype gcType, regMaskTP regs, BYTE* addr) // The 2 GC reg masks can't be overlapping assert((emitThisGCrefRegs & emitThisByrefRegs) == 0); -#endif // EMIT_GENERATE_GCINFO +#endif // EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET } /***************************************************************************** @@ -9434,7 +9434,7 @@ void emitter::emitGCregLiveUpd(GCtype gcType, regNumber reg, BYTE* addr) { assert(emitIssuing); -#if EMIT_GENERATE_GCINFO +#if EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET // Don't track GC changes in epilogs if (emitIGisInEpilog(emitCurIG)) { @@ -9476,7 +9476,7 @@ void emitter::emitGCregLiveUpd(GCtype gcType, regNumber reg, BYTE* addr) // The 2 GC reg masks can't be overlapping assert((emitThisGCrefRegs & emitThisByrefRegs) == 0); -#endif // EMIT_GENERATE_GCINFO +#endif // EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET } /***************************************************************************** @@ -9494,7 +9494,7 @@ void emitter::emitGCregDeadUpdMask(regMaskTP regs, BYTE* addr) return; } -#if EMIT_GENERATE_GCINFO +#if EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET // First, handle the gcref regs going dead regMaskTP gcrefRegs = emitThisGCrefRegs & regs; @@ -9530,7 +9530,7 @@ void emitter::emitGCregDeadUpdMask(regMaskTP regs, BYTE* addr) emitThisByrefRegs &= ~byrefRegs; } -#endif // EMIT_GENERATE_GCINFO +#endif // EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET } /***************************************************************************** @@ -9542,7 +9542,7 @@ void emitter::emitGCregDeadUpd(regNumber reg, BYTE* addr) { assert(emitIssuing); -#if EMIT_GENERATE_GCINFO +#if EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET // Don't track GC changes in epilogs if (emitIGisInEpilog(emitCurIG)) { @@ -9571,7 +9571,7 @@ void emitter::emitGCregDeadUpd(regNumber reg, BYTE* addr) emitThisByrefRegs &= ~regMask; } -#endif // EMIT_GENERATE_GCINFO +#endif // EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET } /***************************************************************************** diff --git a/src/coreclr/jit/gcencode.cpp b/src/coreclr/jit/gcencode.cpp index 40908eccefa695..f7742b072d689e 100644 --- a/src/coreclr/jit/gcencode.cpp +++ b/src/coreclr/jit/gcencode.cpp @@ -4091,7 +4091,12 @@ void GCInfo::gcMakeRegPtrTable( { GCENCODER_WITH_LOGGING(gcInfoEncoderWithLog, gcInfoEncoder); + // TODO-WASM: Enable tracked GC slots for precise GC +#ifdef TARGET_WASM + const bool noTrackedGCSlots = true; +#else const bool noTrackedGCSlots = m_compiler->opts.MinOpts(); +#endif if (mode == MAKE_REG_PTR_MODE_ASSIGN_SLOTS) { @@ -4646,6 +4651,7 @@ void GCInfo::gcInfoRecordGCRegStateChange(GcInfoEncoder* gcInfoEncoder, regMaskSmall byRefMask, regMaskSmall* pPtrRegs) { +#if HAS_FIXED_REGISTER_SET // Precondition: byRefMask is a subset of regMask. assert((byRefMask & ~regMask) == 0); @@ -4703,6 +4709,7 @@ void GCInfo::gcInfoRecordGCRegStateChange(GcInfoEncoder* gcInfoEncoder, // Turn the bit we've just generated off and continue. regMask ^= tmpMask; // EAX,ECX,EDX,EBX,---,EBP,ESI,EDI } +#endif // HAS_FIXED_REGISTER_SET } /************************************************************************** diff --git a/src/coreclr/jit/gcinfo.cpp b/src/coreclr/jit/gcinfo.cpp index e687e169e2a049..d4259158b14291 100644 --- a/src/coreclr/jit/gcinfo.cpp +++ b/src/coreclr/jit/gcinfo.cpp @@ -196,7 +196,7 @@ void GCInfo::gcMarkRegSetNpt(regMaskTP regMask DEBUGARG(bool forceOutput)) void GCInfo::gcMarkRegPtrVal(regNumber reg, var_types type) { -#if EMIT_GENERATE_GCINFO +#if EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET regMaskTP regMask = genRegMask(reg); switch (type) @@ -211,7 +211,7 @@ void GCInfo::gcMarkRegPtrVal(regNumber reg, var_types type) gcMarkRegSetNpt(regMask); break; } -#endif // EMIT_GENERATE_GCINFO +#endif // EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET } //------------------------------------------------------------------------ diff --git a/src/coreclr/jit/lclvars.cpp b/src/coreclr/jit/lclvars.cpp index ba92c8035fd012..34ad714fc1d2c0 100644 --- a/src/coreclr/jit/lclvars.cpp +++ b/src/coreclr/jit/lclvars.cpp @@ -2345,6 +2345,9 @@ void Compiler::lvaSetVarDoNotEnregister(unsigned varNum DEBUGARG(DoNotEnregister case DoNotEnregisterReason::LocalField: JITDUMP("was accessed as a local field\n"); break; + case DoNotEnregisterReason::WasmGCVisibility: + JITDUMP("Wasm GC needs to see it\n"); + break; case DoNotEnregisterReason::VMNeedsStackAddr: JITDUMP("VM needs stack addr\n"); break; @@ -4176,7 +4179,7 @@ unsigned Compiler::lvaGetMaxSpillTempSize() * * Wasm leaf frame, no localloc * - * + * * | caller frame | * +=======================+ <---- Virtual '0' * | | @@ -4190,7 +4193,7 @@ unsigned Compiler::lvaGetMaxSpillTempSize() * V * * Wasm, leaf frame, localloc - * + * * | caller frame | * +=======================+ <---- Virtual '0' * | | @@ -4225,9 +4228,9 @@ unsigned Compiler::lvaGetMaxSpillTempSize() * | | Stack grows | * | downward * V - * + * * Wasm, non-leaf frame, localloc - * + * * | caller frame | * +=======================+ <---- Virtual '0' * | | diff --git a/src/coreclr/jit/regalloc.cpp b/src/coreclr/jit/regalloc.cpp index 315bedffc55a4a..259f9cd9cdfb52 100644 --- a/src/coreclr/jit/regalloc.cpp +++ b/src/coreclr/jit/regalloc.cpp @@ -415,17 +415,6 @@ bool RegAllocImpl::isRegCandidate(LclVarDsc* varDsc) return false; } -#if defined(TARGET_WASM) - // Wasm RA currently does not support EH write-thru, so any local live in or out - // of a handler must be located only on the stack. - // - if (varDsc->lvLiveInOutOfHndlr) - { - compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::LiveInOutOfHandler)); - return false; - } -#endif // defined(TARGET_WASM) - if (varDsc->lvDoNotEnregister) { return false; diff --git a/src/coreclr/jit/regallocwasm.cpp b/src/coreclr/jit/regallocwasm.cpp index 2d8cfcf359626f..2e296c3110a1e0 100644 --- a/src/coreclr/jit/regallocwasm.cpp +++ b/src/coreclr/jit/regallocwasm.cpp @@ -170,7 +170,25 @@ void WasmRegAlloc::IdentifyCandidates() LclVarDsc* varDsc = m_compiler->lvaGetDesc(lclNum); varDsc->SetRegNum(REG_STK); - if (isRegCandidate(varDsc)) + bool varIsRegCandidate = isRegCandidate(varDsc); + + // Wasm RA currently does not support EH write-thru, so any local live in or out + // of a handler must be located only on the stack. + if (varDsc->lvLiveInOutOfHndlr) + { + m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::LiveInOutOfHandler)); + varIsRegCandidate = false; + } + // We also need to ensure that any GC refs are not stored in wasm locals until we have support for + // spilling them to the stack before calls. + // TODO-WASM: Add support for spilling GC refs in order to relax this second restriction. + if (varTypeIsGC(varDsc->lvType)) + { + m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::WasmGCVisibility)); + varIsRegCandidate = false; + } + + if (varIsRegCandidate) { JITDUMP("RA candidate: V%02u\n", lclNum); InitializeCandidate(varDsc); @@ -413,6 +431,14 @@ void WasmRegAlloc::CollectReferencesForNode(GenTree* node) { switch (node->OperGet()) { + case GT_NULLCHECK: + if (node->gtGetOp1()->gtLIRFlags & LIR::Flags::MultiplyUsed) + { + ConsumeTemporaryRegForOperand(node->gtGetOp1() + DEBUGARG("Orphaned GT_NULLCHECK with multiply-used flag")); + } + break; + case GT_LCL_VAR: CollectReferencesForLclVar(node->AsLclVar()); break; diff --git a/src/coreclr/jit/targetwasm.h b/src/coreclr/jit/targetwasm.h index ba00eb9624900d..1262df809f9d3b 100644 --- a/src/coreclr/jit/targetwasm.h +++ b/src/coreclr/jit/targetwasm.h @@ -50,7 +50,7 @@ #define CSE_CONSTS 1 // Enable if we want to CSE constants #define LOWER_DECOMPOSE_LONGS 0 // Decompose TYP_LONG operations into (typically two) TYP_INT ones #define EMIT_TRACK_STACK_DEPTH 0 // No need to track arg pushes/pops -#define EMIT_GENERATE_GCINFO 0 // Codegen and emit not responsible for GC liveness tracking and GCInfo generation +#define EMIT_GENERATE_GCINFO 1 // Codegen and emit generate GC info; on WASM this enables stack slot GC info encoding without fixed-register GC tracking // Since we don't have a fixed register set on WASM, we set most of the following register defines to 'none'-like values. #define REG_FP_FIRST REG_NA diff --git a/src/coreclr/vm/eetwain.cpp b/src/coreclr/vm/eetwain.cpp index 2fef103782ad88..12261ea5d4d492 100644 --- a/src/coreclr/vm/eetwain.cpp +++ b/src/coreclr/vm/eetwain.cpp @@ -712,6 +712,7 @@ bool EECodeManager::IsGcSafe( EECodeInfo *pCodeInfo, return false; } +// FIXME-WASM: Add TARGET_WASM once we implement tail calls on Wasm. #if defined(TARGET_ARM) || defined(TARGET_ARM64) || defined(TARGET_LOONGARCH64) || defined(TARGET_RISCV64) bool EECodeManager::HasTailCalls( EECodeInfo *pCodeInfo) { @@ -1941,7 +1942,7 @@ void InterpreterCodeManager::ResumeAfterCatch(CONTEXT *pContext, size_t targetSS #if defined(HOST_AMD64) && defined(HOST_WINDOWS) targetSSP = pInterpreterFrame->GetInterpExecMethodSSP(); -#endif +#endif ExecuteFunctionBelowContext((PCODE)ThrowResumeAfterCatchException, pContext, targetSSP, resumeSP, resumeIP); #endif // TARGET_WASM } @@ -2112,7 +2113,7 @@ static void VirtualUnwindInterpreterCallFrame(TADDR sp, T_CONTEXT *pContext) else { // This indicates that there are no more interpreter frames to unwind in the current InterpExecMethod - // The stack walker will not find any code manager for the address InterpreterFrame::DummyCallerIP (0) + // The stack walker will not find any code manager for the address InterpreterFrame::DummyCallerIP (0) // and move on to the next explicit frame which is the InterpreterFrame. // The SP is set to the address of the InterpreterFrame. For the case of interpreted exception handling // funclets, this matches the pExInfo->m_csfEHClause.SP that the CallFunclet sets. diff --git a/src/coreclr/vm/gcinfodecoder.cpp b/src/coreclr/vm/gcinfodecoder.cpp index 82842705d217f7..681411ae521851 100644 --- a/src/coreclr/vm/gcinfodecoder.cpp +++ b/src/coreclr/vm/gcinfodecoder.cpp @@ -2274,10 +2274,8 @@ template void TGcInfoDecoder::ReportSt pCallBack(hCallBack, pObjRef, gcFlags DAC_ARG(DacSlotLocation(GetStackReg(spBase), spOffset, true))); } -#ifndef TARGET_WASM // Instantiate the decoder so other files can use it template class TGcInfoDecoder; -#endif // !TARGET_WASM #ifdef FEATURE_INTERPRETER template class TGcInfoDecoder; From 766f347ac9ad86d64e2ad57054a99551d6d11870 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 22:06:19 +0000 Subject: [PATCH 094/115] Add Linux and Android support for ProcessStartInfo.KillOnParentExit using prctl(PR_SET_PDEATHSIG) (#127112) ## Description Adds Linux and Android support for `ProcessStartInfo.KillOnParentExit` using `prctl(PR_SET_PDEATHSIG)`. Since `PR_SET_PDEATHSIG` fires when the calling *thread* exits (not the process), a dedicated long-lived thread is used to perform `fork+exec` so the signal is only delivered when the process exits. ### Key design decisions - **Dedicated thread**: `EnsurePDeathSigThread` lazily starts a detached thread that lives for the process lifetime. The thread-start check uses a plain `bool` protected by the mutex (no atomics needed since the check is always done under the mutex). - **Mutex/condvar synchronization**: Callers submit a `PDeathSigForkRequest` struct to the dedicated thread via a shared pointer, protected by a mutex. Callers wait for `s_pdeathsig_request == NULL` before submitting to prevent overwriting a pending request, and each request has a per-request `done` flag to avoid lost-wakeup issues. - **Orphan detection**: After `prctl(PR_SET_PDEATHSIG)` in the child, a `getppid()` check detects if the parent died between `fork` and `prctl`, since `PR_SET_PDEATHSIG` will not deliver the signal in that case. - **`KillOnParentExit` passed unconditionally**: The managed code passes `KillOnParentExit` to the native shim unconditionally; the native side handles unsupported platforms as a no-op via `HAVE_PR_SET_PDEATHSIG`. - **Android support**: `PR_SET_PDEATHSIG` is available on Android, so `[SupportedOSPlatform("android")]` is added alongside `[SupportedOSPlatform("linux")]`. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> Co-authored-by: Adam Sitnik Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../Interop.ForkAndExecProcess.cs | 6 +- .../ref/System.Diagnostics.Process.cs | 2 + .../SafeHandles/SafeProcessHandle.Unix.cs | 4 +- .../System/Diagnostics/ProcessStartInfo.cs | 5 + .../tests/KillOnParentExitTests.cs | 2 +- .../tests/ProcessTests.Mobile.cs | 13 + src/native/libs/Common/pal_config.h.in | 1 + src/native/libs/System.Native/pal_process.c | 254 +++++++++++++++++- src/native/libs/System.Native/pal_process.h | 3 +- .../libs/System.Native/pal_process_wasi.c | 3 +- src/native/libs/configure.cmake | 5 + 11 files changed, 290 insertions(+), 8 deletions(-) diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs index d33e60fa426eb6..7f2622b6d49c19 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs @@ -17,7 +17,7 @@ internal static unsafe int ForkAndExecProcess( string filename, string[] argv, IDictionary env, string? cwd, bool setUser, uint userId, uint groupId, uint[]? groups, out int lpChildPid, SafeFileHandle? stdinFd, SafeFileHandle? stdoutFd, SafeFileHandle? stderrFd, - bool startDetached, SafeHandle[]? inheritedHandles = null) + bool startDetached, bool killOnParentExit, SafeHandle[]? inheritedHandles = null) { byte** argvPtr = null, envpPtr = null; int result = -1; @@ -76,7 +76,7 @@ internal static unsafe int ForkAndExecProcess( filename, argvPtr, envpPtr, cwd, setUser ? 1 : 0, userId, groupId, pGroups, groups?.Length ?? 0, out lpChildPid, stdinRawFd, stdoutRawFd, stderrRawFd, - pInheritedFds, inheritedFdCount, startDetached ? 1 : 0); + pInheritedFds, inheritedFdCount, startDetached ? 1 : 0, killOnParentExit ? 1 : 0); } return result == 0 ? 0 : Marshal.GetLastPInvokeError(); } @@ -105,7 +105,7 @@ private static unsafe partial int ForkAndExecProcess( string filename, byte** argv, byte** envp, string? cwd, int setUser, uint userId, uint groupId, uint* groups, int groupsLength, out int lpChildPid, int stdinFd, int stdoutFd, int stderrFd, - int* inheritedFds, int inheritedFdCount, int startDetached); + int* inheritedFds, int inheritedFdCount, int startDetached, int killOnParentExit); /// /// Allocates a single native memory block containing both a null-terminated pointer array diff --git a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs index 85682d07a03c30..ea07d694f7c2b7 100644 --- a/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs +++ b/src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs @@ -319,6 +319,8 @@ public ProcessStartInfo(string fileName, System.Collections.Generic.IEnumerable< [System.Diagnostics.CodeAnalysis.AllowNullAttribute] public string FileName { get { throw null; } set { } } public System.Collections.Generic.IList? InheritedHandles { get { throw null; } set { } } + [System.Runtime.Versioning.SupportedOSPlatformAttribute("android")] + [System.Runtime.Versioning.SupportedOSPlatformAttribute("linux")] [System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")] public bool KillOnParentExit { get { throw null; } set { } } [System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")] diff --git a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs index 0b48b5e9d1e191..580c921aa243ec 100644 --- a/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.cs @@ -326,7 +326,9 @@ private static SafeProcessHandle ForkAndExecProcess( resolvedFilename, argv, env, cwd, setCredentials, userId, groupId, groups, out childPid, stdinHandle, stdoutHandle, stderrHandle, - startInfo.StartDetached, inheritedHandles); +#pragma warning disable CA1416 // KillOnParentExit getter works on all platforms; the native shim is a no-op where unsupported + startInfo.StartDetached, startInfo.KillOnParentExit, inheritedHandles); +#pragma warning restore CA1416 if (errno == 0) { diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs index 11ff3f54fe152e..99bd984a3cbe28 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs @@ -281,8 +281,13 @@ public string Arguments /// On Windows, this is implemented using Job Objects with the /// JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE flag. /// + /// + /// On Linux and Android, this is implemented using prctl(PR_SET_PDEATHSIG). + /// /// /// to terminate the child process when the parent exits; otherwise, . The default is . + [SupportedOSPlatform("android")] + [SupportedOSPlatform("linux")] [SupportedOSPlatform("windows")] public bool KillOnParentExit { get; set; } diff --git a/src/libraries/System.Diagnostics.Process/tests/KillOnParentExitTests.cs b/src/libraries/System.Diagnostics.Process/tests/KillOnParentExitTests.cs index 34a100e7e80bbf..ba4e418ddad3a6 100644 --- a/src/libraries/System.Diagnostics.Process/tests/KillOnParentExitTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/KillOnParentExitTests.cs @@ -7,7 +7,7 @@ namespace System.Diagnostics.Tests { - [PlatformSpecific(TestPlatforms.Windows)] + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.Linux)] public class KillOnParentExitTests : ProcessTestBase { [Fact] diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Mobile.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Mobile.cs index f335f0b899a897..baa6808321c1e8 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Mobile.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Mobile.cs @@ -12,6 +12,19 @@ public class MobileProcessTests : ProcessTestBase { private const string NonExistentPath = "/nonexistent_path_for_testing_1234567890"; + [Fact] + [PlatformSpecific(TestPlatforms.Android)] + public void Process_Start_KillOnParentExit_ExitsSuccessfully() + { + using (Process process = Process.Start(new ProcessStartInfo("ls", Path.GetTempPath()) { KillOnParentExit = true })) + { + Assert.NotNull(process); + Assert.True(process.WaitForExit(WaitInMS)); + Assert.Equal(0, process.ExitCode); + Assert.True(process.HasExited); + } + } + [Fact] public void Process_Start_InheritedIO_ExitsSuccessfully() { diff --git a/src/native/libs/Common/pal_config.h.in b/src/native/libs/Common/pal_config.h.in index 58f883951f0397..c960a4cfbb9f24 100644 --- a/src/native/libs/Common/pal_config.h.in +++ b/src/native/libs/Common/pal_config.h.in @@ -16,6 +16,7 @@ #cmakedefine01 HAVE_FORK #cmakedefine01 HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP #cmakedefine01 HAVE_VFORK +#cmakedefine01 HAVE_PR_SET_PDEATHSIG #cmakedefine01 HAVE_CLOSE_RANGE #cmakedefine01 HAVE_FDWALK #cmakedefine01 HAVE_CHMOD diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index 6249f8821bce8d..51cc809e1f7d6a 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -44,6 +44,10 @@ #endif #include +#if HAVE_PR_SET_PDEATHSIG +#include +#endif + #if HAVE_SCHED_SETAFFINITY || HAVE_SCHED_GETAFFINITY #include #endif @@ -325,6 +329,196 @@ static void RestrictHandleInheritance(int32_t* inheritedFds, int32_t inheritedFd } } +// Forward declaration of the internal fork+exec function +static int32_t ForkAndExecProcessInternal( + const char* filename, char* const argv[], char* const envp[], const char* cwd, + int32_t setCredentials, uint32_t userId, uint32_t groupId, uint32_t* groups, int32_t groupsLength, + int32_t* childPid, int32_t stdinFd, int32_t stdoutFd, int32_t stderrFd, + int32_t* inheritedFds, int32_t inheritedFdCount, int32_t startDetached, int32_t applyPDeathSig); + +#if HAVE_PR_SET_PDEATHSIG +// Dedicated thread infrastructure for PR_SET_PDEATHSIG. +// +// On Linux, PR_SET_PDEATHSIG sends the death signal when the *thread* that called +// prctl exits, not when the process exits. To ensure the signal is sent only when +// the process truly exits, we use a long-lived dedicated thread that: +// 1. Performs fork+exec for each request where killOnParentExit is set. +// 2. Lives for the lifetime of the application. +// +// Because the forking thread does not exit until process exit, children forked from +// it can use PR_SET_PDEATHSIG so the signal is delivered when the process exits. + +typedef struct +{ + const char* filename; + char* const* argv; + char* const* envp; + const char* cwd; + int32_t setCredentials; + uint32_t userId; + uint32_t groupId; + uint32_t* groups; + int32_t groupsLength; + int32_t stdinFd; + int32_t stdoutFd; + int32_t stderrFd; + int32_t* inheritedFds; + int32_t inheritedFdCount; + int32_t startDetached; + + // Output + int32_t childPid; + int32_t result; + int32_t errnoValue; + int32_t done; +} PDeathSigForkRequest; + +static pthread_mutex_t s_pdeathsig_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t s_pdeathsig_request_cond = PTHREAD_COND_INITIALIZER; +static pthread_cond_t s_pdeathsig_done_cond = PTHREAD_COND_INITIALIZER; +static PDeathSigForkRequest* s_pdeathsig_request = NULL; +static bool s_pdeathsig_thread_started = false; + +static void* PDeathSigThreadFunc(void* arg) +{ + (void)arg; + + pthread_mutex_lock(&s_pdeathsig_mutex); + + while (1) + { + // Wait for a fork request + while (s_pdeathsig_request == NULL) + { + pthread_cond_wait(&s_pdeathsig_request_cond, &s_pdeathsig_mutex); + } + + PDeathSigForkRequest* req = s_pdeathsig_request; + + // Perform the fork+exec with applyPDeathSig=1 so prctl is called after fork in child + int32_t childPid = -1; + req->result = ForkAndExecProcessInternal( + req->filename, req->argv, req->envp, req->cwd, + req->setCredentials, req->userId, req->groupId, req->groups, req->groupsLength, + &childPid, req->stdinFd, req->stdoutFd, req->stderrFd, + req->inheritedFds, req->inheritedFdCount, req->startDetached, 1); + req->childPid = childPid; + req->errnoValue = errno; + + // Mark this request as done and clear the global slot. + // Use broadcast because multiple callers may be waiting: the submitter waits for + // its done flag, and other callers wait for the slot to become free. + req->done = 1; + s_pdeathsig_request = NULL; + pthread_cond_broadcast(&s_pdeathsig_done_cond); + } + + return NULL; +} + +// Must be called while s_pdeathsig_mutex is held. +static int EnsurePDeathSigThread(void) +{ + if (s_pdeathsig_thread_started) + { + return 0; + } + + pthread_t thread; + pthread_attr_t attr; + int result = pthread_attr_init(&attr); + if (result != 0) + { + errno = result; + return -1; + } + + result = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + if (result != 0) + { + pthread_attr_destroy(&attr); + errno = result; + return -1; + } + + result = pthread_create(&thread, &attr, PDeathSigThreadFunc, NULL); + pthread_attr_destroy(&attr); + + if (result != 0) + { + errno = result; + return -1; + } + + s_pdeathsig_thread_started = true; + return 0; +} + +static int32_t ForkAndExecOnPDeathSigThread( + const char* filename, char* const argv[], char* const envp[], const char* cwd, + int32_t setCredentials, uint32_t userId, uint32_t groupId, uint32_t* groups, int32_t groupsLength, + int32_t* childPid, int32_t stdinFd, int32_t stdoutFd, int32_t stderrFd, + int32_t* inheritedFds, int32_t inheritedFdCount, int32_t startDetached) +{ + PDeathSigForkRequest req; + req.filename = filename; + req.argv = argv; + req.envp = envp; + req.cwd = cwd; + req.setCredentials = setCredentials; + req.userId = userId; + req.groupId = groupId; + req.groups = groups; + req.groupsLength = groupsLength; + req.stdinFd = stdinFd; + req.stdoutFd = stdoutFd; + req.stderrFd = stderrFd; + req.inheritedFds = inheritedFds; + req.inheritedFdCount = inheritedFdCount; + req.startDetached = startDetached; + req.childPid = -1; + req.result = -1; + req.errnoValue = 0; + req.done = 0; + + pthread_mutex_lock(&s_pdeathsig_mutex); + + if (EnsurePDeathSigThread() != 0) + { + pthread_mutex_unlock(&s_pdeathsig_mutex); + *childPid = -1; + return -1; + } + + // Wait until no other request is being processed. This serializes + // concurrent callers so requests cannot overwrite each other. + while (s_pdeathsig_request != NULL) + { + pthread_cond_wait(&s_pdeathsig_done_cond, &s_pdeathsig_mutex); + } + + // Submit request and signal the dedicated thread + assert(s_pdeathsig_request == NULL); + s_pdeathsig_request = &req; + pthread_cond_signal(&s_pdeathsig_request_cond); + + // Wait for the dedicated thread to complete OUR fork+exec. + // We check req.done (not s_pdeathsig_request) so that a concurrent + // caller submitting the next request doesn't prevent us from seeing + // that our request has already completed. + while (!req.done) + { + pthread_cond_wait(&s_pdeathsig_done_cond, &s_pdeathsig_mutex); + } + + pthread_mutex_unlock(&s_pdeathsig_mutex); + + *childPid = req.childPid; + errno = req.errnoValue; + return req.result; +} +#endif // HAVE_PR_SET_PDEATHSIG + int32_t SystemNative_ForkAndExecProcess(const char* filename, char* const argv[], char* const envp[], @@ -340,12 +534,43 @@ int32_t SystemNative_ForkAndExecProcess(const char* filename, int32_t stderrFd, int32_t* inheritedFds, int32_t inheritedFdCount, - int32_t startDetached) + int32_t startDetached, + int32_t killOnParentExit) +{ +#if HAVE_PR_SET_PDEATHSIG + if (killOnParentExit) + { + return ForkAndExecOnPDeathSigThread( + filename, argv, envp, cwd, + setCredentials, userId, groupId, groups, groupsLength, + childPid, stdinFd, stdoutFd, stderrFd, + inheritedFds, inheritedFdCount, startDetached); + } +#else + (void)killOnParentExit; +#endif + + return ForkAndExecProcessInternal( + filename, argv, envp, cwd, + setCredentials, userId, groupId, groups, groupsLength, + childPid, stdinFd, stdoutFd, stderrFd, + inheritedFds, inheritedFdCount, startDetached, 0); +} + +static int32_t ForkAndExecProcessInternal( + const char* filename, char* const argv[], char* const envp[], const char* cwd, + int32_t setCredentials, uint32_t userId, uint32_t groupId, uint32_t* groups, int32_t groupsLength, + int32_t* childPid, int32_t stdinFd, int32_t stdoutFd, int32_t stderrFd, + int32_t* inheritedFds, int32_t inheritedFdCount, int32_t startDetached, int32_t applyPDeathSig) { #if HAVE_FORK || defined(TARGET_OSX) || defined(TARGET_MACCATALYST) assert(NULL != filename && NULL != argv && NULL != envp && NULL != childPid && (groupsLength == 0 || groups != NULL) && "null argument."); +#if !HAVE_PR_SET_PDEATHSIG + (void)applyPDeathSig; +#endif + *childPid = -1; // Make sure we can find and access the executable. exec will do this, of course, but at that point it's already @@ -513,6 +738,11 @@ int32_t SystemNative_ForkAndExecProcess(const char* filename, uint32_t* getGroupsBuffer = NULL; sigset_t signal_set; sigset_t old_signal_set; +#if HAVE_PR_SET_PDEATHSIG + // Capture the parent PID before fork so the child can verify it hasn't been + // reparented (e.g., to a subreaper) between fork and prctl. + pid_t expectedParentPid = getpid(); +#endif #if HAVE_PTHREAD_SETCANCELSTATE int thread_cancel_state; @@ -670,6 +900,27 @@ int32_t SystemNative_ForkAndExecProcess(const char* filename, RestrictHandleInheritance(inheritedFds, inheritedFdCount); } +#if HAVE_PR_SET_PDEATHSIG + if (applyPDeathSig) + { + // Set the parent death signal on the child process. When the parent thread + // that forked this child exits, SIGKILL will be sent to this child. + // We fork from a dedicated long-lived thread to ensure the signal is only + // sent when the process (not an arbitrary thread) exits. + if (prctl(PR_SET_PDEATHSIG, (unsigned long)SIGKILL, 0, 0, 0) == -1) + { + ExitChild(waitForChildToExecPipe[WRITE_END_OF_PIPE], errno); + } + + // If the parent died between fork and prctl, PR_SET_PDEATHSIG will not deliver the signal. + // Detect this by checking if the child process has been reparented. + if (getppid() != expectedParentPid) + { + ExitChild(waitForChildToExecPipe[WRITE_END_OF_PIPE], ESRCH); + } + } +#endif + // Finally, execute the new process. execve will not return if it's successful. execve(filename, argv, envp); ExitChild(waitForChildToExecPipe[WRITE_END_OF_PIPE], errno); // execve failed @@ -752,6 +1003,7 @@ done:; (void)inheritedFds; (void)inheritedFdCount; (void)startDetached; + (void)applyPDeathSig; return -1; #endif } diff --git a/src/native/libs/System.Native/pal_process.h b/src/native/libs/System.Native/pal_process.h index abcc7d87439cd7..1bf5ef32f04bff 100644 --- a/src/native/libs/System.Native/pal_process.h +++ b/src/native/libs/System.Native/pal_process.h @@ -34,7 +34,8 @@ PALEXPORT int32_t SystemNative_ForkAndExecProcess( int32_t stderrFd, // the fd for the child's stderr int32_t* inheritedFds, // array of fds to explicitly inherit (-1 to disable restriction) int32_t inheritedFdCount, // count of fds in inheritedFds; -1 means no restriction - int32_t startDetached); // whether to start the process as a leader of a new session + int32_t startDetached, // whether to start the process as a leader of a new session + int32_t killOnParentExit); // whether to kill the child when the parent exits /************ * The values below in the header are fixed and correct for managed callers to use forever. diff --git a/src/native/libs/System.Native/pal_process_wasi.c b/src/native/libs/System.Native/pal_process_wasi.c index 1ad9f888bbc04c..4aeb370054e856 100644 --- a/src/native/libs/System.Native/pal_process_wasi.c +++ b/src/native/libs/System.Native/pal_process_wasi.c @@ -32,7 +32,8 @@ int32_t SystemNative_ForkAndExecProcess(const char* filename, int32_t stderrFd, int32_t* inheritedFds, int32_t inheritedFdCount, - int32_t startDetached) + int32_t startDetached, + int32_t killOnParentExit) { return -1; } diff --git a/src/native/libs/configure.cmake b/src/native/libs/configure.cmake index ba504df4834918..87a37cca4af0b3 100644 --- a/src/native/libs/configure.cmake +++ b/src/native/libs/configure.cmake @@ -196,6 +196,11 @@ check_symbol_exists( unistd.h HAVE_VFORK) +check_symbol_exists( + PR_SET_PDEATHSIG + "sys/prctl.h" + HAVE_PR_SET_PDEATHSIG) + check_symbol_exists( pipe unistd.h From 392b6dd8d3ce5c13d9027b8886f485a674144b23 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 16:19:06 -0700 Subject: [PATCH 095/115] Rename area label from `area-assemblyloading` to `area-AssemblyLoader` (#127472) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames the `area-assemblyloading` label to `area-AssemblyLoader` to align with the casing convention used by other area labels (e.g., `area-AssemblyLoader-mono`). ## Description - `.github/policies/resourceManagement.yml` — updated `labelAdded` trigger and `hasLabel` condition - `docs/area-owners.md` — updated area label column - `.github/skills/issue-triage/references/area-label-heuristics.md` — updated heuristics table entry --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: agocke <515774+agocke@users.noreply.github.com> --- .github/policies/resourceManagement.yml | 4 ++-- .../skills/issue-triage/references/area-label-heuristics.md | 2 +- docs/area-owners.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/policies/resourceManagement.yml b/.github/policies/resourceManagement.yml index 781a60182c05ef..96a126f70ecf18 100644 --- a/.github/policies/resourceManagement.yml +++ b/.github/policies/resourceManagement.yml @@ -123,7 +123,7 @@ configuration: - payloadType: Pull_Request - or: - labelAdded: - label: area-assemblyloading + label: area-AssemblyLoader - labelAdded: label: area-AssemblyLoader-mono - labelAdded: @@ -335,7 +335,7 @@ configuration: then: - if: - hasLabel: - label: area-assemblyloading + label: area-AssemblyLoader then: - mentionUsers: mentionees: diff --git a/.github/skills/issue-triage/references/area-label-heuristics.md b/.github/skills/issue-triage/references/area-label-heuristics.md index f9132319f84dd7..8b55bdbbde6306 100644 --- a/.github/skills/issue-triage/references/area-label-heuristics.md +++ b/.github/skills/issue-triage/references/area-label-heuristics.md @@ -54,7 +54,7 @@ with the authoritative [`docs/area-owners.md`](../../../../docs/area-owners.md). | JIT, code generation, inlining, tiered compilation | `area-CodeGen-coreclr` | | NativeAOT, ahead-of-time compilation | `area-NativeAOT-coreclr` | | Crossgen2, R2R, ReadyToRun, R2RDump | `area-ReadyToRun` | -| Assembly loading, AssemblyLoadContext | `area-assemblyloading` | +| Assembly loading, AssemblyLoadContext | `area-AssemblyLoader` | | Host, `dotnet` executable, hostfxr, hostpolicy, Host model | `area-Host` | | Interop, COM, P/Invoke, marshalling (runtime) | `area-Interop-coreclr` | | Single-file deployment | `area-Single-File` | diff --git a/docs/area-owners.md b/docs/area-owners.md index 8dd6c39785b59f..e07865f42dd3ce 100644 --- a/docs/area-owners.md +++ b/docs/area-owners.md @@ -12,7 +12,7 @@ Note: Editing this file doesn't update the mapping used by `@dotnet-policy-servi | Area | Lead | Owners (area experts to tag in PRs and issues) | Notes | |------------------------------------------------|----------------------|------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| area-assemblyloading | @agocke | @agocke @elinor-fung | | +| area-AssemblyLoader | @agocke | @agocke @elinor-fung | | | area-AssemblyLoader-mono | @agocke | @agocke @elinor-fung | | | area-Build-mono | @lewing | @akoeplinger | | | area-Codeflow | @dotnet/dnr-codeflow | @dotnet/dnr-codeflow | Used for automated PRs that ingest code from other repos | From 95795627cd854ffe350b576b3c383c6f56e93ed5 Mon Sep 17 00:00:00 2001 From: Rachel Jarvi Date: Mon, 4 May 2026 16:38:14 -0700 Subject: [PATCH 096/115] [cDAC] Changing loader heaps from string-keyed to enum-keyed dict (#127474) Follow-up to https://github.com/dotnet/runtime/pull/127296#discussion_r3138376804 --- docs/design/datacontracts/Loader.md | 41 +++++++---- .../Contracts/ILoader.cs | 17 ++++- .../Contracts/Loader_1.cs | 24 +++---- .../SOSDacImpl.cs | 68 +++++++++---------- src/native/managed/cdac/tests/LoaderTests.cs | 35 +++++----- 5 files changed, 106 insertions(+), 79 deletions(-) diff --git a/docs/design/datacontracts/Loader.md b/docs/design/datacontracts/Loader.md index ee3ef4f1b30eea..8e9d6ccb6f98d9 100644 --- a/docs/design/datacontracts/Loader.md +++ b/docs/design/datacontracts/Loader.md @@ -59,6 +59,21 @@ readonly struct LoaderHeapBlockData TargetNUInt Size { get; init; } TargetPointer NextBlock { get; init; } } + +enum LoaderAllocatorHeapType +{ + Unknown, + LowFrequencyHeap, + HighFrequencyHeap, + StaticsHeap, + StubHeap, + ExecutableHeap, + FixupPrecodeHeap, + NewStubPrecodeHeap, + DynamicHelpersStubHeap, + IndcellHeap, + CacheEntryHeap, +} ``` ``` csharp @@ -105,7 +120,7 @@ TargetPointer GetDynamicIL(ModuleHandle handle, uint token); TargetPointer GetFirstLoaderHeapBlock(TargetPointer loaderHeap); // Returns the data for the given loader heap block (address, size, and next block pointer). LoaderHeapBlockData GetLoaderHeapBlockData(TargetPointer block); -IReadOnlyDictionary GetLoaderAllocatorHeaps(TargetPointer loaderAllocatorPointer); +IReadOnlyDictionary GetLoaderAllocatorHeaps(TargetPointer loaderAllocatorPointer); DebuggerAssemblyControlFlags GetDebuggerInfoBits(ModuleHandle handle); void SetDebuggerInfoBits(ModuleHandle handle, DebuggerAssemblyControlFlags newBits); @@ -784,39 +799,39 @@ TargetPointer GetObjectHandle(TargetPointer loaderAllocatorPointer) return target.ReadPointer(loaderAllocatorPointer + /* LoaderAllocator::ObjectHandle offset */); } -IReadOnlyDictionary GetLoaderAllocatorHeaps(TargetPointer loaderAllocatorPointer) +IReadOnlyDictionary GetLoaderAllocatorHeaps(TargetPointer loaderAllocatorPointer) { // Read LoaderAllocator data LoaderAllocator la = // read LoaderAllocator object at loaderAllocatorPointer // Always-present heaps - Dictionary heaps = { - ["LowFrequencyHeap"] = la.LowFrequencyHeap, - ["HighFrequencyHeap"] = la.HighFrequencyHeap, - ["StaticsHeap"] = la.StaticsHeap, - ["StubHeap"] = la.StubHeap, - ["ExecutableHeap"] = la.ExecutableHeap, + Dictionary heaps = { + [LoaderAllocatorHeapType.LowFrequencyHeap] = la.LowFrequencyHeap, + [LoaderAllocatorHeapType.HighFrequencyHeap] = la.HighFrequencyHeap, + [LoaderAllocatorHeapType.StaticsHeap] = la.StaticsHeap, + [LoaderAllocatorHeapType.StubHeap] = la.StubHeap, + [LoaderAllocatorHeapType.ExecutableHeap] = la.ExecutableHeap, }; // Feature-conditional heaps: only included when the data descriptor field exists if (LoaderAllocator type has "FixupPrecodeHeap" field) - heaps["FixupPrecodeHeap"] = la.FixupPrecodeHeap; + heaps[LoaderAllocatorHeapType.FixupPrecodeHeap] = la.FixupPrecodeHeap; if (LoaderAllocator type has "NewStubPrecodeHeap" field) - heaps["NewStubPrecodeHeap"] = la.NewStubPrecodeHeap; + heaps[LoaderAllocatorHeapType.NewStubPrecodeHeap] = la.NewStubPrecodeHeap; if (LoaderAllocator type has "DynamicHelpersStubHeap" field) - heaps["DynamicHelpersStubHeap"] = la.DynamicHelpersStubHeap; + heaps[LoaderAllocatorHeapType.DynamicHelpersStubHeap] = la.DynamicHelpersStubHeap; // VirtualCallStubManager heaps: only included when VirtualCallStubManager is non-null if (la.VirtualCallStubManager != null) { VirtualCallStubManager vcsMgr = // read VirtualCallStubManager object at la.VirtualCallStubManager - heaps["IndcellHeap"] = vcsMgr.IndcellHeap; + heaps[LoaderAllocatorHeapType.IndcellHeap] = vcsMgr.IndcellHeap; if (VirtualCallStubManager type has "CacheEntryHeap" field) - heaps["CacheEntryHeap"] = vcsMgr.CacheEntryHeap; + heaps[LoaderAllocatorHeapType.CacheEntryHeap] = vcsMgr.CacheEntryHeap; } return heaps; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ILoader.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ILoader.cs index 76a07d7753696f..cdf2b16e1d5899 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ILoader.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ILoader.cs @@ -44,6 +44,21 @@ public enum ClrModifiableAssemblies : uint Debug = 2, } +public enum LoaderAllocatorHeapType +{ + Unknown, + LowFrequencyHeap, + HighFrequencyHeap, + StaticsHeap, + StubHeap, + ExecutableHeap, + FixupPrecodeHeap, + NewStubPrecodeHeap, + DynamicHelpersStubHeap, + IndcellHeap, + CacheEntryHeap, +} + [Flags] public enum AssemblyIterationFlags { @@ -130,7 +145,7 @@ public interface ILoader : IContract TargetPointer GetFirstLoaderHeapBlock(TargetPointer loaderHeap) => throw new NotImplementedException(); // Returns the data for the given loader heap block (address, size, and next block pointer). LoaderHeapBlockData GetLoaderHeapBlockData(TargetPointer block) => throw new NotImplementedException(); - IReadOnlyDictionary GetLoaderAllocatorHeaps(TargetPointer loaderAllocatorPointer) => throw new NotImplementedException(); + IReadOnlyDictionary GetLoaderAllocatorHeaps(TargetPointer loaderAllocatorPointer) => throw new NotImplementedException(); DebuggerAssemblyControlFlags GetDebuggerInfoBits(ModuleHandle handle) => throw new NotImplementedException(); void SetDebuggerInfoBits(ModuleHandle handle, DebuggerAssemblyControlFlags newBits) => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs index 36dd018aae0e02..36c59a7829972f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs @@ -697,38 +697,38 @@ LoaderHeapBlockData ILoader.GetLoaderHeapBlockData(TargetPointer block) }; } - IReadOnlyDictionary ILoader.GetLoaderAllocatorHeaps(TargetPointer loaderAllocatorPointer) + IReadOnlyDictionary ILoader.GetLoaderAllocatorHeaps(TargetPointer loaderAllocatorPointer) { Data.LoaderAllocator loaderAllocator = _target.ProcessedData.GetOrAdd(loaderAllocatorPointer); Target.TypeInfo laType = _target.GetTypeInfo(DataType.LoaderAllocator); - Dictionary heaps = new() + Dictionary heaps = new() { - [nameof(Data.LoaderAllocator.LowFrequencyHeap)] = loaderAllocator.LowFrequencyHeap, - [nameof(Data.LoaderAllocator.HighFrequencyHeap)] = loaderAllocator.HighFrequencyHeap, - [nameof(Data.LoaderAllocator.StaticsHeap)] = loaderAllocator.StaticsHeap, - [nameof(Data.LoaderAllocator.StubHeap)] = loaderAllocator.StubHeap, - [nameof(Data.LoaderAllocator.ExecutableHeap)] = loaderAllocator.ExecutableHeap, + [LoaderAllocatorHeapType.LowFrequencyHeap] = loaderAllocator.LowFrequencyHeap, + [LoaderAllocatorHeapType.HighFrequencyHeap] = loaderAllocator.HighFrequencyHeap, + [LoaderAllocatorHeapType.StaticsHeap] = loaderAllocator.StaticsHeap, + [LoaderAllocatorHeapType.StubHeap] = loaderAllocator.StubHeap, + [LoaderAllocatorHeapType.ExecutableHeap] = loaderAllocator.ExecutableHeap, }; if (laType.Fields.ContainsKey(nameof(Data.LoaderAllocator.FixupPrecodeHeap))) - heaps[nameof(Data.LoaderAllocator.FixupPrecodeHeap)] = loaderAllocator.FixupPrecodeHeap!.Value; + heaps[LoaderAllocatorHeapType.FixupPrecodeHeap] = loaderAllocator.FixupPrecodeHeap!.Value; if (laType.Fields.ContainsKey(nameof(Data.LoaderAllocator.NewStubPrecodeHeap))) - heaps[nameof(Data.LoaderAllocator.NewStubPrecodeHeap)] = loaderAllocator.NewStubPrecodeHeap!.Value; + heaps[LoaderAllocatorHeapType.NewStubPrecodeHeap] = loaderAllocator.NewStubPrecodeHeap!.Value; if (laType.Fields.ContainsKey(nameof(Data.LoaderAllocator.DynamicHelpersStubHeap))) - heaps[nameof(Data.LoaderAllocator.DynamicHelpersStubHeap)] = loaderAllocator.DynamicHelpersStubHeap!.Value; + heaps[LoaderAllocatorHeapType.DynamicHelpersStubHeap] = loaderAllocator.DynamicHelpersStubHeap!.Value; if (loaderAllocator.VirtualCallStubManager != TargetPointer.Null) { Data.VirtualCallStubManager vcsMgr = _target.ProcessedData.GetOrAdd(loaderAllocator.VirtualCallStubManager); Target.TypeInfo vcsType = _target.GetTypeInfo(DataType.VirtualCallStubManager); - heaps[nameof(Data.VirtualCallStubManager.IndcellHeap)] = vcsMgr.IndcellHeap; + heaps[LoaderAllocatorHeapType.IndcellHeap] = vcsMgr.IndcellHeap; if (vcsType.Fields.ContainsKey(nameof(Data.VirtualCallStubManager.CacheEntryHeap))) - heaps[nameof(Data.VirtualCallStubManager.CacheEntryHeap)] = vcsMgr.CacheEntryHeap!.Value; + heaps[LoaderAllocatorHeapType.CacheEntryHeap] = vcsMgr.CacheEntryHeap!.Value; } return heaps; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs index 8a909f25f5ebc7..5154540fd0c6c1 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -6326,28 +6326,28 @@ int ISOSDacInterface13.GetDomainLoaderAllocator(ClrDataAddress domainAddress, Cl // Static ANSI string pointers for all known heap names, in the canonical order matching // LoaderAllocatorLoaderHeapNames in request.cpp. These are process-lifetime allocations, // equivalent to static const char* literals in C++. - private static readonly (string Name, nint AnsiPtr)[] s_heapNameEntries = InitializeHeapNameEntries(); - private (string Name, nint AnsiPtr)[]? _filteredHeapNameEntries; + private static readonly (LoaderAllocatorHeapType HeapType, nint AnsiPtr)[] s_heapNameEntries = InitializeHeapNameEntries(); + private (LoaderAllocatorHeapType HeapType, nint AnsiPtr)[]? _filteredHeapNameEntries; - private static (string Name, nint AnsiPtr)[] InitializeHeapNameEntries() + private static (LoaderAllocatorHeapType heapType, nint AnsiPtr)[] InitializeHeapNameEntries() { // Order must match LoaderAllocatorLoaderHeapNames in src/coreclr/debug/daccess/request.cpp - string[] names = + LoaderAllocatorHeapType[] heapTypes = [ - "LowFrequencyHeap", - "HighFrequencyHeap", - "StaticsHeap", - "StubHeap", - "ExecutableHeap", - "FixupPrecodeHeap", - "NewStubPrecodeHeap", - "DynamicHelpersStubHeap", - "IndcellHeap", - "CacheEntryHeap", + LoaderAllocatorHeapType.LowFrequencyHeap, + LoaderAllocatorHeapType.HighFrequencyHeap, + LoaderAllocatorHeapType.StaticsHeap, + LoaderAllocatorHeapType.StubHeap, + LoaderAllocatorHeapType.ExecutableHeap, + LoaderAllocatorHeapType.FixupPrecodeHeap, + LoaderAllocatorHeapType.NewStubPrecodeHeap, + LoaderAllocatorHeapType.DynamicHelpersStubHeap, + LoaderAllocatorHeapType.IndcellHeap, + LoaderAllocatorHeapType.CacheEntryHeap ]; - var entries = new (string, nint)[names.Length]; - for (int i = 0; i < names.Length; i++) - entries[i] = (names[i], Marshal.StringToHGlobalAnsi(names[i])); + var entries = new (LoaderAllocatorHeapType, nint)[heapTypes.Length]; + for (int i = 0; i < heapTypes.Length; i++) + entries[i] = (heapTypes[i], Marshal.StringToHGlobalAnsi(heapTypes[i].ToString())); return entries; } @@ -6355,7 +6355,7 @@ private static (string Name, nint AnsiPtr)[] InitializeHeapNameEntries() // data descriptor fields exist. This mirrors the DAC's compile-time // LoaderAllocatorLoaderHeapNames array and ensures a fixed count/ordering // regardless of per-loader-allocator runtime state (e.g. VCS manager being null). - private (string Name, nint AnsiPtr)[] GetFilteredHeapNameEntries() + private (LoaderAllocatorHeapType HeapType, nint AnsiPtr)[] GetFilteredHeapNameEntries() { if (_filteredHeapNameEntries is not null) return _filteredHeapNameEntries; @@ -6363,12 +6363,12 @@ private static (string Name, nint AnsiPtr)[] InitializeHeapNameEntries() Target.TypeInfo laType = _target.GetTypeInfo(DataType.LoaderAllocator); Target.TypeInfo vcsType = _target.GetTypeInfo(DataType.VirtualCallStubManager); - var entries = new List<(string Name, nint AnsiPtr)>(); + var entries = new List<(LoaderAllocatorHeapType HeapType, nint AnsiPtr)>(); foreach (var entry in s_heapNameEntries) { - bool include = entry.Name is "IndcellHeap" or "CacheEntryHeap" - ? vcsType.Fields.ContainsKey(entry.Name) - : laType.Fields.ContainsKey(entry.Name); + bool include = entry.HeapType is LoaderAllocatorHeapType.IndcellHeap or LoaderAllocatorHeapType.CacheEntryHeap + ? vcsType.Fields.ContainsKey(entry.HeapType.ToString()) + : laType.Fields.ContainsKey(entry.HeapType.ToString()); if (include) entries.Add(entry); } @@ -6427,15 +6427,13 @@ int ISOSDacInterface13.GetLoaderAllocatorHeapNames(int count, char** ppNames, in } int ISOSDacInterface13.GetLoaderAllocatorHeaps(ClrDataAddress loaderAllocator, int count, ClrDataAddress* pLoaderHeaps, /*LoaderHeapKind*/ int* pKinds, int* pNeeded) { - if (loaderAllocator == 0) - return HResults.E_INVALIDARG; - int hr = HResults.S_OK; try { + if (loaderAllocator == 0) + throw new ArgumentException("loaderAllocator cannot be zero.", nameof(loaderAllocator)); Contracts.ILoader contract = _target.Contracts.Loader; - IReadOnlyDictionary heaps = contract.GetLoaderAllocatorHeaps(loaderAllocator.ToTargetPointer(_target)); - + IReadOnlyDictionary heaps = contract.GetLoaderAllocatorHeaps(loaderAllocator.ToTargetPointer(_target)); var filteredEntries = GetFilteredHeapNameEntries(); int loaderHeapCount = filteredEntries.Length; @@ -6446,17 +6444,14 @@ int ISOSDacInterface13.GetLoaderAllocatorHeaps(ClrDataAddress loaderAllocator, i { if (count < loaderHeapCount) { - hr = HResults.E_INVALIDARG; + throw new ArgumentException($"The count parameter ({count}) is less than the number of loader heaps ({loaderHeapCount}).", nameof(count)); } - else + for (int i = 0; i < loaderHeapCount; i++) { - for (int i = 0; i < loaderHeapCount; i++) - { - pLoaderHeaps[i] = heaps.TryGetValue(filteredEntries[i].Name, out TargetPointer heapAddr) - ? heapAddr.ToClrDataAddress(_target) - : 0; - pKinds[i] = 0; // LoaderHeapKindNormal - } + pLoaderHeaps[i] = heaps.TryGetValue(filteredEntries[i].HeapType, out TargetPointer heapAddr) + ? heapAddr.ToClrDataAddress(_target) + : 0; + pKinds[i] = 0; // LoaderHeapKindNormal } } } @@ -6490,6 +6485,7 @@ int ISOSDacInterface13.GetLoaderAllocatorHeaps(ClrDataAddress loaderAllocator, i #endif return hr; } + int ISOSDacInterface13.GetHandleTableMemoryRegions(DacComNullableByRef ppEnum) { int hr = HResults.S_OK; diff --git a/src/native/managed/cdac/tests/LoaderTests.cs b/src/native/managed/cdac/tests/LoaderTests.cs index cd3b2be5f8cc8e..1af03c11710887 100644 --- a/src/native/managed/cdac/tests/LoaderTests.cs +++ b/src/native/managed/cdac/tests/LoaderTests.cs @@ -159,19 +159,21 @@ public void GetSimpleName_InvalidUtf8(MockTarget.Architecture arch) Assert.Throws(() => contract.GetSimpleName(handle)); } - private static readonly Dictionary MockHeapDictionary = new() - { - ["LowFrequencyHeap"] = new(0x1000), - ["HighFrequencyHeap"] = new(0x2000), - ["StaticsHeap"] = new(0x3000), - ["StubHeap"] = new(0x4000), - ["ExecutableHeap"] = new(0x5000), - ["FixupPrecodeHeap"] = new(0x6000), - ["NewStubPrecodeHeap"] = new(0x7000), - ["IndcellHeap"] = new(0x8000), - ["CacheEntryHeap"] = new(0x9000), + private static readonly Dictionary MockHeapDictionary = new() + { + [LoaderAllocatorHeapType.LowFrequencyHeap] = new(0x1000), + [LoaderAllocatorHeapType.HighFrequencyHeap] = new(0x2000), + [LoaderAllocatorHeapType.StaticsHeap] = new(0x3000), + [LoaderAllocatorHeapType.StubHeap] = new(0x4000), + [LoaderAllocatorHeapType.ExecutableHeap] = new(0x5000), + [LoaderAllocatorHeapType.FixupPrecodeHeap] = new(0x6000), + [LoaderAllocatorHeapType.NewStubPrecodeHeap] = new(0x7000), + [LoaderAllocatorHeapType.IndcellHeap] = new(0x8000), + [LoaderAllocatorHeapType.CacheEntryHeap] = new(0x9000), }; + private static LoaderAllocatorHeapType HeapNameToType(string name) => Enum.Parse(name); + private static ISOSDacInterface13 CreateSOSDacInterface13ForHeapTests(MockTarget.Architecture arch) { var targetBuilder = new TestPlaceholderTarget.Builder(arch); @@ -206,7 +208,7 @@ private static ISOSDacInterface13 CreateSOSDacInterface13ForHeapTests(MockTarget var target = targetBuilder .AddTypes(types) .AddMockContract(Mock.Of( - l => l.GetLoaderAllocatorHeaps(It.IsAny()) == (IReadOnlyDictionary)MockHeapDictionary + l => l.GetLoaderAllocatorHeaps(It.IsAny()) == (IReadOnlyDictionary)MockHeapDictionary && l.GetGlobalLoaderAllocator() == new TargetPointer(0x100))) .Build(); return new SOSDacImpl(target, null); @@ -240,11 +242,10 @@ public void GetLoaderAllocatorHeapNames_GetNames(MockTarget.Architecture arch) Assert.Equal(HResults.S_OK, hr); Assert.Equal(MockHeapDictionary.Count, needed); - HashSet expectedNames = new(MockHeapDictionary.Keys); for (int i = 0; i < needed; i++) { string actual = Marshal.PtrToStringAnsi((nint)names[i])!; - Assert.Contains(actual, expectedNames); + Assert.Contains(HeapNameToType(actual), MockHeapDictionary.Keys); } } @@ -260,11 +261,10 @@ public void GetLoaderAllocatorHeapNames_InsufficientBuffer(MockTarget.Architectu Assert.Equal(HResults.S_FALSE, hr); Assert.Equal(MockHeapDictionary.Count, needed); - HashSet expectedNames = new(MockHeapDictionary.Keys); for (int i = 0; i < 2; i++) { string actual = Marshal.PtrToStringAnsi((nint)names[i])!; - Assert.Contains(actual, expectedNames); + Assert.Contains(HeapNameToType(actual), MockHeapDictionary.Keys); } } @@ -312,7 +312,8 @@ public void GetLoaderAllocatorHeaps_GetHeaps(MockTarget.Architecture arch) for (int i = 0; i < needed; i++) { string name = Marshal.PtrToStringAnsi((nint)names[i])!; - Assert.Equal((ulong)MockHeapDictionary[name], (ulong)heaps[i]); + LoaderAllocatorHeapType heapType = HeapNameToType(name); + Assert.Equal((ulong)MockHeapDictionary[heapType], (ulong)heaps[i]); Assert.Equal(0, kinds[i]); // LoaderHeapKindNormal } } From 2e59c91385316fed8f731f1bed20b0181b4afc17 Mon Sep 17 00:00:00 2001 From: Juan Hoyos <19413848+hoyosjs@users.noreply.github.com> Date: Mon, 4 May 2026 17:09:04 -0700 Subject: [PATCH 097/115] Fix stale lastThrownObjectHandle in DAC GetThreadData during active exception dispatch (#127741) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After #127300 removed ExInfo::m_hThrowable and SetThrowable(), Thread::m_LastThrownObjectHandle is no longer updated during active exception dispatch. This causes a staleclastThrownObjectHandle to ge returned by GetThreadData when a debugger breaks into the target mid-dispatch. When debugging a .NET 11 process with SOS (e.g. via dotnet-dump or WinDbg), the following commands fail if the debuggee is stopped during exception dispatch (for example, with SXE CLR): - !PrintException — reports "Invalid exception object" because it dereferences the stale handle and gets a null or recycled pointer - !Threads — shows no exception column for threads that are actively throwing - !clrstack -a with exception context — may show incomplete or missing exception info --- docs/design/datacontracts/Thread.md | 27 +++++++-- src/coreclr/debug/daccess/request.cpp | 15 ++++- .../Contracts/Thread_1.cs | 33 ++++++++--- .../MockDescriptors/MockDescriptors.Thread.cs | 6 ++ src/native/managed/cdac/tests/ThreadTests.cs | 56 +++++++++++++++++++ 5 files changed, 124 insertions(+), 13 deletions(-) diff --git a/docs/design/datacontracts/Thread.md b/docs/design/datacontracts/Thread.md index 6dc9596712f18c..8d9761703567db 100644 --- a/docs/design/datacontracts/Thread.md +++ b/docs/design/datacontracts/Thread.md @@ -190,6 +190,23 @@ ThreadData GetThreadData(TargetPointer address) allocContextLimit = target.ReadPointer(threadLocals + /* RuntimeThreadLocals::AllocContext offset */ + /* GCAllocContext::Limit offset */); } + // Prefer the active exception from ExInfo (pseudo-handle to m_exception field). + // After the removal of SetThrowable/m_hThrowable, m_LastThrownObjectHandle is only + // updated after exception dispatch completes, so during active dispatch it may be stale. + TargetPointer lastThrownObjectHandle = TargetPointer.Null; + if (exceptionTrackerAddr != TargetPointer.Null) + { + TargetPointer thrownObject = target.ReadPointer(exceptionTrackerAddr + /* ExceptionInfo::ThrownObject offset */); + if (thrownObject != TargetPointer.Null) + { + lastThrownObjectHandle = exceptionTrackerAddr + /* ExceptionInfo::ThrownObject field offset */; + } + } + if (lastThrownObjectHandle == TargetPointer.Null) + { + lastThrownObjectHandle = target.ReadPointer(address + /* Thread::LastThrownObject offset */); + } + ulong threadLinkoffset = ... // offset from Thread data descriptor return new ThreadData( Id: target.Read(address + /* Thread::Id offset */), @@ -199,7 +216,7 @@ ThreadData GetThreadData(TargetPointer address) AllocContextPointer: allocContextPointer, AllocContextLimit: allocContextLimit, Frame: target.ReadPointer(address + /* Thread::Frame offset */), - LastThrownObjectHandle : target.ReadPointer(address + /* Thread::LastThrownObject offset */), + LastThrownObjectHandle : lastThrownObjectHandle, FirstNestedException : firstNestedException, NextThread: target.ReadPointer(address + /* Thread::LinkNext offset */) - threadLinkOffset; ); @@ -285,12 +302,14 @@ TargetPointer IThread.GetCurrentExceptionHandle(TargetPointer threadPointer) TargetPointer exceptionTrackerPtr = target.ReadPointer(threadPointer + /*Thread::ExceptionTracker offset */); if (exceptionTrackerPtr == TargetPointer.Null) return TargetPointer.Null; - TargetPointer thrownObjectHandle = target.ReadPointer(exceptionTrackerPtr + /* ExceptionInfo::ThrownObjectHandle offset */); + TargetPointer thrownObject = target.ReadPointer(exceptionTrackerPtr + /* ExceptionInfo::ThrownObject offset */); - if (thrownObjectHandle == TargetPointer.Null || target.ReadPointer(thrownObjectHandle) == TargetPointer.Null) + if (thrownObject == TargetPointer.Null) return TargetPointer.Null; - return thrownObjectHandle; + // Return the address of the ThrownObject field as a pseudo-handle. + // Callers dereference this address to read the exception Object*. + return exceptionTrackerPtr + /* ExceptionInfo::ThrownObject field offset */; } byte[] IThread.GetWatsonBuckets(TargetPointer threadPointer) diff --git a/src/coreclr/debug/daccess/request.cpp b/src/coreclr/debug/daccess/request.cpp index c68ee4c8530fd4..2a1cb9f719617e 100644 --- a/src/coreclr/debug/daccess/request.cpp +++ b/src/coreclr/debug/daccess/request.cpp @@ -893,8 +893,19 @@ HRESULT ClrDataAccess::GetThreadData(CLRDATA_ADDRESS threadAddr, struct DacpThre // TEB is no longer provided by the runtime. Consumers should look up the TEB // from the OS thread ID via the debugger's native API (e.g., IDebuggerServices::GetThreadTeb). threadData->teb = (CLRDATA_ADDRESS)NULL; - threadData->lastThrownObjectHandle = - TO_CDADDR(thread->m_LastThrownObjectHandle); + // Prefer the active exception from ExInfo (pseudo-handle to m_exception field). + // After the removal of SetThrowable/m_hThrowable, m_LastThrownObjectHandle is only + // updated after exception dispatch completes, so during active dispatch it may be + // stale. GetThrowableAsPseudoHandle returns the address of ExInfo::m_exception + // which has the same dereference semantics as a real GC handle. + { + OBJECTHANDLE ohException = thread->GetThrowableAsPseudoHandle(); + if (ohException == (OBJECTHANDLE)NULL) + { + ohException = thread->m_LastThrownObjectHandle; + } + threadData->lastThrownObjectHandle = TO_CDADDR(ohException); + } threadData->nextThread = HOST_CDADDR(ThreadStore::s_pThreadStore->m_ThreadList.GetNext(thread)); if (thread->m_ExceptionState.m_pCurrentTracker) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs index 289ed53eca9374..067ffc5eeccfa9 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs @@ -88,9 +88,10 @@ ThreadData IThread.GetThreadData(TargetPointer threadPointer) TargetPointer address = _target.ReadPointer(thread.ExceptionTracker); TargetPointer firstNestedException = TargetPointer.Null; bool hasUnhandledException = false; + Data.ExceptionInfo? exceptionInfo = null; if (address != TargetPointer.Null) { - Data.ExceptionInfo exceptionInfo = _target.ProcessedData.GetOrAdd(address); + exceptionInfo = _target.ProcessedData.GetOrAdd(address); firstNestedException = exceptionInfo.PreviousNestedInfo; if (exceptionInfo.ThrownObject != TargetPointer.Null) @@ -104,6 +105,16 @@ ThreadData IThread.GetThreadData(TargetPointer threadPointer) if (thread.LastThrownObjectIsUnhandled != 0) hasUnhandledException = true; + // Prefer the active exception from ExInfo (pseudo-handle to m_exception field). + // After the removal of SetThrowable/m_hThrowable, m_LastThrownObjectHandle is only + // updated after exception dispatch completes, so during active dispatch it may be + // stale. The pseudo-handle has the same dereference semantics as a real GC handle. + TargetPointer lastThrownObjectHandle = GetActiveExceptionPseudoHandle(exceptionInfo, address); + if (lastThrownObjectHandle == TargetPointer.Null) + { + lastThrownObjectHandle = thread.LastThrownObject.Handle; + } + return new ThreadData( threadPointer, thread.Id, @@ -115,7 +126,7 @@ ThreadData IThread.GetThreadData(TargetPointer threadPointer) thread.Frame, firstNestedException, thread.ExposedObject.Handle, - thread.LastThrownObject.Handle, + lastThrownObjectHandle, thread.CurrentCustomDebuggerNotification.Handle, thread.LastThrownObjectIsUnhandled != 0, hasUnhandledException, @@ -218,19 +229,27 @@ TargetPointer IThread.GetThreadLocalStaticBase(TargetPointer threadPointer, Targ return (thread, exceptionInfo, exceptionTrackerPtr); } - TargetPointer IThread.GetCurrentExceptionHandle(TargetPointer threadPointer) + /// + /// Returns the target address of the ExInfo::m_exception field as a pseudo-handle + /// if there is an active exception tracker with a non-null thrown object. + /// Callers dereference this address to read the exception Object*, just like a real + /// GC handle. Returns TargetPointer.Null when no active exception is present. + /// + private TargetPointer GetActiveExceptionPseudoHandle(Data.ExceptionInfo? exceptionInfo, TargetPointer exceptionTrackerAddr) { - var (_, exceptionInfo, exceptionTrackerAddr) = GetThreadExceptionInfo(threadPointer); - if (exceptionInfo is null || exceptionInfo.ThrownObject == TargetPointer.Null) return TargetPointer.Null; - // Return the target address of the ThrownObject field as a pseudo-handle. - // Callers dereference this address to read the exception Object*. Target.TypeInfo type = _target.GetTypeInfo(DataType.ExceptionInfo); return exceptionTrackerAddr + (ulong)type.Fields[nameof(Data.ExceptionInfo.ThrownObject)].Offset; } + TargetPointer IThread.GetCurrentExceptionHandle(TargetPointer threadPointer) + { + var (_, exceptionInfo, exceptionTrackerAddr) = GetThreadExceptionInfo(threadPointer); + return GetActiveExceptionPseudoHandle(exceptionInfo, exceptionTrackerAddr); + } + byte[] IThread.GetWatsonBuckets(TargetPointer threadPointer) { TargetPointer readFrom; diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs index bc3a49679fb3b6..61c3d523afcde9 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs @@ -277,6 +277,12 @@ public ulong CurrentCustomDebuggerNotification } public ulong FrameAddress => GetFieldAddress(FrameFieldName); + + public ulong LastThrownObject + { + get => ReadPointerField(LastThrownObjectFieldName); + set => WritePointerField(LastThrownObjectFieldName, value); + } } internal sealed class MockThreadBuilder diff --git a/src/native/managed/cdac/tests/ThreadTests.cs b/src/native/managed/cdac/tests/ThreadTests.cs index c9b3d9bd30c2bf..e58d224aa5ba4a 100644 --- a/src/native/managed/cdac/tests/ThreadTests.cs +++ b/src/native/managed/cdac/tests/ThreadTests.cs @@ -270,4 +270,60 @@ public void GetCurrentExceptionHandle_HandlePointsToNull(MockTarget.Architecture TargetPointer thrownObjectHandle = contract.GetCurrentExceptionHandle(new TargetPointer(thread!.Address)); Assert.Equal(TargetPointer.Null, thrownObjectHandle); } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetThreadData_LastThrownObjectHandle_ActiveException(MockTarget.Architecture arch) + { + // When an active exception is being dispatched (ExInfo has a non-null ThrownObject), + // GetThreadData should return a pseudo-handle to the ExInfo's ThrownObject field + // instead of the (potentially stale) m_LastThrownObjectHandle. + MockThread? thread = null; + MockExceptionInfo? exceptionInfo = null; + TargetPointer activeException = new(0xBEEF_0001); + + TestPlaceholderTarget target = CreateTarget( + arch, + threadBuilder => + { + thread = threadBuilder.AddThread(1, 1234); + exceptionInfo = threadBuilder.GetExceptionInfo(thread); + exceptionInfo!.ThrownObject = (ulong)activeException; + // Set a stale handle to verify it is NOT returned + thread.LastThrownObject = 0xDEAD_0001; + }); + + IThread contract = target.Contracts.Thread; + ThreadData data = contract.GetThreadData(new TargetPointer(thread!.Address)); + // Should return the pseudo-handle (address of ThrownObject field), not the stale handle + Assert.NotEqual(TargetPointer.Null, data.LastThrownObjectHandle); + Assert.NotEqual(new TargetPointer(0xDEAD_0001), data.LastThrownObjectHandle); + // Dereferencing the pseudo-handle should yield the active exception object + Assert.Equal(activeException, target.ReadPointer(data.LastThrownObjectHandle)); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetThreadData_LastThrownObjectHandle_NoActiveException(MockTarget.Architecture arch) + { + // When no active exception is being dispatched (ExInfo ThrownObject is null), + // GetThreadData should fall back to m_LastThrownObjectHandle. + MockThread? thread = null; + MockExceptionInfo? exceptionInfo = null; + TargetPointer lastThrownHandle = new(0xCAFE_0001); + + TestPlaceholderTarget target = CreateTarget( + arch, + threadBuilder => + { + thread = threadBuilder.AddThread(1, 1234); + exceptionInfo = threadBuilder.GetExceptionInfo(thread); + exceptionInfo!.ThrownObject = 0; + thread.LastThrownObject = (ulong)lastThrownHandle; + }); + + IThread contract = target.Contracts.Thread; + ThreadData data = contract.GetThreadData(new TargetPointer(thread!.Address)); + Assert.Equal(lastThrownHandle, data.LastThrownObjectHandle); + } } From 82f64bf489064ca150d93878885823c8abf5923a Mon Sep 17 00:00:00 2001 From: Matt Thalman Date: Mon, 4 May 2026 19:28:37 -0500 Subject: [PATCH 098/115] Group Renovate container image digest updates into a single PR (#127748) Renovate was updating container image digests with individual PRs (e.g. https://github.com/dotnet/runtime/pull/127739, https://github.com/dotnet/runtime/pull/127738). This is the default behavior of Renovate, to create a separate PR for each dependency name. This is prevented by adding a `groupName` to the package rule. This ensures that all digest pinning is grouped together as one unit. A `groupSlug` is also defined to explicitly define the programmatic name that would be used for things like branch name. --- eng/renovate.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/eng/renovate.json b/eng/renovate.json index b1efc3c669c073..6db2e97b80bcbd 100644 --- a/eng/renovate.json +++ b/eng/renovate.json @@ -37,6 +37,8 @@ "eng/pipelines/libraries/helix-queues-setup.yml" ], "pinDigests": true, + "groupName": "container image digests", + "groupSlug": "container-image-digests", "commitMessageTopic": "container image digests" } ] From 4b97fc1176ddbbd6356d18dd7ddabd2cc46a1c7b Mon Sep 17 00:00:00 2001 From: Adeel Mujahid <3840695+am11@users.noreply.github.com> Date: Tue, 5 May 2026 03:46:31 +0300 Subject: [PATCH 099/115] Delete CreateProcessW impl from PAL (#127604) https://github.com/dotnet/runtime/pull/126888#issuecomment-4264756121 --------- Co-authored-by: Jan Kotas Co-authored-by: Juan Hoyos <19413848+hoyosjs@users.noreply.github.com> --- src/coreclr/debug/di/dbgtransportmanager.cpp | 31 - src/coreclr/debug/di/dbgtransportmanager.h | 15 +- src/coreclr/debug/di/dbgtransportpipeline.cpp | 115 - src/coreclr/debug/di/nativepipeline.h | 14 - src/coreclr/debug/di/process.cpp | 270 +- src/coreclr/debug/di/rsmain.cpp | 196 +- src/coreclr/debug/di/rspriv.h | 46 - src/coreclr/debug/di/shimpriv.h | 18 +- src/coreclr/debug/di/windowspipeline.cpp | 51 - .../dlls/mscordac/mscordac_unixexports.src | 1 - src/coreclr/pal/inc/pal.h | 17 - src/coreclr/pal/src/include/pal/procobj.hpp | 14 - src/coreclr/pal/src/thread/process.cpp | 5050 ++++++----------- src/coreclr/pal/tests/palsuite/CMakeLists.txt | 18 - .../pal/tests/palsuite/compilableTests.txt | 9 - .../OutputDebugStringA/test1/helper.cpp | 34 - .../OutputDebugStringA/test1/test1.cpp | 95 - .../pal/tests/palsuite/paltestlist.txt | 2 - .../CreateProcessW/test1/childProcess.cpp | 109 - .../CreateProcessW/test1/parentProcess.cpp | 168 - .../CreateProcessW/test2/childprocess.cpp | 77 - .../CreateProcessW/test2/parentprocess.cpp | 253 - .../threading/CreateProcessW/test2/test2.h | 30 - .../ExitThread/test2/childprocess.cpp | 40 - .../threading/ExitThread/test2/myexitcode.h | 13 - .../threading/ExitThread/test2/test2.cpp | 133 - .../GetExitCodeProcess/test1/childProcess.cpp | 31 - .../GetExitCodeProcess/test1/myexitcode.h | 13 - .../GetExitCodeProcess/test1/test1.cpp | 128 - .../OpenEventW/test3/childprocess.cpp | 80 - .../threading/OpenEventW/test3/test3.cpp | 191 - .../OpenProcess/test1/childProcess.cpp | 44 - .../threading/OpenProcess/test1/myexitcode.h | 13 - .../threading/OpenProcess/test1/test1.cpp | 198 - .../test5/commonconsts.h | 41 - .../WaitForMultipleObjectsEx/test5/helper.cpp | 121 - .../WaitForMultipleObjectsEx/test5/test5.cpp | 505 -- .../WFSOProcessTest/ChildProcess.cpp | 49 - .../WFSOProcessTest/WFSOProcessTest.cpp | 115 - src/coreclr/utilcode/winfix.cpp | 9 +- 40 files changed, 1868 insertions(+), 6489 deletions(-) delete mode 100644 src/coreclr/pal/tests/palsuite/debug_api/OutputDebugStringA/test1/helper.cpp delete mode 100644 src/coreclr/pal/tests/palsuite/debug_api/OutputDebugStringA/test1/test1.cpp delete mode 100644 src/coreclr/pal/tests/palsuite/threading/CreateProcessW/test1/childProcess.cpp delete mode 100644 src/coreclr/pal/tests/palsuite/threading/CreateProcessW/test1/parentProcess.cpp delete mode 100644 src/coreclr/pal/tests/palsuite/threading/CreateProcessW/test2/childprocess.cpp delete mode 100644 src/coreclr/pal/tests/palsuite/threading/CreateProcessW/test2/parentprocess.cpp delete mode 100644 src/coreclr/pal/tests/palsuite/threading/CreateProcessW/test2/test2.h delete mode 100644 src/coreclr/pal/tests/palsuite/threading/ExitThread/test2/childprocess.cpp delete mode 100644 src/coreclr/pal/tests/palsuite/threading/ExitThread/test2/myexitcode.h delete mode 100644 src/coreclr/pal/tests/palsuite/threading/ExitThread/test2/test2.cpp delete mode 100644 src/coreclr/pal/tests/palsuite/threading/GetExitCodeProcess/test1/childProcess.cpp delete mode 100644 src/coreclr/pal/tests/palsuite/threading/GetExitCodeProcess/test1/myexitcode.h delete mode 100644 src/coreclr/pal/tests/palsuite/threading/GetExitCodeProcess/test1/test1.cpp delete mode 100644 src/coreclr/pal/tests/palsuite/threading/OpenEventW/test3/childprocess.cpp delete mode 100644 src/coreclr/pal/tests/palsuite/threading/OpenEventW/test3/test3.cpp delete mode 100644 src/coreclr/pal/tests/palsuite/threading/OpenProcess/test1/childProcess.cpp delete mode 100644 src/coreclr/pal/tests/palsuite/threading/OpenProcess/test1/myexitcode.h delete mode 100644 src/coreclr/pal/tests/palsuite/threading/OpenProcess/test1/test1.cpp delete mode 100644 src/coreclr/pal/tests/palsuite/threading/WaitForMultipleObjectsEx/test5/commonconsts.h delete mode 100644 src/coreclr/pal/tests/palsuite/threading/WaitForMultipleObjectsEx/test5/helper.cpp delete mode 100644 src/coreclr/pal/tests/palsuite/threading/WaitForMultipleObjectsEx/test5/test5.cpp delete mode 100644 src/coreclr/pal/tests/palsuite/threading/WaitForSingleObject/WFSOProcessTest/ChildProcess.cpp delete mode 100644 src/coreclr/pal/tests/palsuite/threading/WaitForSingleObject/WFSOProcessTest/WFSOProcessTest.cpp diff --git a/src/coreclr/debug/di/dbgtransportmanager.cpp b/src/coreclr/debug/di/dbgtransportmanager.cpp index 828af8928724f6..7c17adadde2784 100644 --- a/src/coreclr/debug/di/dbgtransportmanager.cpp +++ b/src/coreclr/debug/di/dbgtransportmanager.cpp @@ -160,37 +160,6 @@ void DbgTransportTarget::ReleaseTransport(DbgTransportSession *pTransport) pTransport->Shutdown(); } -HRESULT DbgTransportTarget::CreateProcess(LPCWSTR lpApplicationName, - LPCWSTR lpCommandLine, - LPSECURITY_ATTRIBUTES lpProcessAttributes, - LPSECURITY_ATTRIBUTES lpThreadAttributes, - BOOL bInheritHandles, - DWORD dwCreationFlags, - LPVOID lpEnvironment, - LPCWSTR lpCurrentDirectory, - LPSTARTUPINFOW lpStartupInfo, - LPPROCESS_INFORMATION lpProcessInformation) -{ - - BOOL result = WszCreateProcess(lpApplicationName, - lpCommandLine, - lpProcessAttributes, - lpThreadAttributes, - bInheritHandles, - dwCreationFlags, - lpEnvironment, - lpCurrentDirectory, - lpStartupInfo, - lpProcessInformation); - - if (!result) - { - return HRESULT_FROM_GetLastError(); - } - - return S_OK; -} - // Kill the process identified by PID. void DbgTransportTarget::KillProcess(DWORD dwPID) { diff --git a/src/coreclr/debug/di/dbgtransportmanager.h b/src/coreclr/debug/di/dbgtransportmanager.h index f03dd9a401fecf..d7f2273b171949 100644 --- a/src/coreclr/debug/di/dbgtransportmanager.h +++ b/src/coreclr/debug/di/dbgtransportmanager.h @@ -14,8 +14,7 @@ // It also handles things like creating and killing a process. // Usual lifecycle looks like this: -// Debug a new process: -// * CreateProcess(&pid) +// Debug an existing process: // * On Mac, Optionally obtain an application group ID from a user // * Create a ProcessDescriptor pd // * GetTransportForProcess(&pd, &transport) @@ -42,18 +41,6 @@ class DbgTransportTarget // connection at this point). void ReleaseTransport(DbgTransportSession *pTransport); - // When and if the process starts the runtime will be told to halt and wait for a debugger attach. - HRESULT CreateProcess(LPCWSTR lpApplicationName, - LPCWSTR lpCommandLine, - LPSECURITY_ATTRIBUTES lpProcessAttributes, - LPSECURITY_ATTRIBUTES lpThreadAttributes, - BOOL bInheritHandles, - DWORD dwCreationFlags, - LPVOID lpEnvironment, - LPCWSTR lpCurrentDirectory, - LPSTARTUPINFOW lpStartupInfo, - LPPROCESS_INFORMATION lpProcessInformation); - // Kill the process identified by PID. void KillProcess(DWORD dwPID); diff --git a/src/coreclr/debug/di/dbgtransportpipeline.cpp b/src/coreclr/debug/di/dbgtransportpipeline.cpp index 90564df07a0b19..b42f7c4534e080 100644 --- a/src/coreclr/debug/di/dbgtransportpipeline.cpp +++ b/src/coreclr/debug/di/dbgtransportpipeline.cpp @@ -75,20 +75,6 @@ class DbgTransportPipeline : virtual BOOL DebugSetProcessKillOnExit(bool fKillOnExit); - // Create - virtual HRESULT CreateProcessUnderDebugger( - MachineInfo machineInfo, - LPCWSTR lpApplicationName, - LPCWSTR lpCommandLine, - LPSECURITY_ATTRIBUTES lpProcessAttributes, - LPSECURITY_ATTRIBUTES lpThreadAttributes, - BOOL bInheritHandles, - DWORD dwCreationFlags, - LPVOID lpEnvironment, - LPCWSTR lpCurrentDirectory, - LPSTARTUPINFOW lpStartupInfo, - LPPROCESS_INFORMATION lpProcessInformation); - // Attach virtual HRESULT DebugActiveProcess(MachineInfo machineInfo, const ProcessDescriptor& processDescriptor); @@ -182,107 +168,6 @@ BOOL DbgTransportPipeline::DebugSetProcessKillOnExit(bool fKillOnExit) return TRUE; } -// Create an process under the debugger. -HRESULT DbgTransportPipeline::CreateProcessUnderDebugger( - MachineInfo machineInfo, - LPCWSTR lpApplicationName, - LPCWSTR lpCommandLine, - LPSECURITY_ATTRIBUTES lpProcessAttributes, - LPSECURITY_ATTRIBUTES lpThreadAttributes, - BOOL bInheritHandles, - DWORD dwCreationFlags, - LPVOID lpEnvironment, - LPCWSTR lpCurrentDirectory, - LPSTARTUPINFOW lpStartupInfo, - LPPROCESS_INFORMATION lpProcessInformation) -{ - // INativeEventPipeline has a 1:1 relationship with CordbProcess. - _ASSERTE(!IsTransportRunning()); - - // We don't support interop-debugging on the Mac. - _ASSERTE(!(dwCreationFlags & (DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS))); - - // When we're using a transport we can't deal with creating a suspended process (we need the process to - // startup in order that it can start up a transport thread and reply to our messages). - _ASSERTE(!(dwCreationFlags & CREATE_SUSPENDED)); - - // Connect to the debugger proxy on the remote machine and ask it to create a process for us. - HRESULT hr = E_FAIL; - - m_pProxy = &g_DbgTransportTarget; - hr = m_pProxy->CreateProcess(lpApplicationName, - lpCommandLine, - lpProcessAttributes, - lpThreadAttributes, - bInheritHandles, - dwCreationFlags, - lpEnvironment, - lpCurrentDirectory, - lpStartupInfo, - lpProcessInformation); - - if (SUCCEEDED(hr)) - { - ProcessDescriptor processDescriptor = ProcessDescriptor::Create(lpProcessInformation->dwProcessId, NULL); - - // Establish a connection to the actual runtime to be debugged. - hr = m_pProxy->GetTransportForProcess(&processDescriptor, - &m_pTransport, - &m_hProcess); - if (SUCCEEDED(hr)) - { - // Wait for the connection to become usable (or time out). - if (!m_pTransport->WaitForSessionToOpen(10000)) - { - hr = CORDBG_E_TIMEOUT; - } - else - { - if (!m_pTransport->UseAsDebugger(&m_ticket)) - { - hr = CORDBG_E_DEBUGGER_ALREADY_ATTACHED; - } - } - } - } - - if (SUCCEEDED(hr)) - { - _ASSERTE((m_hProcess != NULL) && (m_hProcess != INVALID_HANDLE_VALUE)); - - m_dwProcessId = lpProcessInformation->dwProcessId; - - // For Mac remote debugging, we don't actually have a process handle to hand back to the debugger. - // Instead, we return a handle to an event as the "process handle". The Win32 event thread also waits - // on this event handle, and the event will be signaled when the proxy notifies us that the process - // on the remote machine is terminated. However, normally the debugger calls CloseHandle() immediately - // on the "process handle" after CreateProcess() returns. Doing so causes the Win32 event thread to - // continue waiting on a closed event handle, and so it will never wake up. - // (In fact, in Whidbey, we also duplicate the process handle in code:CordbProcess::Init.) - if (!DuplicateHandle(GetCurrentProcess(), - m_hProcess, - GetCurrentProcess(), - &(lpProcessInformation->hProcess), - 0, // ignored since we are going to pass DUPLICATE_SAME_ACCESS - FALSE, - DUPLICATE_SAME_ACCESS)) - { - hr = HRESULT_FROM_GetLastError(); - } - } - - if (SUCCEEDED(hr)) - { - m_fRunning = TRUE; - } - else - { - Dispose(); - } - - return hr; -} - // Attach the debugger to this process. HRESULT DbgTransportPipeline::DebugActiveProcess(MachineInfo machineInfo, const ProcessDescriptor& processDescriptor) { diff --git a/src/coreclr/debug/di/nativepipeline.h b/src/coreclr/debug/di/nativepipeline.h index 8076830e5607d1..442ab32138b5ff 100644 --- a/src/coreclr/debug/di/nativepipeline.h +++ b/src/coreclr/debug/di/nativepipeline.h @@ -50,20 +50,6 @@ class INativeEventPipeline virtual BOOL DebugSetProcessKillOnExit(bool fKillOnExit) = 0; - // Create - virtual HRESULT CreateProcessUnderDebugger( - MachineInfo machineInfo, - LPCWSTR lpApplicationName, - LPCWSTR lpCommandLine, - LPSECURITY_ATTRIBUTES lpProcessAttributes, - LPSECURITY_ATTRIBUTES lpThreadAttributes, - BOOL bInheritHandles, - DWORD dwCreationFlags, - LPVOID lpEnvironment, - LPCWSTR lpCurrentDirectory, - LPSTARTUPINFOW lpStartupInfo, - LPPROCESS_INFORMATION lpProcessInformation) = 0; - // Attach virtual HRESULT DebugActiveProcess(MachineInfo machineInfo, const ProcessDescriptor& processDescriptor) = 0; diff --git a/src/coreclr/debug/di/process.cpp b/src/coreclr/debug/di/process.cpp index 25495d6974b86a..b47cc24cf3b563 100644 --- a/src/coreclr/debug/di/process.cpp +++ b/src/coreclr/debug/di/process.cpp @@ -1065,95 +1065,6 @@ CordbProcess::~CordbProcess() // Set this to mark that we really did cleanup. } -//----------------------------------------------------------------------------- -// Static build helper. -// This will create a process under the pCordb root, and add it to the list. -// We don't return the process - caller gets the pid and looks it up under -// the Cordb object. -// -// Arguments: -// pCordb - Pointer to the implementation of the owning Cordb object implementing the -// owning ICD interface. -// szProgramName - Name of the program to execute. -// szProgramArgs - Command line arguments for the process. -// lpProcessAttributes - OS-specific attributes for process creation. -// lpThreadAttributes - OS-specific attributes for thread creation. -// fInheritFlags - OS-specific flag for child process inheritance. -// dwCreationFlags - OS-specific creation flags. -// lpEnvironment - OS-specific environmental strings. -// szCurrentDirectory - OS-specific string for directory to run in. -// lpStartupInfo - OS-specific info on startup. -// lpProcessInformation - OS-specific process information buffer. -// corDebugFlags - What type of process to create, currently always managed. -//----------------------------------------------------------------------------- -HRESULT ShimProcess::CreateProcess( - Cordb * pCordb, - ICorDebugRemoteTarget * pRemoteTarget, - LPCWSTR szProgramName, - _In_z_ LPWSTR szProgramArgs, - LPSECURITY_ATTRIBUTES lpProcessAttributes, - LPSECURITY_ATTRIBUTES lpThreadAttributes, - BOOL fInheritHandles, - DWORD dwCreationFlags, - PVOID lpEnvironment, - LPCWSTR szCurrentDirectory, - LPSTARTUPINFOW lpStartupInfo, - LPPROCESS_INFORMATION lpProcessInformation, - CorDebugCreateProcessFlags corDebugFlags -) -{ - _ASSERTE(pCordb != NULL); - -#if defined(FEATURE_DBGIPC_TRANSPORT_DI) - // The transport cannot deal with creating a suspended process (it needs the debugger to start up and - // listen for connections). - _ASSERTE((dwCreationFlags & CREATE_SUSPENDED) == 0); -#endif // FEATURE_DBGIPC_TRANSPORT_DI - - HRESULT hr = S_OK; - - RSExtSmartPtr pShim; - EX_TRY - { - pShim.Assign(new ShimProcess()); - - // Indicate that this process was started under the debugger as opposed to attaching later. - pShim->m_attached = false; - - hr = pShim->CreateAndStartWin32ET(pCordb); - IfFailThrow(hr); - - // Call out to newly created Win32-event Thread to create the process. - // If this succeeds, new CordbProcess will add a ref to the ShimProcess - hr = pShim->GetWin32EventThread()->SendCreateProcessEvent(pShim->GetMachineInfo(), - szProgramName, - szProgramArgs, - lpProcessAttributes, - lpThreadAttributes, - fInheritHandles, - dwCreationFlags, - lpEnvironment, - szCurrentDirectory, - lpStartupInfo, - lpProcessInformation, - corDebugFlags); - IfFailThrow(hr); - } - EX_CATCH_HRESULT(hr); - - // If this succeeds, then process takes ownership of thread. Else we need to kill it. - if (FAILED(hr)) - { - if (pShim != NULL) - { - pShim->Dispose(); - } - } - // Always release our ref to ShimProcess. If the Process was created, then it takes a reference. - - return hr; -} - //----------------------------------------------------------------------------- // Static build helper for the attach case. // On success, this will add the process to the pCordb list, and then @@ -10583,12 +10494,11 @@ HRESULT CordbRCEventThread::Stop() enum { W32ETA_NONE = 0, - W32ETA_CREATE_PROCESS = 1, - W32ETA_ATTACH_PROCESS = 2, - W32ETA_CONTINUE = 3, - W32ETA_DETACH = 4, + W32ETA_ATTACH_PROCESS = 1, + W32ETA_CONTINUE = 2, + W32ETA_DETACH = 3, #ifdef OUT_OF_PROCESS_SETTHREADCONTEXT - W32ETA_CAN_DETACH = 5 + W32ETA_CAN_DETACH = 4 #endif // OUT_OF_PROCESS_SETTHREADCONTEXT }; @@ -11737,11 +11647,6 @@ void CordbWin32EventThread::Win32EventLoop() ExitProcess(false); // not detach fEventAvailable = false; } - // Should we create a process? - else if (m_action == W32ETA_CREATE_PROCESS) - { - CreateProcess(); - } // Should we attach to a process? else if (m_action == W32ETA_ATTACH_PROCESS) { @@ -13890,173 +13795,6 @@ void CordbWin32EventThread::ForceDbgContinue(CordbProcess *pProcess, CordbUnmana } -// -// Send a CreateProcess event to the Win32 thread to have it create us -// a new process. -// -HRESULT CordbWin32EventThread::SendCreateProcessEvent( - MachineInfo machineInfo, - LPCWSTR programName, - _In_z_ LPWSTR programArgs, - LPSECURITY_ATTRIBUTES lpProcessAttributes, - LPSECURITY_ATTRIBUTES lpThreadAttributes, - BOOL bInheritHandles, - DWORD dwCreationFlags, - PVOID lpEnvironment, - LPCWSTR lpCurrentDirectory, - LPSTARTUPINFOW lpStartupInfo, - LPPROCESS_INFORMATION lpProcessInformation, - CorDebugCreateProcessFlags corDebugFlags) -{ - HRESULT hr = S_OK; - - LockSendToWin32EventThreadMutex(); - LOG((LF_CORDB, LL_EVERYTHING, "CordbWin32EventThread::SCPE Called\n")); - m_actionData.createData.machineInfo = machineInfo; - m_actionData.createData.programName = programName; - m_actionData.createData.programArgs = programArgs; - m_actionData.createData.lpProcessAttributes = lpProcessAttributes; - m_actionData.createData.lpThreadAttributes = lpThreadAttributes; - m_actionData.createData.bInheritHandles = bInheritHandles; - m_actionData.createData.dwCreationFlags = dwCreationFlags; - m_actionData.createData.lpEnvironment = lpEnvironment; - m_actionData.createData.lpCurrentDirectory = lpCurrentDirectory; - m_actionData.createData.lpStartupInfo = lpStartupInfo; - m_actionData.createData.lpProcessInformation = lpProcessInformation; - m_actionData.createData.corDebugFlags = corDebugFlags; - - // m_action is set last so that the win32 event thread can inspect - // it and take action without actually having to take any - // locks. The lock around this here is simply to prevent multiple - // threads from making requests at the same time. - m_action = W32ETA_CREATE_PROCESS; - - BOOL succ = SetEvent(m_threadControlEvent); - - if (succ) - { - DWORD ret = WaitForSingleObject(m_actionTakenEvent, INFINITE); - - LOG((LF_CORDB, LL_EVERYTHING, "Process Handle is: %x, m_threadControlEvent is %x\n", - (UINT_PTR)m_actionData.createData.lpProcessInformation->hProcess, (UINT_PTR)m_threadControlEvent)); - - if (ret == WAIT_OBJECT_0) - hr = m_actionResult; - else - hr = HRESULT_FROM_GetLastError(); - } - else - hr = HRESULT_FROM_GetLastError(); - - UnlockSendToWin32EventThreadMutex(); - - return hr; -} - - -//--------------------------------------------------------------------------------------- -// -// Create a process -// -// Assumptions: -// This occurs on the win32 event thread. It is invokved via -// a message sent from code:CordbWin32EventThread::SendCreateProcessEvent -// -// Notes: -// Create a new process. This is called in the context of the Win32 -// event thread to ensure that if we're Win32 debugging the process -// that the same thread that waits for debugging events will be the -// thread that creates the process. -// -//--------------------------------------------------------------------------------------- -void CordbWin32EventThread::CreateProcess() -{ - m_action = W32ETA_NONE; - HRESULT hr = S_OK; - - DWORD dwCreationFlags = m_actionData.createData.dwCreationFlags; - - // If the creation flags has DEBUG_PROCESS in them, then we're - // Win32 debugging this process. Otherwise, we have to create - // suspended to give us time to setup up our side of the IPC - // channel. - BOOL fInteropDebugging = -#if defined(FEATURE_INTEROP_DEBUGGING) - (dwCreationFlags & (DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS)); -#else - false; // Interop not supported. -#endif - - // Have Win32 create the process... - hr = m_pNativePipeline->CreateProcessUnderDebugger( - m_actionData.createData.machineInfo, - m_actionData.createData.programName, - m_actionData.createData.programArgs, - m_actionData.createData.lpProcessAttributes, - m_actionData.createData.lpThreadAttributes, - m_actionData.createData.bInheritHandles, - dwCreationFlags, - m_actionData.createData.lpEnvironment, - m_actionData.createData.lpCurrentDirectory, - m_actionData.createData.lpStartupInfo, - m_actionData.createData.lpProcessInformation); - - if (SUCCEEDED(hr)) - { - // Process ID is filled in after process is successfully created. - DWORD dwProcessId = m_actionData.createData.lpProcessInformation->dwProcessId; - ProcessDescriptor pd = ProcessDescriptor::FromPid(dwProcessId); - - RSUnsafeExternalSmartPtr pProcess; - hr = m_pShim->InitializeDataTarget(&pd); - - if (SUCCEEDED(hr)) - { - // To emulate V2 semantics, we pass 0 for the clrInstanceID into - // OpenVirtualProcess. This will then connect to the first CLR - // loaded. - const ULONG64 cFirstClrLoaded = 0; - hr = CordbProcess::OpenVirtualProcess(cFirstClrLoaded, m_pShim->GetDataTarget(), NULL, m_cordb, &pd, m_pShim, &pProcess); - } - - // Shouldn't happen on a create, only an attach - _ASSERTE(hr != CORDBG_E_DEBUGGER_ALREADY_ATTACHED); - - // Remember the process in the global list of processes. - if (SUCCEEDED(hr)) - { - EX_TRY - { - // Mark if we're interop-debugging - if (fInteropDebugging) - { - pProcess->EnableInteropDebugging(); - } - - m_cordb->AddProcess(pProcess); // will take ref if it succeeds - } - EX_CATCH_HRESULT(hr); - } - - // If we're Win32 attached to this process, then increment the - // proper count, otherwise add this process to the wait set - // and resume the process's main thread. - if (SUCCEEDED(hr)) - { - _ASSERTE(m_pProcess == NULL); - m_pProcess.Assign(pProcess); - } - } - - - // - // Signal the hr to the caller. - // - m_actionResult = hr; - SetEvent(m_actionTakenEvent); -} - - // // Send a DebugActiveProcess event to the Win32 thread to have it attach to // a new process. diff --git a/src/coreclr/debug/di/rsmain.cpp b/src/coreclr/debug/di/rsmain.cpp index 2daafa9e75be81..c7d5c5a9e24e49 100644 --- a/src/coreclr/debug/di/rsmain.cpp +++ b/src/coreclr/debug/di/rsmain.cpp @@ -1499,17 +1499,6 @@ HRESULT Cordb::SetUnmanagedHandler(ICorDebugUnmanagedCallback *pCallback) return S_OK; } -// CreateProcess() isn't supported on Windows CoreCLR. -// It is currently supported on Mac CoreCLR, but that may change. -bool Cordb::IsCreateProcessSupported() -{ -#if !defined(FEATURE_DBGIPC_TRANSPORT_DI) - return false; -#else - return true; -#endif -} - // Given everything we know about our configuration, can we support interop-debugging bool Cordb::IsInteropDebuggingSupported() { @@ -1562,152 +1551,22 @@ HRESULT Cordb::CreateProcess(LPCWSTR lpApplicationName, CorDebugCreateProcessFlags debuggingFlags, ICorDebugProcess **ppProcess) { - return CreateProcessCommon(NULL, - lpApplicationName, - lpCommandLine, - lpProcessAttributes, - lpThreadAttributes, - bInheritHandles, - dwCreationFlags, - lpEnvironment, - lpCurrentDirectory, - lpStartupInfo, - lpProcessInformation, - debuggingFlags, - ppProcess); -} - -HRESULT Cordb::CreateProcessCommon(ICorDebugRemoteTarget * pRemoteTarget, - LPCWSTR lpApplicationName, - _In_z_ LPWSTR lpCommandLine, - LPSECURITY_ATTRIBUTES lpProcessAttributes, - LPSECURITY_ATTRIBUTES lpThreadAttributes, - BOOL bInheritHandles, - DWORD dwCreationFlags, - PVOID lpEnvironment, - LPCWSTR lpCurrentDirectory, - LPSTARTUPINFOW lpStartupInfo, - LPPROCESS_INFORMATION lpProcessInformation, - CorDebugCreateProcessFlags debuggingFlags, - ICorDebugProcess ** ppProcess) -{ - // If you hit this assert, it means that you are attempting to create a process without specifying the version - // number. - _ASSERTE(CorDebugInvalidVersion != m_debuggerSpecifiedVersion); - PUBLIC_API_ENTRY(this); FAIL_IF_NEUTERED(this); - VALIDATE_POINTER_TO_OBJECT(ppProcess, ICorDebugProcess**); - - HRESULT hr = S_OK; - - EX_TRY - { - if (!m_initialized) - { - ThrowHR(E_FAIL); - } - - // Check that we support the debugger version - CheckCompatibility(); - - #ifdef FEATURE_INTEROP_DEBUGGING - // DEBUG_PROCESS (=0x1) means debug this process & all future children. - // DEBUG_ONLY_THIS_PROCESS =(0x2) means just debug the immediate process. - // If we want to support DEBUG_PROCESS, then we need to have the RS sniff for new CREATE_PROCESS - // events and spawn new CordbProcess for them. - switch(dwCreationFlags & (DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS)) - { - // 1) managed-only debugging - case 0: - break; - - // 2) failure - returns E_NOTIMPL. (as this would involve debugging all of our children processes). - case DEBUG_PROCESS: - ThrowHR(E_NOTIMPL); - - // 3) Interop-debugging. - // Note that MSDN (at least as of Jan 2003) is wrong about this flag. MSDN claims - // DEBUG_ONLY_THIS_PROCESS w/o DEBUG_PROCESS should be ignored. - // But it really should do launch as a debuggee (but not auto-attach to child processes). - case DEBUG_ONLY_THIS_PROCESS: - // Emprically, this is the common case for native / interop-debugging. - break; - - // 4) Interop. - // The spec for ICorDebug::CreateProcess says this is the one to use for interop-debugging. - case DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS: - // Win2k does not honor these flags properly. So we just use - // It treats (DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS) as if it were DEBUG_PROCESS. - // We'll just always touch up the flags, even though WinXP and above is fine here. - // Per win2k issue, strip off DEBUG_PROCESS, so that we're just left w/ DEBUG_ONLY_THIS_PROCESS. - dwCreationFlags &= ~(DEBUG_PROCESS); - break; - - default: - UNREACHABLE(); - } - - #endif // FEATURE_INTEROP_DEBUGGING - - // Must have a managed-callback by now. - if ((m_managedCallback == NULL) || (m_managedCallback2 == NULL) || (m_managedCallback3 == NULL) || (m_managedCallback4 == NULL)) - { - ThrowHR(E_FAIL); - } + (void)lpApplicationName; + (void)lpCommandLine; + (void)lpProcessAttributes; + (void)lpThreadAttributes; + (void)bInheritHandles; + (void)dwCreationFlags; + (void)lpEnvironment; + (void)lpCurrentDirectory; + (void)lpStartupInfo; + (void)lpProcessInformation; + (void)debuggingFlags; + (void)ppProcess; - if (!IsCreateProcessSupported()) - { - ThrowHR(E_NOTIMPL); - } - - if (!IsInteropDebuggingSupported() && - ((dwCreationFlags & (DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS)) != 0)) - { - ThrowHR(CORDBG_E_INTEROP_NOT_SUPPORTED); - } - - // Check that we can even accept another debuggee before trying anything. - EnsureAllowAnotherProcess(); - - } EX_CATCH_HRESULT(hr); - if (FAILED(hr)) - { - return hr; - } - - hr = ShimProcess::CreateProcess(this, - pRemoteTarget, - lpApplicationName, - lpCommandLine, - lpProcessAttributes, - lpThreadAttributes, - bInheritHandles, - dwCreationFlags, - lpEnvironment, - lpCurrentDirectory, - lpStartupInfo, - lpProcessInformation, - debuggingFlags - ); - - LOG((LF_CORDB, LL_EVERYTHING, "Handle in Cordb::CreateProcess is: %.I64x\n", lpProcessInformation->hProcess)); - - if (SUCCEEDED(hr)) - { - LockProcessList(); - - CordbProcess * pProcess = GetProcessList()->GetBase(lpProcessInformation->dwProcessId); - - UnlockProcessList(); - - _ASSERTE(pProcess != NULL); - - pProcess->ExternalAddRef(); - *ppProcess = (ICorDebugProcess *)pProcess; - } - - return hr; + return E_NOTIMPL; } @@ -1730,19 +1589,22 @@ HRESULT Cordb::CreateProcessEx(ICorDebugRemoteTarget * pRemoteTarget, return E_INVALIDARG; } - return CreateProcessCommon(pRemoteTarget, - lpApplicationName, - lpCommandLine, - lpProcessAttributes, - lpThreadAttributes, - bInheritHandles, - dwCreationFlags, - lpEnvironment, - lpCurrentDirectory, - lpStartupInfo, - lpProcessInformation, - debuggingFlags, - ppProcess); + PUBLIC_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + (void)lpApplicationName; + (void)lpCommandLine; + (void)lpProcessAttributes; + (void)lpThreadAttributes; + (void)bInheritHandles; + (void)dwCreationFlags; + (void)lpEnvironment; + (void)lpCurrentDirectory; + (void)lpStartupInfo; + (void)lpProcessInformation; + (void)debuggingFlags; + (void)ppProcess; + + return E_NOTIMPL; } diff --git a/src/coreclr/debug/di/rspriv.h b/src/coreclr/debug/di/rspriv.h index 0933ddd4fbacfa..69f2876a8c1650 100644 --- a/src/coreclr/debug/di/rspriv.h +++ b/src/coreclr/debug/di/rspriv.h @@ -2226,20 +2226,6 @@ class Cordb : public CordbBase, public ICorDebug, public ICorDebugRemote // Methods not exposed via a COM interface. //----------------------------------------------------------- - HRESULT CreateProcessCommon(ICorDebugRemoteTarget * pRemoteTarget, - LPCWSTR lpApplicationName, - _In_z_ LPWSTR lpCommandLine, - LPSECURITY_ATTRIBUTES lpProcessAttributes, - LPSECURITY_ATTRIBUTES lpThreadAttributes, - BOOL bInheritHandles, - DWORD dwCreationFlags, - PVOID lpEnvironment, - LPCWSTR lpCurrentDirectory, - LPSTARTUPINFOW lpStartupInfo, - LPPROCESS_INFORMATION lpProcessInformation, - CorDebugCreateProcessFlags debuggingFlags, - ICorDebugProcess **ppProcess); - HRESULT DebugActiveProcessCommon(ICorDebugRemoteTarget * pRemoteTarget, DWORD id, BOOL win32Attach, ICorDebugProcess **ppProcess); void EnsureCanLaunchOrAttach(BOOL fWin32DebuggingEnabled); @@ -2291,7 +2277,6 @@ class Cordb : public CordbBase, public ICorDebug, public ICorDebugRemote HMODULE GetTargetCLR() { return m_targetCLR; } private: - bool IsCreateProcessSupported(); bool IsInteropDebuggingSupported(); void CheckCompatibility(); @@ -10007,19 +9992,6 @@ class CordbWin32EventThread HRESULT Start(); HRESULT Stop(); - HRESULT SendCreateProcessEvent(MachineInfo machineInfo, - LPCWSTR programName, - _In_z_ LPWSTR programArgs, - LPSECURITY_ATTRIBUTES lpProcessAttributes, - LPSECURITY_ATTRIBUTES lpThreadAttributes, - BOOL bInheritHandles, - DWORD dwCreationFlags, - PVOID lpEnvironment, - LPCWSTR lpCurrentDirectory, - LPSTARTUPINFOW lpStartupInfo, - LPPROCESS_INFORMATION lpProcessInformation, - CorDebugCreateProcessFlags corDebugFlags); - HRESULT SendDebugActiveProcessEvent(MachineInfo machineInfo, const ProcessDescriptor *pProcessDescriptor, bool fWin32Attach, @@ -10070,8 +10042,6 @@ class CordbWin32EventThread void ThreadProc(); static DWORD WINAPI ThreadProc(LPVOID parameter); - void CreateProcess(); - INativeEventPipeline * m_pNativePipeline; @@ -10110,22 +10080,6 @@ class CordbWin32EventThread HRESULT m_actionResult; union { - struct - { - MachineInfo machineInfo; - LPCWSTR programName; - LPWSTR programArgs; - LPSECURITY_ATTRIBUTES lpProcessAttributes; - LPSECURITY_ATTRIBUTES lpThreadAttributes; - BOOL bInheritHandles; - DWORD dwCreationFlags; - PVOID lpEnvironment; - LPCWSTR lpCurrentDirectory; - LPSTARTUPINFOW lpStartupInfo; - LPPROCESS_INFORMATION lpProcessInformation; - CorDebugCreateProcessFlags corDebugFlags; - } createData; - struct { MachineInfo machineInfo; diff --git a/src/coreclr/debug/di/shimpriv.h b/src/coreclr/debug/di/shimpriv.h index 2bd2fd5b45bd9a..acbf2b5da4b522 100644 --- a/src/coreclr/debug/di/shimpriv.h +++ b/src/coreclr/debug/di/shimpriv.h @@ -352,7 +352,7 @@ class ShimProcess // Initialization phases. // 1. allocate new ShimProcess(). This lets us spin up a Win32 EventThread, which can then // be used to - // 2. Call ShimProcess::CreateProcess/DebugActiveProcess. This will call CreateAndStartWin32ET to + // 2. Call ShimProcess::DebugActiveProcess. This will call CreateAndStartWin32ET to // craete the w32et. // 3. Create OS-debugging pipeline. This establishes the physical OS process and gets us a pid/handle // 4. pShim->InitializeDataTarget - this creates a reader/writer abstraction around the OS process. @@ -364,22 +364,6 @@ class ShimProcess // Creation //----------------------------------------------------------- - static HRESULT CreateProcess( - Cordb * pCordb, - ICorDebugRemoteTarget * pRemoteTarget, - LPCWSTR programName, - _In_z_ LPWSTR programArgs, - LPSECURITY_ATTRIBUTES lpProcessAttributes, - LPSECURITY_ATTRIBUTES lpThreadAttributes, - BOOL bInheritHandles, - DWORD dwCreationFlags, - PVOID lpEnvironment, - LPCWSTR lpCurrentDirectory, - LPSTARTUPINFOW lpStartupInfo, - LPPROCESS_INFORMATION lpProcessInformation, - CorDebugCreateProcessFlags corDebugFlags - ); - static HRESULT DebugActiveProcess( Cordb * pCordb, ICorDebugRemoteTarget * pRemoteTarget, diff --git a/src/coreclr/debug/di/windowspipeline.cpp b/src/coreclr/debug/di/windowspipeline.cpp index 56b8312d3d39fd..44c675a2c48f46 100644 --- a/src/coreclr/debug/di/windowspipeline.cpp +++ b/src/coreclr/debug/di/windowspipeline.cpp @@ -56,20 +56,6 @@ class WindowsNativePipeline : virtual BOOL DebugSetProcessKillOnExit(bool fKillOnExit); - // Create - virtual HRESULT CreateProcessUnderDebugger( - MachineInfo machineInfo, - LPCWSTR lpApplicationName, - LPCWSTR lpCommandLine, - LPSECURITY_ATTRIBUTES lpProcessAttributes, - LPSECURITY_ATTRIBUTES lpThreadAttributes, - BOOL bInheritHandles, - DWORD dwCreationFlags, - LPVOID lpEnvironment, - LPCWSTR lpCurrentDirectory, - LPSTARTUPINFOW lpStartupInfo, - LPPROCESS_INFORMATION lpProcessInformation); - // Attach virtual HRESULT DebugActiveProcess(MachineInfo machineInfo, const ProcessDescriptor& processDescriptor); @@ -125,43 +111,6 @@ BOOL WindowsNativePipeline::DebugSetProcessKillOnExit(bool fKillOnExit) return TRUE; } -// Create an process under the debugger. -HRESULT WindowsNativePipeline::CreateProcessUnderDebugger( - MachineInfo machineInfo, - LPCWSTR lpApplicationName, - LPCWSTR lpCommandLine, - LPSECURITY_ATTRIBUTES lpProcessAttributes, - LPSECURITY_ATTRIBUTES lpThreadAttributes, - BOOL bInheritHandles, - DWORD dwCreationFlags, - LPVOID lpEnvironment, - LPCWSTR lpCurrentDirectory, - LPSTARTUPINFOW lpStartupInfo, - LPPROCESS_INFORMATION lpProcessInformation) -{ - // This is always doing Native-debugging at the OS-level. - dwCreationFlags |= (DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS); - - BOOL ret = ::WszCreateProcess( - lpApplicationName, - lpCommandLine, - lpProcessAttributes, - lpThreadAttributes, - bInheritHandles, - dwCreationFlags, - lpEnvironment, - lpCurrentDirectory, - lpStartupInfo, - lpProcessInformation); - if (!ret) - { - return HRESULT_FROM_GetLastError(); - } - - m_dwProcessId = lpProcessInformation->dwProcessId; - return S_OK; -} - // Attach the debugger to this process. HRESULT WindowsNativePipeline::DebugActiveProcess(MachineInfo machineInfo, const ProcessDescriptor& processDescriptor) { diff --git a/src/coreclr/dlls/mscordac/mscordac_unixexports.src b/src/coreclr/dlls/mscordac/mscordac_unixexports.src index 849f9e945fcf17..ebbe8c166a92af 100644 --- a/src/coreclr/dlls/mscordac/mscordac_unixexports.src +++ b/src/coreclr/dlls/mscordac/mscordac_unixexports.src @@ -65,7 +65,6 @@ nativeStringResourceTable_mscorrc #CreateFileW #CreateEventW #CreateEventExW -#CreateProcessW #CreateSemaphoreExW #CreateThread #CloseHandle diff --git a/src/coreclr/pal/inc/pal.h b/src/coreclr/pal/inc/pal.h index 9fb95fffdd5385..3f74cf3ec21b28 100644 --- a/src/coreclr/pal/inc/pal.h +++ b/src/coreclr/pal/inc/pal.h @@ -730,23 +730,6 @@ typedef struct _PROCESS_INFORMATION { DWORD dwThreadId_PAL_Undefined; } PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION; -PALIMPORT -BOOL -PALAPI -CreateProcessW( - IN LPCWSTR lpApplicationName, - IN LPWSTR lpCommandLine, - IN LPSECURITY_ATTRIBUTES lpProcessAttributes, - IN LPSECURITY_ATTRIBUTES lpThreadAttributes, - IN BOOL bInheritHandles, - IN DWORD dwCreationFlags, - IN LPVOID lpEnvironment, - IN LPCWSTR lpCurrentDirectory, - IN LPSTARTUPINFOW lpStartupInfo, - OUT LPPROCESS_INFORMATION lpProcessInformation); - -#define CreateProcess CreateProcessW - PALIMPORT PAL_NORETURN VOID diff --git a/src/coreclr/pal/src/include/pal/procobj.hpp b/src/coreclr/pal/src/include/pal/procobj.hpp index 80f9aa327a1058..a412afea1ca3ad 100644 --- a/src/coreclr/pal/src/include/pal/procobj.hpp +++ b/src/coreclr/pal/src/include/pal/procobj.hpp @@ -74,20 +74,6 @@ namespace CorUnix LONG lAttachCount; }; - PAL_ERROR - InternalCreateProcess( - CPalThread *pThread, - LPCWSTR lpApplicationName, - LPWSTR lpCommandLine, - LPSECURITY_ATTRIBUTES lpProcessAttributes, - LPSECURITY_ATTRIBUTES lpThreadAttributes, - DWORD dwCreationFlags, - LPVOID lpEnvironment, - LPCWSTR lpCurrentDirectory, - LPSTARTUPINFOW lpStartupInfo, - LPPROCESS_INFORMATION lpProcessInformation - ); - PAL_ERROR InitializeProcessCommandLine( LPWSTR lpwstrCmdLine, diff --git a/src/coreclr/pal/src/thread/process.cpp b/src/coreclr/pal/src/thread/process.cpp index 31996bb4c3e78c..4586b7c8114691 100644 --- a/src/coreclr/pal/src/thread/process.cpp +++ b/src/coreclr/pal/src/thread/process.cpp @@ -196,16 +196,6 @@ const char* g_argvCreateDump[MAX_ARGV_ENTRIES] = { nullptr }; // pthread_key_t CorUnix::thObjKey; -static WCHAR W16_WHITESPACE[]= {0x0020, 0x0009, 0x000D, 0}; -static WCHAR W16_WHITESPACE_DQUOTE[]= {0x0020, 0x0009, 0x000D, '"', 0}; - -enum FILETYPE -{ - FILE_ERROR,/*ERROR*/ - FILE_UNIX, /*Unix Executable*/ - FILE_DIR /*Directory*/ -}; - #pragma pack(push,1) // When creating the semaphore name on Mac running in a sandbox, We reference this structure as a byte array // in order to encode its data into a string. Its important to make sure there is no padding between the fields @@ -257,10 +247,6 @@ CreateSemaphoreName( const UnambiguousProcessDescriptor& unambiguousProcessDescriptor, LPCSTR applicationGroupId); -static BOOL getFileName(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, PathCharString& lpFileName); -static char ** buildArgv(LPCWSTR lpCommandLine, PathCharString& lpAppPath, UINT *pnArg); -static BOOL getPath(PathCharString& lpFileName, PathCharString& lpPathFileName); -static int checkFileType(LPCSTR lpFileName); static BOOL PROCEndProcess(HANDLE hProcess, UINT uExitCode, BOOL bTerminateUnconditionally); /*++ @@ -324,2983 +310,1923 @@ GetCurrentProcess( return hPseudoCurrentProcess; } + /*++ Function: - CreateProcessW - -Note: - Only Standard handles need to be inherited. - Security attributes parameters are not used. + GetExitCodeProcess See MSDN doc. --*/ BOOL PALAPI -CreateProcessW( - IN LPCWSTR lpApplicationName, - IN LPWSTR lpCommandLine, - IN LPSECURITY_ATTRIBUTES lpProcessAttributes, - IN LPSECURITY_ATTRIBUTES lpThreadAttributes, - IN BOOL bInheritHandles, - IN DWORD dwCreationFlags, - IN LPVOID lpEnvironment, - IN LPCWSTR lpCurrentDirectory, - IN LPSTARTUPINFOW lpStartupInfo, - OUT LPPROCESS_INFORMATION lpProcessInformation) +GetExitCodeProcess( + IN HANDLE hProcess, + IN LPDWORD lpExitCode) { - PAL_ERROR palError = NO_ERROR; CPalThread *pThread; + PAL_ERROR palError = NO_ERROR; + DWORD dwExitCode; + PROCESS_STATE ps; - PERF_ENTRY(CreateProcessW); - ENTRY("CreateProcessW(lpAppName=%p (%S), lpCmdLine=%p (%S), lpProcessAttr=%p," - "lpThreadAttr=%p, bInherit=%d, dwFlags=%#x, lpEnv=%p," - "lpCurrentDir=%p (%S), lpStartupInfo=%p, lpProcessInfo=%p)\n", - lpApplicationName?lpApplicationName:W16_NULLSTRING, - lpApplicationName?lpApplicationName:W16_NULLSTRING, - lpCommandLine?lpCommandLine:W16_NULLSTRING, - lpCommandLine?lpCommandLine:W16_NULLSTRING,lpProcessAttributes, - lpThreadAttributes, bInheritHandles, dwCreationFlags,lpEnvironment, - lpCurrentDirectory?lpCurrentDirectory:W16_NULLSTRING, - lpCurrentDirectory?lpCurrentDirectory:W16_NULLSTRING, - lpStartupInfo, lpProcessInformation); + PERF_ENTRY(GetExitCodeProcess); + ENTRY("GetExitCodeProcess(hProcess = %p, lpExitCode = %p)\n", + hProcess, lpExitCode); pThread = InternalGetCurrentThread(); - palError = InternalCreateProcess( - pThread, - lpApplicationName, - lpCommandLine, - lpProcessAttributes, - lpThreadAttributes, - dwCreationFlags, - lpEnvironment, - lpCurrentDirectory, - lpStartupInfo, - lpProcessInformation - ); - - if (NO_ERROR != palError) + if(NULL == lpExitCode) { - pThread->SetLastError(palError); + WARN("Got NULL lpExitCode\n"); + palError = ERROR_INVALID_PARAMETER; + goto done; } - LOGEXIT("CreateProcessW returns BOOL %d\n", NO_ERROR == palError); - PERF_EXIT(CreateProcessW); - - return NO_ERROR == palError; -} - -PAL_ERROR -PrepareStandardHandle( - CPalThread *pThread, - HANDLE hFile, - IPalObject **ppobjFile, - int *piFd - ) -{ - PAL_ERROR palError = NO_ERROR; - IPalObject *pobjFile = NULL; - IDataLock *pDataLock = NULL; - CFileProcessLocalData *pLocalData = NULL; - int iError = 0; - - palError = g_pObjectManager->ReferenceObjectByHandle( + palError = PROCGetProcessStatus( pThread, - hFile, - &aotFile, - &pobjFile + hProcess, + &ps, + &dwExitCode ); if (NO_ERROR != palError) { - ERROR("Bad handle passed through CreateProcess\n"); - goto PrepareStandardHandleExit; + ASSERT("Couldn't get process status information!\n"); + goto done; } - palError = pobjFile->GetProcessLocalData( - pThread, - ReadLock, - &pDataLock, - reinterpret_cast(&pLocalData) - ); - - if (NO_ERROR != palError) + if( PS_DONE == ps ) { - ASSERT("Unable to access file data\n"); - goto PrepareStandardHandleExit; + *lpExitCode = dwExitCode; } - - // - // The passed in file needs to be inheritable - // - - if (!pLocalData->inheritable) + else { - ERROR("Non-inheritable handle passed through CreateProcess\n"); - palError = ERROR_INVALID_HANDLE; - goto PrepareStandardHandleExit; + *lpExitCode = STILL_ACTIVE; } - iError = fcntl(pLocalData->unix_fd, F_SETFD, 0); - if (-1 == iError) +done: + + if (NO_ERROR != palError) { - ERROR("Unable to remove close-on-exec for file (errno %i)\n", errno); - palError = ERROR_INVALID_HANDLE; - goto PrepareStandardHandleExit; + pThread->SetLastError(palError); } - *piFd = pLocalData->unix_fd; - pDataLock->ReleaseLock(pThread, FALSE); - pDataLock = NULL; - - // - // Transfer pobjFile reference to out parameter - // + LOGEXIT("GetExitCodeProcess returns BOOL %d\n", NO_ERROR == palError); + PERF_EXIT(GetExitCodeProcess); - *ppobjFile = pobjFile; - pobjFile = NULL; + return NO_ERROR == palError; +} -PrepareStandardHandleExit: +/*++ +Function: + ExitProcess - if (NULL != pDataLock) - { - pDataLock->ReleaseLock(pThread, FALSE); - } +See MSDN doc. +--*/ +PAL_NORETURN +VOID +PALAPI +ExitProcess( + IN UINT uExitCode) +{ + DWORD old_terminator; - if (NULL != pobjFile) - { - pobjFile->ReleaseReference(pThread); - } + PERF_ENTRY_ONLY(ExitProcess); + ENTRY("ExitProcess(uExitCode=0x%x)\n", uExitCode ); - return palError; -} + old_terminator = InterlockedCompareExchange(&terminator, GetCurrentThreadId(), 0); -PAL_ERROR -CorUnix::InternalCreateProcess( - CPalThread *pThread, - LPCWSTR lpApplicationName, - LPWSTR lpCommandLine, - LPSECURITY_ATTRIBUTES lpProcessAttributes, - LPSECURITY_ATTRIBUTES lpThreadAttributes, - DWORD dwCreationFlags, - LPVOID lpEnvironment, - LPCWSTR lpCurrentDirectory, - LPSTARTUPINFOW lpStartupInfo, - LPPROCESS_INFORMATION lpProcessInformation - ) -{ -#if defined(TARGET_TVOS) || defined(TARGET_WASM) - return ERROR_NOT_SUPPORTED; -#else - PAL_ERROR palError = NO_ERROR; - IPalObject *pobjProcess = NULL; - IPalObject *pobjProcessRegistered = NULL; - IDataLock *pLocalDataLock = NULL; - CProcProcessLocalData *pLocalData; - IDataLock *pSharedDataLock = NULL; - CPalThread *pDummyThread = NULL; - HANDLE hDummyThread = NULL; - HANDLE hProcess = NULL; - CObjectAttributes oa(NULL, lpProcessAttributes); - - IPalObject *pobjFileIn = NULL; - int iFdIn = -1; - IPalObject *pobjFileOut = NULL; - int iFdOut = -1; - IPalObject *pobjFileErr = NULL; - int iFdErr = -1; - - pid_t processId; - PathCharString lpFileNamePS; - char **lppArgv = NULL; - UINT nArg; - int iRet; - char **EnvironmentArray=NULL; - int child_blocking_pipe = -1; - int parent_blocking_pipe = -1; - - /* Validate parameters */ - - /* note : specs indicate lpApplicationName should always - be NULL; however support for it is already implemented. Leaving the code - in, specs can change; but rejecting non-NULL for now to conform to the - spec. */ - if( NULL != lpApplicationName ) + if (GetCurrentThreadId() == old_terminator) { - ASSERT("lpApplicationName should be NULL, but is %S instead\n", - lpApplicationName); - palError = ERROR_INVALID_PARAMETER; - goto InternalCreateProcessExit; + // This thread has already initiated termination. This can happen + // in two ways: + // 1) DllMain(DLL_PROCESS_DETACH) triggers a call to ExitProcess. + // 2) PAL_exit() is called after the last PALTerminate(). + // If the PAL is still initialized, we go straight through to + // PROCEndProcess. If it isn't, we simply exit. + if (!PALIsInitialized()) + { + exit(uExitCode); + ASSERT("exit has returned\n"); + } + else + { + WARN("thread re-called ExitProcess\n"); + PROCEndProcess(GetCurrentProcess(), uExitCode, FALSE); + } } - - if (0 != (dwCreationFlags & ~(CREATE_SUSPENDED|CREATE_NEW_CONSOLE))) + else if (0 != old_terminator) { - ASSERT("Unexpected creation flags (%#x)\n", dwCreationFlags); - palError = ERROR_INVALID_PARAMETER; - goto InternalCreateProcessExit; - } + /* another thread has already initiated the termination process. we + could just block on the PALInitLock critical section, but then + PROCSuspendOtherThreads would hang... so sleep forever here, we're + terminating anyway - /* Security attributes parameters are ignored */ - if (lpProcessAttributes != NULL && - (lpProcessAttributes->lpSecurityDescriptor != NULL || - lpProcessAttributes->bInheritHandle != TRUE)) - { - ASSERT("lpProcessAttributes is invalid, parameter ignored (%p)\n", - lpProcessAttributes); - palError = ERROR_INVALID_PARAMETER; - goto InternalCreateProcessExit; + Update: [TODO] PROCSuspendOtherThreads has been removed. Can this + code be changed? */ + WARN("termination already started from another thread; blocking.\n"); + while (true) + { + poll(NULL, 0, INFTIM); + } } - if (lpThreadAttributes != NULL) + /* ExitProcess may be called even if PAL is not initialized. + Verify if process structure exist + */ + if (PALInitLock() && PALIsInitialized()) { - ASSERT("lpThreadAttributes parameter must be NULL (%p)\n", - lpThreadAttributes); - palError = ERROR_INVALID_PARAMETER; - goto InternalCreateProcessExit; - } + PROCEndProcess(GetCurrentProcess(), uExitCode, FALSE); - /* note : Win32 crashes in this case */ - if(NULL == lpStartupInfo) - { - ERROR("lpStartupInfo is NULL\n"); - palError = ERROR_INVALID_PARAMETER; - goto InternalCreateProcessExit; + /* Should not get here, because we terminate the current process */ + ASSERT("PROCEndProcess has returned\n"); } - - /* Validate lpStartupInfo.cb field */ - if (lpStartupInfo->cb < sizeof(STARTUPINFOW)) + else { - ASSERT("lpStartupInfo parameter structure size is invalid (%u)\n", - lpStartupInfo->cb); - palError = ERROR_INVALID_PARAMETER; - goto InternalCreateProcessExit; - } + exit(uExitCode); - /* lpStartupInfo should be either zero or STARTF_USESTDHANDLES */ - if (lpStartupInfo->dwFlags & ~STARTF_USESTDHANDLES) - { - ASSERT("lpStartupInfo parameter invalid flags (%#x)\n", - lpStartupInfo->dwFlags); - palError = ERROR_INVALID_PARAMETER; - goto InternalCreateProcessExit; + /* Should not get here, because we terminate the current process */ + ASSERT("exit has returned\n"); } - /* validate given standard handles if we have any */ - if (lpStartupInfo->dwFlags & STARTF_USESTDHANDLES) - { - palError = PrepareStandardHandle( - pThread, - lpStartupInfo->hStdInput, - &pobjFileIn, - &iFdIn - ); + /* this should never get executed */ + ASSERT("ExitProcess should not return!\n"); + while (true); +} - if (NO_ERROR != palError) - { - goto InternalCreateProcessExit; - } +/*++ +Function: + TerminateProcess - palError = PrepareStandardHandle( - pThread, - lpStartupInfo->hStdOutput, - &pobjFileOut, - &iFdOut - ); +Note: + hProcess is a handle on the current process. - if (NO_ERROR != palError) - { - goto InternalCreateProcessExit; - } +See MSDN doc. +--*/ +BOOL +PALAPI +TerminateProcess( + IN HANDLE hProcess, + IN UINT uExitCode) +{ + BOOL ret; - palError = PrepareStandardHandle( - pThread, - lpStartupInfo->hStdError, - &pobjFileErr, - &iFdErr - ); + PERF_ENTRY(TerminateProcess); + ENTRY("TerminateProcess(hProcess=%p, uExitCode=%u)\n",hProcess, uExitCode ); - if (NO_ERROR != palError) - { - goto InternalCreateProcessExit; - } - } + ret = PROCEndProcess(hProcess, uExitCode, TRUE); - if (!getFileName(lpApplicationName, lpCommandLine, lpFileNamePS)) - { - ERROR("Can't find executable!\n"); - palError = ERROR_FILE_NOT_FOUND; - goto InternalCreateProcessExit; - } + LOGEXIT("TerminateProcess returns BOOL %d\n", ret); + PERF_EXIT(TerminateProcess); + return ret; +} - /* check type of file */ - iRet = checkFileType(lpFileNamePS); +/*++ +Function: + RaiseFailFastException - switch (iRet) - { - case FILE_ERROR: /* file not found, or not an executable */ - WARN ("File is not valid (%s)", lpFileNamePS.GetString()); - palError = ERROR_FILE_NOT_FOUND; - goto InternalCreateProcessExit; - - case FILE_UNIX: /* Unix binary file */ - break; /* nothing to do */ - - case FILE_DIR:/*Directory*/ - WARN ("File is a Directory (%s)", lpFileNamePS.GetString()); - palError = ERROR_ACCESS_DENIED; - goto InternalCreateProcessExit; - break; +See MSDN doc. +--*/ +VOID +PALAPI +DECLSPEC_NORETURN +RaiseFailFastException( + IN PEXCEPTION_RECORD pExceptionRecord, + IN PCONTEXT pContextRecord, + IN DWORD dwFlags) +{ + PERF_ENTRY(RaiseFailFastException); + ENTRY("RaiseFailFastException"); - default: /* not supposed to get here */ - ASSERT ("Invalid return type from checkFileType"); - palError = ERROR_FILE_NOT_FOUND; - goto InternalCreateProcessExit; - } + TerminateCurrentProcessNoExit(TRUE); + for (;;) PROCAbort(); - /* build Argument list, lppArgv is allocated in buildArgv function and - requires to be freed */ - lppArgv = buildArgv(lpCommandLine, lpFileNamePS, &nArg); + LOGEXIT("RaiseFailFastException"); + PERF_EXIT(RaiseFailFastException); +} - /* set the Environment variable */ - if (lpEnvironment != NULL) - { - unsigned i; - // Since CREATE_UNICODE_ENVIRONMENT isn't supported we know the string is ansi - unsigned EnvironmentEntries = 0; - // Convert the environment block to array of strings - // Count the number of entries - // Is it a string that contains null terminated string, the end is delimited - // by two null in a row. - for (i = 0; ((char *)lpEnvironment)[i]!='\0'; i++) - { - EnvironmentEntries ++; - for (;((char *)lpEnvironment)[i]!='\0'; i++) - { - } - } - EnvironmentEntries++; - EnvironmentArray = (char **)malloc(EnvironmentEntries * sizeof(char *)); - - EnvironmentEntries = 0; - // Convert the environment block to array of strings - // Count the number of entries - // Is it a string that contains null terminated string, the end is delimited - // by two null in a row. - for (i = 0; ((char *)lpEnvironment)[i]!='\0'; i++) - { - EnvironmentArray[EnvironmentEntries] = &((char *)lpEnvironment)[i]; - EnvironmentEntries ++; - for (;((char *)lpEnvironment)[i]!='\0'; i++) - { - } - } - EnvironmentArray[EnvironmentEntries] = NULL; - } +/*++ +Function: + PROCEndProcess - // - // Allocate and register the process object for the new process - // + Called from TerminateProcess and ExitProcess. This does the work of + TerminateProcess, but also takes a flag that determines whether we + shut down unconditionally. If the flag is set, the PAL will do very + little extra work before exiting. Most importantly, it won't shut + down any DLLs that are loaded. - palError = g_pObjectManager->AllocateObject( - pThread, - &otProcess, - &oa, - &pobjProcess - ); +--*/ +static BOOL PROCEndProcess(HANDLE hProcess, UINT uExitCode, BOOL bTerminateUnconditionally) +{ + DWORD dwProcessId; + BOOL ret = FALSE; - if (NO_ERROR != palError) + dwProcessId = PROCGetProcessIDFromHandle(hProcess); + if (dwProcessId == 0) { - ERROR("Unable to allocate object for new process\n"); - goto InternalCreateProcessExit; + SetLastError(ERROR_INVALID_HANDLE); } - - palError = g_pObjectManager->RegisterObject( - pThread, - pobjProcess, - &aotProcess, - &hProcess, - &pobjProcessRegistered - ); - - // - // pobjProcess is invalidated by the above call, so - // NULL it out here - // - - pobjProcess = NULL; - - if (NO_ERROR != palError) + else if(dwProcessId != GetCurrentProcessId()) { - ERROR("Unable to register new process object\n"); - goto InternalCreateProcessExit; - } + if (uExitCode != 0) + WARN("exit code 0x%x ignored for external process.\n", uExitCode); - // - // Create a new "dummy" thread object - // + if (kill(dwProcessId, SIGKILL) == 0) + { + ret = TRUE; + } + else + { + switch (errno) { + case ESRCH: + SetLastError(ERROR_INVALID_HANDLE); + break; + case EPERM: + SetLastError(ERROR_ACCESS_DENIED); + break; + default: + // Unexpected failure. + ASSERT(FALSE); + SetLastError(ERROR_INTERNAL_ERROR); + break; + } + } + } + else + { + // WARN/ERROR before starting the termination process and/or leaving the PAL. + if (bTerminateUnconditionally) + { + WARN("exit code 0x%x ignored for terminate.\n", uExitCode); + } + else if ((uExitCode & 0xff) != uExitCode) + { + // TODO: Convert uExitCodes into sysexits(3)? + ERROR("exit() only supports the lower 8-bits of an exit code. " + "status will only see error 0x%x instead of 0x%x.\n", uExitCode & 0xff, uExitCode); + } - palError = InternalCreateDummyThread( - pThread, - lpThreadAttributes, - &pDummyThread, - &hDummyThread - ); + TerminateCurrentProcessNoExit(bTerminateUnconditionally); - if (dwCreationFlags & CREATE_SUSPENDED) - { - int pipe_descs[2]; + LOGEXIT("PROCEndProcess will not return\n"); - if (-1 == pipe(pipe_descs)) + if (bTerminateUnconditionally) { - ERROR("pipe() failed! error is %d (%s)\n", errno, strerror(errno)); - palError = ERROR_NOT_ENOUGH_MEMORY; - goto InternalCreateProcessExit; + // abort() has the semantics that + // (1) it doesn't run atexit handlers + // (2) can invoke CrashReporter or produce a coredump, which is appropriate for TerminateProcess calls + // TerminationRequestHandlingRoutine in synchmanager.cpp sets the exit code to this special value. The + // Watson analyzer needs to know that the process was terminated with a SIGTERM. + PROCAbort(uExitCode == (128 + SIGTERM) ? SIGTERM : SIGABRT); + } + else + { + exit(uExitCode); } - /* [0] is read end, [1] is write end */ - pDummyThread->suspensionInfo.SetBlockingPipe(pipe_descs[1]); - parent_blocking_pipe = pipe_descs[1]; - child_blocking_pipe = pipe_descs[0]; + ASSERT(FALSE); // we shouldn't get here } - palError = pobjProcessRegistered->GetProcessLocalData( - pThread, - WriteLock, - &pLocalDataLock, - reinterpret_cast(&pLocalData) - ); + return ret; +} - if (NO_ERROR != palError) - { - ASSERT("Unable to obtain local data for new process object\n"); - goto InternalCreateProcessExit; - } +/*++ +Function: + PAL_SetShutdownCallback + +Abstract: + Sets a callback that is executed when the PAL is shut down because of + ExitProcess, TerminateProcess or PAL_Shutdown but not PAL_Terminate/Ex. + + NOTE: Currently only one callback can be set at a time. +--*/ +PALIMPORT +VOID +PALAPI +PAL_SetShutdownCallback( + IN PSHUTDOWN_CALLBACK callback) +{ + _ASSERTE(g_shutdownCallback == nullptr); + g_shutdownCallback = callback; +} +/*++ +Function: + PAL_SetCreateDumpCallback - /* fork the new process */ - processId = fork(); +Abstract: + Sets a callback that is executed when create dump is launched to create a crash dump. - if (processId == -1) - { - ASSERT("Unable to create a new process with fork()\n"); - if (-1 != child_blocking_pipe) - { - close(child_blocking_pipe); - close(parent_blocking_pipe); - } + NOTE: Currently only one callback can be set at a time. +--*/ +PALIMPORT +VOID +PALAPI +PAL_SetCreateDumpCallback( + IN PCREATEDUMP_CALLBACK callback) +{ + _ASSERTE(g_createdumpCallback == nullptr); + g_createdumpCallback = callback; +} - palError = ERROR_INTERNAL_ERROR; - goto InternalCreateProcessExit; - } +/*++ +Function: + PAL_SetLogManagedCallstackForSignalCallback - /* From the time the child process begins running, to when it reaches execve, - the child process is not a real PAL process and does not own any PAL - resources, although it has access to the PAL resources of its parent process. - Thus, while the child process is in this window, it is dangerous for it to affect - its parent's PAL resources. As a consequence, no PAL code should be used - in this window; all code should make unix calls. Note the use of _exit - instead of exit to avoid calling PAL_Terminate and the lack of TRACE's and - ASSERT's. */ +Abstract: + Sets a callback that is executed when a signal is received to log the managed callstack. + Used by Android CoreCLR since CreateDump is not supported on Android. - if (processId == 0) /* child process */ - { - // At this point, the PAL should be considered uninitialized for this child process. + NOTE: Currently only one callback can be set at a time. +--*/ +PALIMPORT +VOID +PALAPI +PAL_SetLogManagedCallstackForSignalCallback( + IN PLOGMANAGEDCALLSTACKFORSIGNAL_CALLBACK callback) +{ + _ASSERTE(g_logManagedCallstackForSignalCallback == nullptr); + g_logManagedCallstackForSignalCallback = callback; +} - // Don't want to enter the init_critsec here since we're trying to avoid - // calling PAL functions. Furthermore, nothing should be changing - // the init_count in the child process at this point since this is the only - // thread executing. - init_count = 0; +// Build the semaphore names using the PID and a value that can be used for distinguishing +// between processes with the same PID (which ran at different times). This is to avoid +// cases where a prior process with the same PID exited abnormally without having a chance +// to clean up its semaphore. +// Note to anyone modifying these names in the future: Semaphore names on OS X are limited +// to SEM_NAME_LEN characters, including null. SEM_NAME_LEN is 31 (at least on OS X 10.11). +// NetBSD limits semaphore names to 15 characters, including null (at least up to 7.99.25). +// Keep 31 length for Core 1.0 RC2 compatibility +#if defined(__NetBSD__) +static const char* RuntimeSemaphoreNameFormat = "/clr%s%08llx"; +#else +static const char* RuntimeSemaphoreNameFormat = "/clr%s%08x%016llx"; +#endif - sigset_t sm; +static const char* RuntimeStartupSemaphoreName = "st"; +static const char* RuntimeContinueSemaphoreName = "co"; - // - // Clear out the signal mask for the new process. - // +#if defined(__NetBSD__) +static uint64_t HashSemaphoreName(uint64_t a, uint64_t b) +{ + return (a ^ b) & 0xffffffff; +} +#else +#define HashSemaphoreName(a,b) a,b +#endif - sigemptyset(&sm); - iRet = sigprocmask(SIG_SETMASK, &sm, NULL); - if (iRet != 0) - { - _exit(EXIT_FAILURE); - } +static const char *const TwoWayNamedPipePrefix = "clr-debug-pipe"; +static const char* IpcNameFormat = "%s-%d-%llu-%s"; - if (dwCreationFlags & CREATE_SUSPENDED) - { - BYTE resume_code = 0; - ssize_t read_ret; +#ifdef ENABLE_RUNTIME_EVENTS_OVER_PIPES +static const char* RuntimeStartupPipeName = "st"; +static const char* RuntimeContinuePipeName = "co"; - /* close the write end of the pipe, the child doesn't need it */ - close(parent_blocking_pipe); +#define PIPE_OPEN_RETRY_DELAY_NS 500000000 // 500 ms - read_again: - /* block until ResumeThread writes something to the pipe */ - read_ret = read(child_blocking_pipe, &resume_code, sizeof(resume_code)); - if (sizeof(resume_code) != read_ret) - { - if (read_ret == -1 && EINTR == errno) - { - goto read_again; - } - else - { - /* note : read might return 0 (and return EAGAIN) if the other - end of the pipe gets closed - for example because the parent - process dies (very) abruptly */ - _exit(EXIT_FAILURE); - } - } - if (WAKEUPCODE != resume_code) - { - // resume_code should always equal WAKEUPCODE. - _exit(EXIT_FAILURE); - } +typedef enum +{ + RuntimeEventsOverPipes_Disabled = 0, + RuntimeEventsOverPipes_Succeeded = 1, + RuntimeEventsOverPipes_Failed = 2, +} RuntimeEventsOverPipes; - close(child_blocking_pipe); - } +typedef enum +{ + RuntimeEvent_Unknown = 0, + RuntimeEvent_Started = 1, + RuntimeEvent_Continue = 2, +} RuntimeEvent; - /* Set the current directory */ - if (lpCurrentDirectory) - { - SetCurrentDirectoryW(lpCurrentDirectory); - } +static +int +OpenPipe(const char* name, int mode) +{ + int fd = -1; + int flags = mode | O_NONBLOCK; - /* Set the standard handles to the incoming values */ - if (lpStartupInfo->dwFlags & STARTF_USESTDHANDLES) +#if defined(FD_CLOEXEC) + flags |= O_CLOEXEC; +#endif + + while (fd == -1) + { + fd = open(name, flags); + if (fd == -1) { - /* For each handle, we need to duplicate the incoming unix - fd to the corresponding standard one. The API that I use, - dup2, will copy the source to the destination, automatically - closing the existing destination, in an atomic way */ - if (dup2(iFdIn, STDIN_FILENO) == -1) + if (mode == O_WRONLY && errno == ENXIO) { - // Didn't duplicate standard in. - _exit(EXIT_FAILURE); + PAL_nanosleep(PIPE_OPEN_RETRY_DELAY_NS); + continue; } - - if (dup2(iFdOut, STDOUT_FILENO) == -1) + else if (errno == EINTR) { - // Didn't duplicate standard out. - _exit(EXIT_FAILURE); + continue; } - - if (dup2(iFdErr, STDERR_FILENO) == -1) + else { - // Didn't duplicate standard error. - _exit(EXIT_FAILURE); + break; } - - /* now close the original FDs, we don't need them anymore */ - close(iFdIn); - close(iFdOut); - close(iFdErr); } + } - /* execute the new process */ - - if (EnvironmentArray) + if (fd != -1) + { + flags = fcntl(fd, F_GETFL); + if (flags != -1) { - execve(lpFileNamePS, lppArgv, EnvironmentArray); + flags &= ~O_NONBLOCK; + if (fcntl(fd, F_SETFL, flags) == -1) + { + close(fd); + fd = -1; + } } else { - execve(lpFileNamePS, lppArgv, palEnvironment); + close(fd); + fd = -1; } - - /* if we get here, it means the execve function call failed so just exit */ - _exit(EXIT_FAILURE); - } - - /* parent process */ - - /* close the read end of the pipe, the parent doesn't need it */ - close(child_blocking_pipe); - - /* Set the process ID */ - pLocalData->dwProcessId = processId; - pLocalDataLock->ReleaseLock(pThread, TRUE); - pLocalDataLock = NULL; - - // - // Release file handle info; we don't need them anymore. Note that - // this must happen after we've released the data locks, as - // otherwise a deadlock could result. - // - - if (lpStartupInfo->dwFlags & STARTF_USESTDHANDLES) - { - pobjFileIn->ReleaseReference(pThread); - pobjFileIn = NULL; - pobjFileOut->ReleaseReference(pThread); - pobjFileOut = NULL; - pobjFileErr->ReleaseReference(pThread); - pobjFileErr = NULL; } - /* fill PROCESS_INFORMATION structure */ - lpProcessInformation->hProcess = hProcess; - lpProcessInformation->hThread = hDummyThread; - lpProcessInformation->dwProcessId = processId; - lpProcessInformation->dwThreadId_PAL_Undefined = 0; - - - TRACE("New process created: id=%#x\n", processId); - -InternalCreateProcessExit: + return fd; +} - if (NULL != pLocalDataLock) +static +void +ClosePipe(int fd) +{ + if (fd != -1) { - pLocalDataLock->ReleaseLock(pThread, FALSE); + while (close(fd) < 0 && errno == EINTR); } +} - if (NULL != pSharedDataLock) - { - pSharedDataLock->ReleaseLock(pThread, FALSE); - } +static +RuntimeEventsOverPipes +NotifyRuntimeUsingPipes() +{ + RuntimeEventsOverPipes result = RuntimeEventsOverPipes_Disabled; + char startupPipeName[MAX_DEBUGGER_TRANSPORT_PIPE_NAME_LENGTH]; + char continuePipeName[MAX_DEBUGGER_TRANSPORT_PIPE_NAME_LENGTH]; + int startupPipeFd = -1; + int continuePipeFd = -1; + size_t offset = 0; - if (NULL != pobjProcess) + LPCSTR applicationGroupId = PAL_GetApplicationGroupId(); + + PAL_GetTransportPipeName(continuePipeName, gPID, applicationGroupId, RuntimeContinuePipeName); + TRACE("NotifyRuntimeUsingPipes: opening continue '%s' pipe\n", continuePipeName); + + continuePipeFd = OpenPipe(continuePipeName, O_RDONLY); + if (continuePipeFd == -1) { - pobjProcess->ReleaseReference(pThread); + if (errno == ENOENT || errno == EACCES) + { + TRACE("NotifyRuntimeUsingPipes: pipe %s not found/accessible, runtime events over pipes disabled\n", continuePipeName); + } + else + { + TRACE("NotifyRuntimeUsingPipes: open(%s) failed: %d (%s)\n", continuePipeName, errno, strerror(errno)); + result = RuntimeEventsOverPipes_Failed; + } + + goto exit; } - if (NULL != pobjProcessRegistered) + PAL_GetTransportPipeName(startupPipeName, gPID, applicationGroupId, RuntimeStartupPipeName); + TRACE("NotifyRuntimeUsingPipes: opening startup '%s' pipe\n", startupPipeName); + + startupPipeFd = OpenPipe(startupPipeName, O_WRONLY); + if (startupPipeFd == -1) { - pobjProcessRegistered->ReleaseReference(pThread); + if (errno == ENOENT || errno == EACCES) + { + TRACE("NotifyRuntimeUsingPipes: pipe %s not found/accessible, runtime events over pipes disabled\n", startupPipeName); + } + else + { + TRACE("NotifyRuntimeUsingPipes: open(%s) failed: %d (%s)\n", startupPipeName, errno, strerror(errno)); + result = RuntimeEventsOverPipes_Failed; + } + + goto exit; } - if (NO_ERROR != palError) + TRACE("NotifyRuntimeUsingPipes: sending started event\n"); + { - if (NULL != hProcess) + unsigned char event = (unsigned char)RuntimeEvent_Started; + unsigned char *buffer = &event; + int bytesToWrite = sizeof(event); + int bytesWritten = 0; + + do { - g_pObjectManager->RevokeHandle(pThread, hProcess); + bytesWritten = write(startupPipeFd, buffer + offset, bytesToWrite - offset); + if (bytesWritten > 0) + { + offset += bytesWritten; + } } + while ((bytesWritten > 0 && offset < bytesToWrite) || (bytesWritten == -1 && errno == EINTR)); - if (NULL != hDummyThread) + if (offset != bytesToWrite) { - g_pObjectManager->RevokeHandle(pThread, hDummyThread); + TRACE("NotifyRuntimeUsingPipes: write(%s) failed: %d (%s)\n", startupPipeName, errno, strerror(errno)); + goto exit; } } - if (EnvironmentArray) - { - free(EnvironmentArray); - } + TRACE("NotifyRuntimeUsingPipes: waiting on continue event\n"); - /* if we still have the file structures at this point, it means we - encountered an error sometime between when we acquired them and when we - fork()ed. We not only have to release them, we have to give them back - their close-on-exec flag */ - if (NULL != pobjFileIn) { - if(-1 == fcntl(iFdIn, F_SETFD, 1)) + unsigned char event = (unsigned char)RuntimeEvent_Unknown; + unsigned char *buffer = &event; + int bytesToRead = sizeof(event); + int bytesRead = 0; + + offset = 0; + do { - WARN("couldn't restore close-on-exec flag to stdin descriptor! " - "errno is %d (%s)\n", errno, strerror(errno)); + bytesRead = read(continuePipeFd, buffer + offset, bytesToRead - offset); + if (bytesRead > 0) + { + offset += bytesRead; + } } - pobjFileIn->ReleaseReference(pThread); - } + while ((bytesRead > 0 && offset < bytesToRead) || (bytesRead == -1 && errno == EINTR)); - if (NULL != pobjFileOut) - { - if(-1 == fcntl(iFdOut, F_SETFD, 1)) + if (offset == bytesToRead && event == (unsigned char)RuntimeEvent_Continue) + { + TRACE("NotifyRuntimeUsingPipes: received continue event\n"); + } + else { - WARN("couldn't restore close-on-exec flag to stdout descriptor! " - "errno is %d (%s)\n", errno, strerror(errno)); + TRACE("NotifyRuntimeUsingPipes: received invalid event\n"); + goto exit; } - pobjFileOut->ReleaseReference(pThread); } - if (NULL != pobjFileErr) + result = RuntimeEventsOverPipes_Succeeded; + +exit: + + if (startupPipeFd != -1) { - if(-1 == fcntl(iFdErr, F_SETFD, 1)) - { - WARN("couldn't restore close-on-exec flag to stderr descriptor! " - "errno is %d (%s)\n", errno, strerror(errno)); - } - pobjFileErr->ReleaseReference(pThread); + ClosePipe(startupPipeFd); } - /* free allocated memory */ - if (lppArgv) + if (continuePipeFd != -1) { - free(*lppArgv); - free(lppArgv); + ClosePipe(continuePipeFd); } - return palError; -#endif // !TARGET_TVOS && !TARGET_WASM + return result; } +#endif // ENABLE_RUNTIME_EVENTS_OVER_PIPES - -/*++ -Function: - GetExitCodeProcess - -See MSDN doc. ---*/ +static BOOL -PALAPI -GetExitCodeProcess( - IN HANDLE hProcess, - IN LPDWORD lpExitCode) +NotifyRuntimeUsingSemaphores() { - CPalThread *pThread; - PAL_ERROR palError = NO_ERROR; - DWORD dwExitCode; - PROCESS_STATE ps; + char startupSemName[CLR_SEM_MAX_NAMELEN]; + char continueSemName[CLR_SEM_MAX_NAMELEN]; + sem_t *startupSem = SEM_FAILED; + sem_t *continueSem = SEM_FAILED; + BOOL launched = FALSE; - PERF_ENTRY(GetExitCodeProcess); - ENTRY("GetExitCodeProcess(hProcess = %p, lpExitCode = %p)\n", - hProcess, lpExitCode); + UINT64 processIdDisambiguationKey = 0; + BOOL ret = GetProcessIdDisambiguationKey(gPID, &processIdDisambiguationKey); - pThread = InternalGetCurrentThread(); + // If GetProcessIdDisambiguationKey failed for some reason, it should set the value + // to 0. We expect that anyone else making the semaphore name will also fail and thus + // will also try to use 0 as the value. + _ASSERTE(ret == TRUE || processIdDisambiguationKey == 0); - if(NULL == lpExitCode) - { - WARN("Got NULL lpExitCode\n"); - palError = ERROR_INVALID_PARAMETER; - goto done; - } + UnambiguousProcessDescriptor unambiguousProcessDescriptor(gPID, processIdDisambiguationKey); + LPCSTR applicationGroupId = PAL_GetApplicationGroupId(); + CreateSemaphoreName(startupSemName, RuntimeStartupSemaphoreName, unambiguousProcessDescriptor, applicationGroupId); + CreateSemaphoreName(continueSemName, RuntimeContinueSemaphoreName, unambiguousProcessDescriptor, applicationGroupId); - palError = PROCGetProcessStatus( - pThread, - hProcess, - &ps, - &dwExitCode - ); + TRACE("NotifyRuntimeUsingSemaphores: opening continue '%s' startup '%s'\n", continueSemName, startupSemName); - if (NO_ERROR != palError) + // Open the debugger startup semaphore. If it doesn't exists, then we do nothing and return + startupSem = sem_open(startupSemName, 0); + if (startupSem == SEM_FAILED) { - ASSERT("Couldn't get process status information!\n"); - goto done; + TRACE("NotifyRuntimeUsingSemaphores: sem_open(%s) failed: %d (%s)\n", startupSemName, errno, strerror(errno)); + goto exit; } - if( PS_DONE == ps ) + continueSem = sem_open(continueSemName, 0); + if (continueSem == SEM_FAILED) { - *lpExitCode = dwExitCode; + ASSERT("sem_open(%s) failed: %d (%s)\n", continueSemName, errno, strerror(errno)); + goto exit; } - else + + // Wake up the debugger waiting for startup + if (sem_post(startupSem) != 0) { - *lpExitCode = STILL_ACTIVE; + ASSERT("sem_post(startupSem) failed: errno is %d (%s)\n", errno, strerror(errno)); + goto exit; } -done: - - if (NO_ERROR != palError) + // Now wait until the debugger's runtime startup notification is finished + while (sem_wait(continueSem) != 0) { - pThread->SetLastError(palError); + if (EINTR == errno) + { + TRACE("NotifyRuntimeUsingSemaphores: sem_wait() failed with EINTR; re-waiting"); + continue; + } + ASSERT("sem_wait(continueSem) failed: errno is %d (%s)\n", errno, strerror(errno)); + goto exit; } - LOGEXIT("GetExitCodeProcess returns BOOL %d\n", NO_ERROR == palError); - PERF_EXIT(GetExitCodeProcess); + // Returns that the runtime was successfully launched for debugging + launched = TRUE; - return NO_ERROR == palError; +exit: + if (startupSem != SEM_FAILED) + { + sem_close(startupSem); + } + if (continueSem != SEM_FAILED) + { + sem_close(continueSem); + } + return launched; } /*++ -Function: - ExitProcess + PAL_NotifyRuntimeStarted -See MSDN doc. + Signals the debugger waiting for runtime startup notification to continue and + waits until the debugger signals us to continue. + +Parameters: + None + +Return value: + TRUE - successfully launched by debugger, FALSE - not launched or some failure in the handshake --*/ -PAL_NORETURN -VOID +BOOL PALAPI -ExitProcess( - IN UINT uExitCode) +PAL_NotifyRuntimeStarted() { - DWORD old_terminator; +#ifdef ENABLE_RUNTIME_EVENTS_OVER_PIPES + // Test pipes as runtime event transport. + RuntimeEventsOverPipes result = NotifyRuntimeUsingPipes(); + switch (result) + { + case RuntimeEventsOverPipes_Disabled: + TRACE("PAL_NotifyRuntimeStarted: pipe handshake disabled, try semaphores\n"); + return NotifyRuntimeUsingSemaphores(); + case RuntimeEventsOverPipes_Failed: + TRACE("PAL_NotifyRuntimeStarted: pipe handshake failed\n"); + return FALSE; + case RuntimeEventsOverPipes_Succeeded: + TRACE("PAL_NotifyRuntimeStarted: pipe handshake succeeded\n"); + return TRUE; + default: + // Unexpected result. + return FALSE; + } +#else + return NotifyRuntimeUsingSemaphores(); +#endif // ENABLE_RUNTIME_EVENTS_OVER_PIPES +} - PERF_ENTRY_ONLY(ExitProcess); - ENTRY("ExitProcess(uExitCode=0x%x)\n", uExitCode ); +LPCSTR +PALAPI +PAL_GetApplicationGroupId() +{ +#ifdef __APPLE__ + return gApplicationGroupId; +#else + return nullptr; +#endif +} - old_terminator = InterlockedCompareExchange(&terminator, GetCurrentThreadId(), 0); +#ifdef __APPLE__ - if (GetCurrentThreadId() == old_terminator) +// We use 7bits from each byte, so this computes the extra size we need to encode a given byte count +constexpr int GetExtraEncodedAreaSize(UINT rawByteCount) +{ + return (rawByteCount+6)/7; +} +const int SEMAPHORE_ENCODED_NAME_EXTRA_LENGTH = GetExtraEncodedAreaSize(sizeof(UnambiguousProcessDescriptor)); +const int SEMAPHORE_ENCODED_NAME_LENGTH = + sizeof(UnambiguousProcessDescriptor) + /* For process ID + disambiguationKey */ + SEMAPHORE_ENCODED_NAME_EXTRA_LENGTH; /* For base 255 extra encoding space */ + +static_assert(MAX_APPLICATION_GROUP_ID_LENGTH + + 1 /* For / */ + + 2 /* For ST/CO name prefix */ + + SEMAPHORE_ENCODED_NAME_LENGTH /* For encoded name string */ + + 1 /* For null terminator */ + <= CLR_SEM_MAX_NAMELEN); + +// In Apple we are limited by the length of the semaphore name. However, the characters which can be used in the +// name can be anything between 1 and 255 (since 0 will terminate the string). Thus, we encode each byte b in +// unambiguousProcessDescriptor as b ? b : 1, and mark an additional bit indicating if b is 0 or not. We use 7 bits +// out of each extra byte so 1 bit will always be '1'. This will ensure that our extra bytes are never 0 which are +// invalid characters. Thus we need an extra byte for each 7 input bytes. Hence, only extra 2 bytes for the name string. +void EncodeSemaphoreName(char *encodedSemName, const UnambiguousProcessDescriptor& unambiguousProcessDescriptor) +{ + const unsigned char *buffer = (const unsigned char *)&unambiguousProcessDescriptor; + char *extraEncodingBits = encodedSemName + sizeof(UnambiguousProcessDescriptor); + + // Reset the extra encoding bit area + for (int i=0; i 0 && length < CLR_SEM_MAX_NAMELEN); - /* Should not get here, because we terminate the current process */ - ASSERT("PROCEndProcess has returned\n"); + EncodeSemaphoreName(semName+length, unambiguousProcessDescriptor); + length += SEMAPHORE_ENCODED_NAME_LENGTH; + semName[length] = 0; } else +#endif // __APPLE__ { - exit(uExitCode); - - /* Should not get here, because we terminate the current process */ - ASSERT("exit has returned\n"); + length = sprintf_s( + semName, + CLR_SEM_MAX_NAMELEN, + RuntimeSemaphoreNameFormat, + semaphoreName, + HashSemaphoreName(unambiguousProcessDescriptor.m_processId, unambiguousProcessDescriptor.m_disambiguationKey)); } - /* this should never get executed */ - ASSERT("ExitProcess should not return!\n"); - while (true); + _ASSERTE(length > 0 && length < CLR_SEM_MAX_NAMELEN ); } /*++ -Function: - TerminateProcess - -Note: - hProcess is a handle on the current process. + Function: + GetProcessIdDisambiguationKey -See MSDN doc. + Get a numeric value that can be used to disambiguate between processes with the same PID, + provided that one of them is still running. The numeric value can mean different things + on different platforms, so it should not be used for any other purpose. Under the hood, + it is implemented based on the creation time of the process. --*/ BOOL -PALAPI -TerminateProcess( - IN HANDLE hProcess, - IN UINT uExitCode) +GetProcessIdDisambiguationKey(DWORD processId, UINT64 *disambiguationKey) { - BOOL ret; - - PERF_ENTRY(TerminateProcess); - ENTRY("TerminateProcess(hProcess=%p, uExitCode=%u)\n",hProcess, uExitCode ); - - ret = PROCEndProcess(hProcess, uExitCode, TRUE); - - LOGEXIT("TerminateProcess returns BOOL %d\n", ret); - PERF_EXIT(TerminateProcess); - return ret; -} + if (disambiguationKey == nullptr) + { + _ASSERTE(!"disambiguationKey argument cannot be null!"); + return FALSE; + } -/*++ -Function: - RaiseFailFastException + *disambiguationKey = 0; -See MSDN doc. ---*/ -VOID -PALAPI -DECLSPEC_NORETURN -RaiseFailFastException( - IN PEXCEPTION_RECORD pExceptionRecord, - IN PCONTEXT pContextRecord, - IN DWORD dwFlags) -{ - PERF_ENTRY(RaiseFailFastException); - ENTRY("RaiseFailFastException"); +#if defined(__APPLE__) || defined(__FreeBSD__) - TerminateCurrentProcessNoExit(TRUE); - for (;;) PROCAbort(); + // On OS X, we return the process start time expressed in Unix time (the number of seconds + // since the start of the Unix epoch). + struct kinfo_proc info = {}; + size_t size = sizeof(info); + int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, (int)processId }; + int ret = ::sysctl(mib, sizeof(mib)/sizeof(*mib), &info, &size, nullptr, 0); - LOGEXIT("RaiseFailFastException"); - PERF_EXIT(RaiseFailFastException); -} + if (ret == 0) + { +#if defined(__APPLE__) + timeval procStartTime = info.kp_proc.p_starttime; +#else // __FreeBSD__ + timeval procStartTime = info.ki_start; +#endif + long secondsSinceEpoch = procStartTime.tv_sec; -/*++ -Function: - PROCEndProcess + *disambiguationKey = secondsSinceEpoch; + return TRUE; + } + else + { + _ASSERTE(!"Failed to get start time of a process."); + return FALSE; + } - Called from TerminateProcess and ExitProcess. This does the work of - TerminateProcess, but also takes a flag that determines whether we - shut down unconditionally. If the flag is set, the PAL will do very - little extra work before exiting. Most importantly, it won't shut - down any DLLs that are loaded. +#elif defined(__NetBSD__) ---*/ -static BOOL PROCEndProcess(HANDLE hProcess, UINT uExitCode, BOOL bTerminateUnconditionally) -{ - DWORD dwProcessId; - BOOL ret = FALSE; + // On NetBSD, we return the process start time expressed in Unix time (the number of seconds + // since the start of the Unix epoch). + kvm_t *kd; + int cnt; + struct kinfo_proc2 *info; - dwProcessId = PROCGetProcessIDFromHandle(hProcess); - if (dwProcessId == 0) + kd = kvm_open(nullptr, nullptr, nullptr, KVM_NO_FILES, "kvm_open"); + if (kd == nullptr) { - SetLastError(ERROR_INVALID_HANDLE); + _ASSERTE(!"Failed to get start time of a process."); + return FALSE; } - else if(dwProcessId != GetCurrentProcessId()) - { - if (uExitCode != 0) - WARN("exit code 0x%x ignored for external process.\n", uExitCode); - if (kill(dwProcessId, SIGKILL) == 0) - { - ret = TRUE; - } - else - { - switch (errno) { - case ESRCH: - SetLastError(ERROR_INVALID_HANDLE); - break; - case EPERM: - SetLastError(ERROR_ACCESS_DENIED); - break; - default: - // Unexpected failure. - ASSERT(FALSE); - SetLastError(ERROR_INTERNAL_ERROR); - break; - } - } - } - else + info = kvm_getproc2(kd, KERN_PROC_PID, processId, sizeof(struct kinfo_proc2), &cnt); + if (info == nullptr || cnt < 1) { - // WARN/ERROR before starting the termination process and/or leaving the PAL. - if (bTerminateUnconditionally) - { - WARN("exit code 0x%x ignored for terminate.\n", uExitCode); - } - else if ((uExitCode & 0xff) != uExitCode) - { - // TODO: Convert uExitCodes into sysexits(3)? - ERROR("exit() only supports the lower 8-bits of an exit code. " - "status will only see error 0x%x instead of 0x%x.\n", uExitCode & 0xff, uExitCode); - } + kvm_close(kd); + _ASSERTE(!"Failed to get start time of a process."); + return FALSE; + } - TerminateCurrentProcessNoExit(bTerminateUnconditionally); + kvm_close(kd); - LOGEXIT("PROCEndProcess will not return\n"); + long secondsSinceEpoch = info->p_ustart_sec; + *disambiguationKey = secondsSinceEpoch; - if (bTerminateUnconditionally) - { - // abort() has the semantics that - // (1) it doesn't run atexit handlers - // (2) can invoke CrashReporter or produce a coredump, which is appropriate for TerminateProcess calls - // TerminationRequestHandlingRoutine in synchmanager.cpp sets the exit code to this special value. The - // Watson analyzer needs to know that the process was terminated with a SIGTERM. - PROCAbort(uExitCode == (128 + SIGTERM) ? SIGTERM : SIGABRT); - } - else - { - exit(uExitCode); - } + return TRUE; - ASSERT(FALSE); // we shouldn't get here +#elif defined(__HAIKU__) + + // On Haiku, we return the process start time expressed in microseconds since boot time. + + team_info info; + + if (get_team_info(processId, &info) == B_OK) + { + *disambiguationKey = info.start_time; + return TRUE; + } + else + { + WARN("Failed to get start time of a process."); + return FALSE; } - return ret; +#elif HAVE_PROCFS_STAT + + // Here we read /proc//stat file to get the start time for the process. + // We return this value (which is expressed in jiffies since boot time). + + // Making something like: /proc/123/stat + char statFileName[64]; + + INDEBUG(int chars = ) + snprintf(statFileName, sizeof(statFileName), "/proc/%d/stat", processId); + _ASSERTE(chars > 0 && chars <= (int)sizeof(statFileName)); + + FILE *statFile = fopen(statFileName, "r"); + if (statFile == nullptr) + { + TRACE("GetProcessIdDisambiguationKey: fopen() FAILED"); + SetLastError(ERROR_INVALID_HANDLE); + return FALSE; + } + + char *line = nullptr; + size_t lineLen = 0; + if (getline(&line, &lineLen, statFile) == -1) + { + TRACE("GetProcessIdDisambiguationKey: getline() FAILED"); + SetLastError(ERROR_INVALID_HANDLE); + free(line); + fclose(statFile); + return FALSE; + } + + unsigned long long starttime; + + // According to `man proc`, the second field in the stat file is the filename of the executable, + // in parentheses. Tokenizing the stat file using spaces as separators breaks when that name + // has spaces in it, so we start using sscanf_s after skipping everything up to and including the + // last closing paren and the space after it. + char *scanStartPosition = strrchr(line, ')') + 2; + + // All the format specifiers for the fields in the stat file are provided by 'man proc'. + int sscanfRet = sscanf_s(scanStartPosition, + "%*c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %*u %*u %*d %*d %*d %*d %*d %*d %llu \n", + &starttime); + + free(line); + fclose(statFile); + + if (sscanfRet != 1) + { + _ASSERTE(!"Failed to parse stat file contents with sscanf_s."); + return FALSE; + } + + *disambiguationKey = starttime; + return TRUE; + +#else + // If this is not OS X and we don't have /proc, we just return FALSE. + WARN("GetProcessIdDisambiguationKey was called but is not implemented on this platform!"); + return FALSE; +#endif } /*++ -Function: - PAL_SetShutdownCallback - -Abstract: - Sets a callback that is executed when the PAL is shut down because of - ExitProcess, TerminateProcess or PAL_Shutdown but not PAL_Terminate/Ex. + Function: + PAL_GetTransportName - NOTE: Currently only one callback can be set at a time. + Builds the transport IPC names from the process id. --*/ -PALIMPORT VOID PALAPI -PAL_SetShutdownCallback( - IN PSHUTDOWN_CALLBACK callback) +PAL_GetTransportName( + const unsigned int MAX_TRANSPORT_NAME_LENGTH, + OUT char *name, + IN const char *prefix, + IN DWORD id, + IN const char *applicationGroupId, + IN const char *suffix) { - _ASSERTE(g_shutdownCallback == nullptr); - g_shutdownCallback = callback; + *name = '\0'; + DWORD dwRetVal = 0; + UINT64 disambiguationKey = 0; + PathCharString formatBufferString; + BOOL ret = GetProcessIdDisambiguationKey(id, &disambiguationKey); + char *formatBuffer = formatBufferString.OpenStringBuffer(MAX_TRANSPORT_NAME_LENGTH-1); + if (formatBuffer == nullptr) + { + ERROR("Out Of Memory"); + return; + } + + // If GetProcessIdDisambiguationKey failed for some reason, it should set the value + // to 0. We expect that anyone else making the pipe name will also fail and thus will + // also try to use 0 as the value. + _ASSERTE(ret == TRUE || disambiguationKey == 0); +#ifdef __APPLE__ + if (nullptr != applicationGroupId) + { + // Verify the length of the application group ID + int applicationGroupIdLength = strlen(applicationGroupId); + if (applicationGroupIdLength > MAX_APPLICATION_GROUP_ID_LENGTH) + { + ERROR("The length of applicationGroupId is larger than MAX_APPLICATION_GROUP_ID_LENGTH"); + return; + } + + // In sandbox, all IPC files (locks, pipes) should be written to the application group + // container. The path returned by GetTempPathA will be unique for each process and cannot + // be used for IPC between two different processes + if (!GetApplicationContainerFolder(formatBufferString, applicationGroupId, applicationGroupIdLength)) + { + ERROR("Out Of Memory"); + return; + } + + // Verify the size of the path won't exceed maximum allowed size + if (formatBufferString.GetCount() >= MAX_TRANSPORT_NAME_LENGTH) + { + ERROR("GetApplicationContainerFolder returned a path that was larger than MAX_TRANSPORT_NAME_LENGTH"); + return; + } + } + else +#endif // __APPLE__ + { + // Get a temp file location + dwRetVal = ::GetTempPathA(MAX_TRANSPORT_NAME_LENGTH, formatBuffer); + if (dwRetVal == 0) + { + ERROR("GetTempPath failed (0x%08x)", ::GetLastError()); + return; + } + if (dwRetVal > MAX_TRANSPORT_NAME_LENGTH) + { + ERROR("GetTempPath returned a path that was larger than MAX_TRANSPORT_NAME_LENGTH"); + return; + } + } + + if (strncat_s(formatBuffer, MAX_TRANSPORT_NAME_LENGTH, IpcNameFormat, strlen(IpcNameFormat)) == STRUNCATE) + { + ERROR("TransportPipeName was larger than MAX_TRANSPORT_NAME_LENGTH"); + return; + } + + int chars = snprintf(name, MAX_TRANSPORT_NAME_LENGTH, formatBuffer, prefix, id, disambiguationKey, suffix); + _ASSERTE(chars > 0 && (unsigned int)chars < MAX_TRANSPORT_NAME_LENGTH); } /*++ -Function: - PAL_SetCreateDumpCallback - -Abstract: - Sets a callback that is executed when create dump is launched to create a crash dump. + Function: + PAL_GetTransportPipeName - NOTE: Currently only one callback can be set at a time. + Builds the transport pipe names from the process id. --*/ -PALIMPORT VOID PALAPI -PAL_SetCreateDumpCallback( - IN PCREATEDUMP_CALLBACK callback) +PAL_GetTransportPipeName( + OUT char *name, + IN DWORD id, + IN const char *applicationGroupId, + IN const char *suffix) { - _ASSERTE(g_createdumpCallback == nullptr); - g_createdumpCallback = callback; + PAL_GetTransportName( + MAX_DEBUGGER_TRANSPORT_PIPE_NAME_LENGTH, + name, + TwoWayNamedPipePrefix, + id, + applicationGroupId, + suffix); } /*++ Function: - PAL_SetLogManagedCallstackForSignalCallback - -Abstract: - Sets a callback that is executed when a signal is received to log the managed callstack. - Used by Android CoreCLR since CreateDump is not supported on Android. + GetCommandLineW - NOTE: Currently only one callback can be set at a time. +See MSDN doc. --*/ -PALIMPORT -VOID +LPWSTR PALAPI -PAL_SetLogManagedCallstackForSignalCallback( - IN PLOGMANAGEDCALLSTACKFORSIGNAL_CALLBACK callback) +GetCommandLineW( + VOID) { - _ASSERTE(g_logManagedCallstackForSignalCallback == nullptr); - g_logManagedCallstackForSignalCallback = callback; -} + PERF_ENTRY(GetCommandLineW); + ENTRY("GetCommandLineW()\n"); -// Build the semaphore names using the PID and a value that can be used for distinguishing -// between processes with the same PID (which ran at different times). This is to avoid -// cases where a prior process with the same PID exited abnormally without having a chance -// to clean up its semaphore. -// Note to anyone modifying these names in the future: Semaphore names on OS X are limited -// to SEM_NAME_LEN characters, including null. SEM_NAME_LEN is 31 (at least on OS X 10.11). -// NetBSD limits semaphore names to 15 characters, including null (at least up to 7.99.25). -// Keep 31 length for Core 1.0 RC2 compatibility -#if defined(__NetBSD__) -static const char* RuntimeSemaphoreNameFormat = "/clr%s%08llx"; -#else -static const char* RuntimeSemaphoreNameFormat = "/clr%s%08x%016llx"; -#endif + LPWSTR lpwstr = g_lpwstrCmdLine ? g_lpwstrCmdLine : (LPWSTR)W(""); -static const char* RuntimeStartupSemaphoreName = "st"; -static const char* RuntimeContinueSemaphoreName = "co"; + LOGEXIT("GetCommandLineW returns LPWSTR %p (%S)\n", + g_lpwstrCmdLine, + lpwstr); + PERF_EXIT(GetCommandLineW); -#if defined(__NetBSD__) -static uint64_t HashSemaphoreName(uint64_t a, uint64_t b) -{ - return (a ^ b) & 0xffffffff; + return lpwstr; } -#else -#define HashSemaphoreName(a,b) a,b -#endif -static const char *const TwoWayNamedPipePrefix = "clr-debug-pipe"; -static const char* IpcNameFormat = "%s-%d-%llu-%s"; - -#ifdef ENABLE_RUNTIME_EVENTS_OVER_PIPES -static const char* RuntimeStartupPipeName = "st"; -static const char* RuntimeContinuePipeName = "co"; - -#define PIPE_OPEN_RETRY_DELAY_NS 500000000 // 500 ms +/*++ +Function: + OpenProcess -typedef enum -{ - RuntimeEventsOverPipes_Disabled = 0, - RuntimeEventsOverPipes_Succeeded = 1, - RuntimeEventsOverPipes_Failed = 2, -} RuntimeEventsOverPipes; +See MSDN doc. -typedef enum +Notes : +dwDesiredAccess is ignored (all supported operations will be allowed) +bInheritHandle is ignored (no inheritance) +--*/ +HANDLE +PALAPI +OpenProcess( + DWORD dwDesiredAccess, + BOOL bInheritHandle, + DWORD dwProcessId) { - RuntimeEvent_Unknown = 0, - RuntimeEvent_Started = 1, - RuntimeEvent_Continue = 2, -} RuntimeEvent; + PAL_ERROR palError; + CPalThread *pThread; + IPalObject *pobjProcess = NULL; + IPalObject *pobjProcessRegistered = NULL; + IDataLock *pDataLock; + CProcProcessLocalData *pLocalData; + CObjectAttributes oa; + HANDLE hProcess = NULL; -static -int -OpenPipe(const char* name, int mode) -{ - int fd = -1; - int flags = mode | O_NONBLOCK; + PERF_ENTRY(OpenProcess); + ENTRY("OpenProcess(dwDesiredAccess=0x%08x, bInheritHandle=%d, " + "dwProcessId = 0x%08x)\n", + dwDesiredAccess, bInheritHandle, dwProcessId ); -#if defined(FD_CLOEXEC) - flags |= O_CLOEXEC; -#endif + pThread = InternalGetCurrentThread(); - while (fd == -1) + if (0 == dwProcessId) { - fd = open(name, flags); - if (fd == -1) - { - if (mode == O_WRONLY && errno == ENXIO) - { - PAL_nanosleep(PIPE_OPEN_RETRY_DELAY_NS); - continue; - } - else if (errno == EINTR) - { - continue; - } - else - { - break; - } - } + palError = ERROR_INVALID_PARAMETER; + goto OpenProcessExit; } - if (fd != -1) + palError = g_pObjectManager->AllocateObject( + pThread, + &otProcess, + &oa, + &pobjProcess + ); + + if (NO_ERROR != palError) { - flags = fcntl(fd, F_GETFL); - if (flags != -1) - { - flags &= ~O_NONBLOCK; - if (fcntl(fd, F_SETFL, flags) == -1) - { - close(fd); - fd = -1; - } - } - else - { - close(fd); - fd = -1; - } + goto OpenProcessExit; } - return fd; -} + palError = pobjProcess->GetProcessLocalData( + pThread, + WriteLock, + &pDataLock, + reinterpret_cast(&pLocalData) + ); -static -void -ClosePipe(int fd) -{ - if (fd != -1) + if (NO_ERROR != palError) { - while (close(fd) < 0 && errno == EINTR); + goto OpenProcessExit; } -} -static -RuntimeEventsOverPipes -NotifyRuntimeUsingPipes() -{ - RuntimeEventsOverPipes result = RuntimeEventsOverPipes_Disabled; - char startupPipeName[MAX_DEBUGGER_TRANSPORT_PIPE_NAME_LENGTH]; - char continuePipeName[MAX_DEBUGGER_TRANSPORT_PIPE_NAME_LENGTH]; - int startupPipeFd = -1; - int continuePipeFd = -1; - size_t offset = 0; + pLocalData->dwProcessId = dwProcessId; + pDataLock->ReleaseLock(pThread, TRUE); - LPCSTR applicationGroupId = PAL_GetApplicationGroupId(); + palError = g_pObjectManager->RegisterObject( + pThread, + pobjProcess, + &aotProcess, + &hProcess, + &pobjProcessRegistered + ); - PAL_GetTransportPipeName(continuePipeName, gPID, applicationGroupId, RuntimeContinuePipeName); - TRACE("NotifyRuntimeUsingPipes: opening continue '%s' pipe\n", continuePipeName); + // + // pobjProcess was invalidated by the above call, so NULL + // it out here + // - continuePipeFd = OpenPipe(continuePipeName, O_RDONLY); - if (continuePipeFd == -1) - { - if (errno == ENOENT || errno == EACCES) - { - TRACE("NotifyRuntimeUsingPipes: pipe %s not found/accessible, runtime events over pipes disabled\n", continuePipeName); - } - else - { - TRACE("NotifyRuntimeUsingPipes: open(%s) failed: %d (%s)\n", continuePipeName, errno, strerror(errno)); - result = RuntimeEventsOverPipes_Failed; - } + pobjProcess = NULL; - goto exit; - } + // + // TODO: check to see if the process actually exists? + // - PAL_GetTransportPipeName(startupPipeName, gPID, applicationGroupId, RuntimeStartupPipeName); - TRACE("NotifyRuntimeUsingPipes: opening startup '%s' pipe\n", startupPipeName); +OpenProcessExit: - startupPipeFd = OpenPipe(startupPipeName, O_WRONLY); - if (startupPipeFd == -1) + if (NULL != pobjProcess) { - if (errno == ENOENT || errno == EACCES) - { - TRACE("NotifyRuntimeUsingPipes: pipe %s not found/accessible, runtime events over pipes disabled\n", startupPipeName); - } - else - { - TRACE("NotifyRuntimeUsingPipes: open(%s) failed: %d (%s)\n", startupPipeName, errno, strerror(errno)); - result = RuntimeEventsOverPipes_Failed; - } - - goto exit; + pobjProcess->ReleaseReference(pThread); } - TRACE("NotifyRuntimeUsingPipes: sending started event\n"); - + if (NULL != pobjProcessRegistered) { - unsigned char event = (unsigned char)RuntimeEvent_Started; - unsigned char *buffer = &event; - int bytesToWrite = sizeof(event); - int bytesWritten = 0; - - do - { - bytesWritten = write(startupPipeFd, buffer + offset, bytesToWrite - offset); - if (bytesWritten > 0) - { - offset += bytesWritten; - } - } - while ((bytesWritten > 0 && offset < bytesToWrite) || (bytesWritten == -1 && errno == EINTR)); - - if (offset != bytesToWrite) - { - TRACE("NotifyRuntimeUsingPipes: write(%s) failed: %d (%s)\n", startupPipeName, errno, strerror(errno)); - goto exit; - } + pobjProcessRegistered->ReleaseReference(pThread); } - TRACE("NotifyRuntimeUsingPipes: waiting on continue event\n"); - + if (NO_ERROR != palError) { - unsigned char event = (unsigned char)RuntimeEvent_Unknown; - unsigned char *buffer = &event; - int bytesToRead = sizeof(event); - int bytesRead = 0; - - offset = 0; - do - { - bytesRead = read(continuePipeFd, buffer + offset, bytesToRead - offset); - if (bytesRead > 0) - { - offset += bytesRead; - } - } - while ((bytesRead > 0 && offset < bytesToRead) || (bytesRead == -1 && errno == EINTR)); - - if (offset == bytesToRead && event == (unsigned char)RuntimeEvent_Continue) - { - TRACE("NotifyRuntimeUsingPipes: received continue event\n"); - } - else - { - TRACE("NotifyRuntimeUsingPipes: received invalid event\n"); - goto exit; - } + pThread->SetLastError(palError); } - result = RuntimeEventsOverPipes_Succeeded; + LOGEXIT("OpenProcess returns HANDLE %p\n", hProcess); + PERF_EXIT(OpenProcess); + return hProcess; +} -exit: +/*++ +Function + PROCNotifyProcessShutdown - if (startupPipeFd != -1) + Calls the abort handler to do any shutdown cleanup. Call be called + from the unhandled native exception handler. + +(no return value) +--*/ +VOID +PROCNotifyProcessShutdown(bool isExecutingOnAltStack) +{ + // Call back into the coreclr to clean up the debugger transport pipes + PSHUTDOWN_CALLBACK callback = InterlockedExchangePointer(&g_shutdownCallback, NULL); + if (callback != NULL) { - ClosePipe(startupPipeFd); + callback(isExecutingOnAltStack); } +} - if (continuePipeFd != -1) - { - ClosePipe(continuePipeFd); - } +/*++ +Function + PROCNotifyProcessShutdownDestructor - return result; -} -#endif // ENABLE_RUNTIME_EVENTS_OVER_PIPES + Called at process exit, invokes process shutdown notification -static -BOOL -NotifyRuntimeUsingSemaphores() +(no return value) +--*/ +__attribute__((destructor)) +VOID +PROCNotifyProcessShutdownDestructor() { - char startupSemName[CLR_SEM_MAX_NAMELEN]; - char continueSemName[CLR_SEM_MAX_NAMELEN]; - sem_t *startupSem = SEM_FAILED; - sem_t *continueSem = SEM_FAILED; - BOOL launched = FALSE; - - UINT64 processIdDisambiguationKey = 0; - BOOL ret = GetProcessIdDisambiguationKey(gPID, &processIdDisambiguationKey); - - // If GetProcessIdDisambiguationKey failed for some reason, it should set the value - // to 0. We expect that anyone else making the semaphore name will also fail and thus - // will also try to use 0 as the value. - _ASSERTE(ret == TRUE || processIdDisambiguationKey == 0); + PROCNotifyProcessShutdown(/* isExecutingOnAltStack */ false); +} - UnambiguousProcessDescriptor unambiguousProcessDescriptor(gPID, processIdDisambiguationKey); - LPCSTR applicationGroupId = PAL_GetApplicationGroupId(); - CreateSemaphoreName(startupSemName, RuntimeStartupSemaphoreName, unambiguousProcessDescriptor, applicationGroupId); - CreateSemaphoreName(continueSemName, RuntimeContinueSemaphoreName, unambiguousProcessDescriptor, applicationGroupId); +/*++ +Function: + PROCFormatInt - TRACE("NotifyRuntimeUsingSemaphores: opening continue '%s' startup '%s'\n", continueSemName, startupSemName); + Helper function to format an ULONG32 as a string. - // Open the debugger startup semaphore. If it doesn't exists, then we do nothing and return - startupSem = sem_open(startupSemName, 0); - if (startupSem == SEM_FAILED) +--*/ +char* +PROCFormatInt(ULONG32 value) +{ + char* buffer = (char*)malloc(128); + if (buffer != nullptr) { - TRACE("NotifyRuntimeUsingSemaphores: sem_open(%s) failed: %d (%s)\n", startupSemName, errno, strerror(errno)); - goto exit; + if (sprintf_s(buffer, 128, "%d", value) == -1) + { + free(buffer); + buffer = nullptr; + } } + return buffer; +} - continueSem = sem_open(continueSemName, 0); - if (continueSem == SEM_FAILED) - { - ASSERT("sem_open(%s) failed: %d (%s)\n", continueSemName, errno, strerror(errno)); - goto exit; - } +/*++ +Function: + PROCFormatInt64 - // Wake up the debugger waiting for startup - if (sem_post(startupSem) != 0) - { - ASSERT("sem_post(startupSem) failed: errno is %d (%s)\n", errno, strerror(errno)); - goto exit; - } + Helper function to format an ULONG64 as a string. - // Now wait until the debugger's runtime startup notification is finished - while (sem_wait(continueSem) != 0) +--*/ +char* +PROCFormatInt64(ULONG64 value) +{ + char* buffer = (char*)malloc(128); + if (buffer != nullptr) { - if (EINTR == errno) + if (sprintf_s(buffer, 128, "%lld", value) == -1) { - TRACE("NotifyRuntimeUsingSemaphores: sem_wait() failed with EINTR; re-waiting"); - continue; + free(buffer); + buffer = nullptr; } - ASSERT("sem_wait(continueSem) failed: errno is %d (%s)\n", errno, strerror(errno)); - goto exit; - } - - // Returns that the runtime was successfully launched for debugging - launched = TRUE; - -exit: - if (startupSem != SEM_FAILED) - { - sem_close(startupSem); - } - if (continueSem != SEM_FAILED) - { - sem_close(continueSem); } - return launched; + return buffer; } /*++ - PAL_NotifyRuntimeStarted +Function + PROCBuildCreateDumpCommandLine - Signals the debugger waiting for runtime startup notification to continue and - waits until the debugger signals us to continue. +Abstract + Builds the createdump command line from the arguments. -Parameters: - None +Return + TRUE - succeeds, FALSE - fails -Return value: - TRUE - successfully launched by debugger, FALSE - not launched or some failure in the handshake --*/ BOOL -PALAPI -PAL_NotifyRuntimeStarted() +PROCBuildCreateDumpCommandLine( + const char* argv[], + char** pprogram, + char** ppidarg, + const char* dumpName, + const char* logFileName, + INT dumpType, + ULONG32 flags) { -#ifdef ENABLE_RUNTIME_EVENTS_OVER_PIPES - // Test pipes as runtime event transport. - RuntimeEventsOverPipes result = NotifyRuntimeUsingPipes(); - switch (result) + if (g_szCoreCLRPath == nullptr) { - case RuntimeEventsOverPipes_Disabled: - TRACE("PAL_NotifyRuntimeStarted: pipe handshake disabled, try semaphores\n"); - return NotifyRuntimeUsingSemaphores(); - case RuntimeEventsOverPipes_Failed: - TRACE("PAL_NotifyRuntimeStarted: pipe handshake failed\n"); - return FALSE; - case RuntimeEventsOverPipes_Succeeded: - TRACE("PAL_NotifyRuntimeStarted: pipe handshake succeeded\n"); - return TRUE; - default: - // Unexpected result. return FALSE; } -#else - return NotifyRuntimeUsingSemaphores(); -#endif // ENABLE_RUNTIME_EVENTS_OVER_PIPES -} - -LPCSTR -PALAPI -PAL_GetApplicationGroupId() -{ -#ifdef __APPLE__ - return gApplicationGroupId; -#else - return nullptr; -#endif -} - -#ifdef __APPLE__ - -// We use 7bits from each byte, so this computes the extra size we need to encode a given byte count -constexpr int GetExtraEncodedAreaSize(UINT rawByteCount) -{ - return (rawByteCount+6)/7; -} -const int SEMAPHORE_ENCODED_NAME_EXTRA_LENGTH = GetExtraEncodedAreaSize(sizeof(UnambiguousProcessDescriptor)); -const int SEMAPHORE_ENCODED_NAME_LENGTH = - sizeof(UnambiguousProcessDescriptor) + /* For process ID + disambiguationKey */ - SEMAPHORE_ENCODED_NAME_EXTRA_LENGTH; /* For base 255 extra encoding space */ - -static_assert(MAX_APPLICATION_GROUP_ID_LENGTH - + 1 /* For / */ - + 2 /* For ST/CO name prefix */ - + SEMAPHORE_ENCODED_NAME_LENGTH /* For encoded name string */ - + 1 /* For null terminator */ - <= CLR_SEM_MAX_NAMELEN); - -// In Apple we are limited by the length of the semaphore name. However, the characters which can be used in the -// name can be anything between 1 and 255 (since 0 will terminate the string). Thus, we encode each byte b in -// unambiguousProcessDescriptor as b ? b : 1, and mark an additional bit indicating if b is 0 or not. We use 7 bits -// out of each extra byte so 1 bit will always be '1'. This will ensure that our extra bytes are never 0 which are -// invalid characters. Thus we need an extra byte for each 7 input bytes. Hence, only extra 2 bytes for the name string. -void EncodeSemaphoreName(char *encodedSemName, const UnambiguousProcessDescriptor& unambiguousProcessDescriptor) -{ - const unsigned char *buffer = (const unsigned char *)&unambiguousProcessDescriptor; - char *extraEncodingBits = encodedSemName + sizeof(UnambiguousProcessDescriptor); - - // Reset the extra encoding bit area - for (int i=0; i 0 && length < CLR_SEM_MAX_NAMELEN); - - EncodeSemaphoreName(semName+length, unambiguousProcessDescriptor); - length += SEMAPHORE_ENCODED_NAME_LENGTH; - semName[length] = 0; + *(last + 1) = '\0'; } else -#endif // __APPLE__ { - length = sprintf_s( - semName, - CLR_SEM_MAX_NAMELEN, - RuntimeSemaphoreNameFormat, - semaphoreName, - HashSemaphoreName(unambiguousProcessDescriptor.m_processId, unambiguousProcessDescriptor.m_disambiguationKey)); + program[0] = '\0'; } - - _ASSERTE(length > 0 && length < CLR_SEM_MAX_NAMELEN ); -} - -/*++ - Function: - GetProcessIdDisambiguationKey - - Get a numeric value that can be used to disambiguate between processes with the same PID, - provided that one of them is still running. The numeric value can mean different things - on different platforms, so it should not be used for any other purpose. Under the hood, - it is implemented based on the creation time of the process. ---*/ -BOOL -GetProcessIdDisambiguationKey(DWORD processId, UINT64 *disambiguationKey) -{ - if (disambiguationKey == nullptr) + if (strcat_s(program, programLen, DumpGeneratorName) != SAFECRT_SUCCESS) { - _ASSERTE(!"disambiguationKey argument cannot be null!"); return FALSE; } - - *disambiguationKey = 0; - -#if defined(__APPLE__) || defined(__FreeBSD__) - - // On OS X, we return the process start time expressed in Unix time (the number of seconds - // since the start of the Unix epoch). - struct kinfo_proc info = {}; - size_t size = sizeof(info); - int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, (int)processId }; - int ret = ::sysctl(mib, sizeof(mib)/sizeof(*mib), &info, &size, nullptr, 0); - - if (ret == 0) - { -#if defined(__APPLE__) - timeval procStartTime = info.kp_proc.p_starttime; -#else // __FreeBSD__ - timeval procStartTime = info.ki_start; -#endif - long secondsSinceEpoch = procStartTime.tv_sec; - - *disambiguationKey = secondsSinceEpoch; - return TRUE; - } - else + *ppidarg = PROCFormatInt(gPID); + if (*ppidarg == nullptr) { - _ASSERTE(!"Failed to get start time of a process."); return FALSE; } -#elif defined(__NetBSD__) - - // On NetBSD, we return the process start time expressed in Unix time (the number of seconds - // since the start of the Unix epoch). - kvm_t *kd; - int cnt; - struct kinfo_proc2 *info; + int argc = 0; + argv[argc++] = program; - kd = kvm_open(nullptr, nullptr, nullptr, KVM_NO_FILES, "kvm_open"); - if (kd == nullptr) + if (dumpName != nullptr) { - _ASSERTE(!"Failed to get start time of a process."); - return FALSE; + argv[argc++] = "--name"; + argv[argc++] = dumpName; } - info = kvm_getproc2(kd, KERN_PROC_PID, processId, sizeof(struct kinfo_proc2), &cnt); - if (info == nullptr || cnt < 1) + switch (dumpType) { - kvm_close(kd); - _ASSERTE(!"Failed to get start time of a process."); - return FALSE; + case DumpTypeNormal: + argv[argc++] = "--normal"; + break; + case DumpTypeWithHeap: + argv[argc++] = "--withheap"; + break; + case DumpTypeTriage: + argv[argc++] = "--triage"; + break; + case DumpTypeFull: + argv[argc++] = "--full"; + break; + default: + break; } - kvm_close(kd); - - long secondsSinceEpoch = info->p_ustart_sec; - *disambiguationKey = secondsSinceEpoch; - - return TRUE; - -#elif defined(__HAIKU__) - - // On Haiku, we return the process start time expressed in microseconds since boot time. - - team_info info; - - if (get_team_info(processId, &info) == B_OK) + if (flags & GenerateDumpFlagsLoggingEnabled) { - *disambiguationKey = info.start_time; - return TRUE; + argv[argc++] = "--diag"; } - else + + if (flags & GenerateDumpFlagsVerboseLoggingEnabled) { - WARN("Failed to get start time of a process."); - return FALSE; + argv[argc++] = "--verbose"; } -#elif HAVE_PROCFS_STAT - - // Here we read /proc//stat file to get the start time for the process. - // We return this value (which is expressed in jiffies since boot time). - - // Making something like: /proc/123/stat - char statFileName[64]; - - INDEBUG(int chars = ) - snprintf(statFileName, sizeof(statFileName), "/proc/%d/stat", processId); - _ASSERTE(chars > 0 && chars <= (int)sizeof(statFileName)); - - FILE *statFile = fopen(statFileName, "r"); - if (statFile == nullptr) + if (flags & GenerateDumpFlagsCrashReportEnabled) { - TRACE("GetProcessIdDisambiguationKey: fopen() FAILED"); - SetLastError(ERROR_INVALID_HANDLE); - return FALSE; + argv[argc++] = "--crashreport"; } - char *line = nullptr; - size_t lineLen = 0; - if (getline(&line, &lineLen, statFile) == -1) + if (flags & GenerateDumpFlagsCrashReportOnlyEnabled) { - TRACE("GetProcessIdDisambiguationKey: getline() FAILED"); - SetLastError(ERROR_INVALID_HANDLE); - free(line); - fclose(statFile); - return FALSE; + argv[argc++] = "--crashreportonly"; } - unsigned long long starttime; - - // According to `man proc`, the second field in the stat file is the filename of the executable, - // in parentheses. Tokenizing the stat file using spaces as separators breaks when that name - // has spaces in it, so we start using sscanf_s after skipping everything up to and including the - // last closing paren and the space after it. - char *scanStartPosition = strrchr(line, ')') + 2; - - // All the format specifiers for the fields in the stat file are provided by 'man proc'. - int sscanfRet = sscanf_s(scanStartPosition, - "%*c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %*u %*u %*d %*d %*d %*d %*d %*d %llu \n", - &starttime); - - free(line); - fclose(statFile); + if (g_running_in_exe) + { + argv[argc++] = "--singlefile"; + } - if (sscanfRet != 1) + if (logFileName != nullptr) { - _ASSERTE(!"Failed to parse stat file contents with sscanf_s."); - return FALSE; + argv[argc++] = "--logtofile"; + argv[argc++] = logFileName; } - *disambiguationKey = starttime; - return TRUE; + argv[argc++] = *ppidarg; -#else - // If this is not OS X and we don't have /proc, we just return FALSE. - WARN("GetProcessIdDisambiguationKey was called but is not implemented on this platform!"); - return FALSE; -#endif + argv[argc] = nullptr; + _ASSERTE(argc < MAX_ARGV_ENTRIES); + + return TRUE; } /*++ - Function: - PAL_GetTransportName +Function: + PROCCreateCrashDump - Builds the transport IPC names from the process id. + Creates crash dump of the process. Can be called from the unhandled + native exception handler. Allows only one thread to generate the core + dump if serialize is true. + +Return: + TRUE - succeeds, FALSE - fails --*/ -VOID -PALAPI -PAL_GetTransportName( - const unsigned int MAX_TRANSPORT_NAME_LENGTH, - OUT char *name, - IN const char *prefix, - IN DWORD id, - IN const char *applicationGroupId, - IN const char *suffix) +BOOL +PROCCreateCrashDump( + const char* argv[], + LPSTR errorMessageBuffer, + INT cbErrorMessageBuffer, + bool serialize) { - *name = '\0'; - DWORD dwRetVal = 0; - UINT64 disambiguationKey = 0; - PathCharString formatBufferString; - BOOL ret = GetProcessIdDisambiguationKey(id, &disambiguationKey); - char *formatBuffer = formatBufferString.OpenStringBuffer(MAX_TRANSPORT_NAME_LENGTH-1); - if (formatBuffer == nullptr) +#if defined(TARGET_IOS) || defined(TARGET_TVOS) || defined(TARGET_WASM) + return FALSE; +#else + _ASSERTE(argv[0] != nullptr); + _ASSERTE(errorMessageBuffer == nullptr || cbErrorMessageBuffer > 0); + + if (serialize) { - ERROR("Out Of Memory"); - return; + size_t currentThreadId = THREADSilentGetCurrentThreadId(); + size_t previousThreadId = InterlockedCompareExchange(&g_crashingThreadId, currentThreadId, 0); + if (previousThreadId != 0) + { + // Return error if reenter this code + if (previousThreadId == currentThreadId) + { + return false; + } + + // The first thread generates the crash info and any other threads are blocked + while (true) + { + poll(NULL, 0, INFTIM); + } + } } - // If GetProcessIdDisambiguationKey failed for some reason, it should set the value - // to 0. We expect that anyone else making the pipe name will also fail and thus will - // also try to use 0 as the value. - _ASSERTE(ret == TRUE || disambiguationKey == 0); -#ifdef __APPLE__ - if (nullptr != applicationGroupId) + int pipe_descs[4]; + if (pipe(pipe_descs) == -1 || pipe(pipe_descs + 2) == -1) { - // Verify the length of the application group ID - int applicationGroupIdLength = strlen(applicationGroupId); - if (applicationGroupIdLength > MAX_APPLICATION_GROUP_ID_LENGTH) + if (errorMessageBuffer != nullptr) { - ERROR("The length of applicationGroupId is larger than MAX_APPLICATION_GROUP_ID_LENGTH"); - return; + sprintf_s(errorMessageBuffer, cbErrorMessageBuffer, "Problem launching createdump: pipe() FAILED %s (%d)\n", strerror(errno), errno); } + return false; + } - // In sandbox, all IPC files (locks, pipes) should be written to the application group - // container. The path returned by GetTempPathA will be unique for each process and cannot - // be used for IPC between two different processes - if (!GetApplicationContainerFolder(formatBufferString, applicationGroupId, applicationGroupIdLength)) + // from parent (write) to child (read), used to signal prctl(PR_SET_PTRACER, childpid) is done + int child_read_pipe = pipe_descs[0]; + int parent_write_pipe = pipe_descs[1]; + // from child (write) to parent (read), used to capture createdump's stderr + int parent_read_pipe = pipe_descs[2]; + int child_write_pipe = pipe_descs[3]; + + // Fork the core dump child process. + pid_t childpid = fork(); + + // If error, write an error to trace log and abort + if (childpid == -1) + { + if (errorMessageBuffer != nullptr) { - ERROR("Out Of Memory"); - return; + sprintf_s(errorMessageBuffer, cbErrorMessageBuffer, "Problem launching createdump: fork() FAILED %s (%d)\n", strerror(errno), errno); } - - // Verify the size of the path won't exceed maximum allowed size - if (formatBufferString.GetCount() >= MAX_TRANSPORT_NAME_LENGTH) + for (int i = 0; i < 4; i++) { - ERROR("GetApplicationContainerFolder returned a path that was larger than MAX_TRANSPORT_NAME_LENGTH"); - return; + close(pipe_descs[i]); } + return false; } - else -#endif // __APPLE__ - { - // Get a temp file location - dwRetVal = ::GetTempPathA(MAX_TRANSPORT_NAME_LENGTH, formatBuffer); - if (dwRetVal == 0) - { - ERROR("GetTempPath failed (0x%08x)", ::GetLastError()); - return; - } - if (dwRetVal > MAX_TRANSPORT_NAME_LENGTH) - { - ERROR("GetTempPath returned a path that was larger than MAX_TRANSPORT_NAME_LENGTH"); - return; - } - } - - if (strncat_s(formatBuffer, MAX_TRANSPORT_NAME_LENGTH, IpcNameFormat, strlen(IpcNameFormat)) == STRUNCATE) - { - ERROR("TransportPipeName was larger than MAX_TRANSPORT_NAME_LENGTH"); - return; - } - - int chars = snprintf(name, MAX_TRANSPORT_NAME_LENGTH, formatBuffer, prefix, id, disambiguationKey, suffix); - _ASSERTE(chars > 0 && (unsigned int)chars < MAX_TRANSPORT_NAME_LENGTH); -} - -/*++ - Function: - PAL_GetTransportPipeName - - Builds the transport pipe names from the process id. ---*/ -VOID -PALAPI -PAL_GetTransportPipeName( - OUT char *name, - IN DWORD id, - IN const char *applicationGroupId, - IN const char *suffix) -{ - PAL_GetTransportName( - MAX_DEBUGGER_TRANSPORT_PIPE_NAME_LENGTH, - name, - TwoWayNamedPipePrefix, - id, - applicationGroupId, - suffix); -} - -/*++ -Function: - GetCommandLineW - -See MSDN doc. ---*/ -LPWSTR -PALAPI -GetCommandLineW( - VOID) -{ - PERF_ENTRY(GetCommandLineW); - ENTRY("GetCommandLineW()\n"); - - LPWSTR lpwstr = g_lpwstrCmdLine ? g_lpwstrCmdLine : (LPWSTR)W(""); - - LOGEXIT("GetCommandLineW returns LPWSTR %p (%S)\n", - g_lpwstrCmdLine, - lpwstr); - PERF_EXIT(GetCommandLineW); - - return lpwstr; -} - -/*++ -Function: - OpenProcess - -See MSDN doc. - -Notes : -dwDesiredAccess is ignored (all supported operations will be allowed) -bInheritHandle is ignored (no inheritance) ---*/ -HANDLE -PALAPI -OpenProcess( - DWORD dwDesiredAccess, - BOOL bInheritHandle, - DWORD dwProcessId) -{ - PAL_ERROR palError; - CPalThread *pThread; - IPalObject *pobjProcess = NULL; - IPalObject *pobjProcessRegistered = NULL; - IDataLock *pDataLock; - CProcProcessLocalData *pLocalData; - CObjectAttributes oa; - HANDLE hProcess = NULL; - - PERF_ENTRY(OpenProcess); - ENTRY("OpenProcess(dwDesiredAccess=0x%08x, bInheritHandle=%d, " - "dwProcessId = 0x%08x)\n", - dwDesiredAccess, bInheritHandle, dwProcessId ); - - pThread = InternalGetCurrentThread(); - - if (0 == dwProcessId) - { - palError = ERROR_INVALID_PARAMETER; - goto OpenProcessExit; - } - - palError = g_pObjectManager->AllocateObject( - pThread, - &otProcess, - &oa, - &pobjProcess - ); - - if (NO_ERROR != palError) - { - goto OpenProcessExit; - } - - palError = pobjProcess->GetProcessLocalData( - pThread, - WriteLock, - &pDataLock, - reinterpret_cast(&pLocalData) - ); - - if (NO_ERROR != palError) - { - goto OpenProcessExit; - } - - pLocalData->dwProcessId = dwProcessId; - pDataLock->ReleaseLock(pThread, TRUE); - - palError = g_pObjectManager->RegisterObject( - pThread, - pobjProcess, - &aotProcess, - &hProcess, - &pobjProcessRegistered - ); - - // - // pobjProcess was invalidated by the above call, so NULL - // it out here - // - - pobjProcess = NULL; - - // - // TODO: check to see if the process actually exists? - // - -OpenProcessExit: - - if (NULL != pobjProcess) - { - pobjProcess->ReleaseReference(pThread); - } - - if (NULL != pobjProcessRegistered) - { - pobjProcessRegistered->ReleaseReference(pThread); - } - - if (NO_ERROR != palError) - { - pThread->SetLastError(palError); - } - - LOGEXIT("OpenProcess returns HANDLE %p\n", hProcess); - PERF_EXIT(OpenProcess); - return hProcess; -} - -/*++ -Function - PROCNotifyProcessShutdown - - Calls the abort handler to do any shutdown cleanup. Call be called - from the unhandled native exception handler. - -(no return value) ---*/ -VOID -PROCNotifyProcessShutdown(bool isExecutingOnAltStack) -{ - // Call back into the coreclr to clean up the debugger transport pipes - PSHUTDOWN_CALLBACK callback = InterlockedExchangePointer(&g_shutdownCallback, NULL); - if (callback != NULL) - { - callback(isExecutingOnAltStack); - } -} - -/*++ -Function - PROCNotifyProcessShutdownDestructor - - Called at process exit, invokes process shutdown notification - -(no return value) ---*/ -__attribute__((destructor)) -VOID -PROCNotifyProcessShutdownDestructor() -{ - PROCNotifyProcessShutdown(/* isExecutingOnAltStack */ false); -} - -/*++ -Function: - PROCFormatInt - - Helper function to format an ULONG32 as a string. - ---*/ -char* -PROCFormatInt(ULONG32 value) -{ - char* buffer = (char*)malloc(128); - if (buffer != nullptr) - { - if (sprintf_s(buffer, 128, "%d", value) == -1) - { - free(buffer); - buffer = nullptr; - } - } - return buffer; -} - -/*++ -Function: - PROCFormatInt64 - - Helper function to format an ULONG64 as a string. - ---*/ -char* -PROCFormatInt64(ULONG64 value) -{ - char* buffer = (char*)malloc(128); - if (buffer != nullptr) - { - if (sprintf_s(buffer, 128, "%lld", value) == -1) - { - free(buffer); - buffer = nullptr; - } - } - return buffer; -} - -/*++ -Function - PROCBuildCreateDumpCommandLine - -Abstract - Builds the createdump command line from the arguments. - -Return - TRUE - succeeds, FALSE - fails - ---*/ -BOOL -PROCBuildCreateDumpCommandLine( - const char* argv[], - char** pprogram, - char** ppidarg, - const char* dumpName, - const char* logFileName, - INT dumpType, - ULONG32 flags) -{ - if (g_szCoreCLRPath == nullptr) - { - return FALSE; - } - const char* DumpGeneratorName = "createdump"; - int programLen = strlen(g_szCoreCLRPath) + strlen(DumpGeneratorName) + 1; - char* program = *pprogram = (char*)malloc(programLen); - if (program == nullptr) - { - return FALSE; - } - if (strcpy_s(program, programLen, g_szCoreCLRPath) != SAFECRT_SUCCESS) - { - return FALSE; - } - char *last = strrchr(program, '/'); - if (last != nullptr) - { - *(last + 1) = '\0'; - } - else - { - program[0] = '\0'; - } - if (strcat_s(program, programLen, DumpGeneratorName) != SAFECRT_SUCCESS) - { - return FALSE; - } - *ppidarg = PROCFormatInt(gPID); - if (*ppidarg == nullptr) - { - return FALSE; - } - - int argc = 0; - argv[argc++] = program; - - if (dumpName != nullptr) - { - argv[argc++] = "--name"; - argv[argc++] = dumpName; - } - - switch (dumpType) - { - case DumpTypeNormal: - argv[argc++] = "--normal"; - break; - case DumpTypeWithHeap: - argv[argc++] = "--withheap"; - break; - case DumpTypeTriage: - argv[argc++] = "--triage"; - break; - case DumpTypeFull: - argv[argc++] = "--full"; - break; - default: - break; - } - - if (flags & GenerateDumpFlagsLoggingEnabled) - { - argv[argc++] = "--diag"; - } - - if (flags & GenerateDumpFlagsVerboseLoggingEnabled) - { - argv[argc++] = "--verbose"; - } - - if (flags & GenerateDumpFlagsCrashReportEnabled) - { - argv[argc++] = "--crashreport"; - } - - if (flags & GenerateDumpFlagsCrashReportOnlyEnabled) - { - argv[argc++] = "--crashreportonly"; - } - - if (g_running_in_exe) - { - argv[argc++] = "--singlefile"; - } - - if (logFileName != nullptr) - { - argv[argc++] = "--logtofile"; - argv[argc++] = logFileName; - } - - argv[argc++] = *ppidarg; - - argv[argc] = nullptr; - _ASSERTE(argc < MAX_ARGV_ENTRIES); - - return TRUE; -} - -/*++ -Function: - PROCCreateCrashDump - - Creates crash dump of the process. Can be called from the unhandled - native exception handler. Allows only one thread to generate the core - dump if serialize is true. - -Return: - TRUE - succeeds, FALSE - fails ---*/ -BOOL -PROCCreateCrashDump( - const char* argv[], - LPSTR errorMessageBuffer, - INT cbErrorMessageBuffer, - bool serialize) -{ -#if defined(TARGET_IOS) || defined(TARGET_TVOS) || defined(TARGET_WASM) - return FALSE; -#else - _ASSERTE(argv[0] != nullptr); - _ASSERTE(errorMessageBuffer == nullptr || cbErrorMessageBuffer > 0); - - if (serialize) - { - size_t currentThreadId = THREADSilentGetCurrentThreadId(); - size_t previousThreadId = InterlockedCompareExchange(&g_crashingThreadId, currentThreadId, 0); - if (previousThreadId != 0) - { - // Return error if reenter this code - if (previousThreadId == currentThreadId) - { - return false; - } - - // The first thread generates the crash info and any other threads are blocked - while (true) - { - poll(NULL, 0, INFTIM); - } - } - } - - int pipe_descs[4]; - if (pipe(pipe_descs) == -1 || pipe(pipe_descs + 2) == -1) - { - if (errorMessageBuffer != nullptr) - { - sprintf_s(errorMessageBuffer, cbErrorMessageBuffer, "Problem launching createdump: pipe() FAILED %s (%d)\n", strerror(errno), errno); - } - return false; - } - - // from parent (write) to child (read), used to signal prctl(PR_SET_PTRACER, childpid) is done - int child_read_pipe = pipe_descs[0]; - int parent_write_pipe = pipe_descs[1]; - // from child (write) to parent (read), used to capture createdump's stderr - int parent_read_pipe = pipe_descs[2]; - int child_write_pipe = pipe_descs[3]; - - // Fork the core dump child process. - pid_t childpid = fork(); - - // If error, write an error to trace log and abort - if (childpid == -1) - { - if (errorMessageBuffer != nullptr) - { - sprintf_s(errorMessageBuffer, cbErrorMessageBuffer, "Problem launching createdump: fork() FAILED %s (%d)\n", strerror(errno), errno); - } - for (int i = 0; i < 4; i++) - { - close(pipe_descs[i]); - } - return false; - } - else if (childpid == 0) - { - int callbackResult = 0; - - close(parent_read_pipe); - close(parent_write_pipe); - - // Wait for prctl(PR_SET_PTRACER, childpid) in parent - char buffer; - int bytesRead; - while((bytesRead = read(child_read_pipe, &buffer, 1)) < 0 && errno == EINTR); - close(child_read_pipe); - - if (bytesRead != 1) - { - fprintf(stderr, "Problem reading from createdump child_read_pipe: %s (%d)\n", strerror(errno), errno); - close(child_write_pipe); - exit(-1); - } - - // Only dup the child's stderr if there is error buffer - if (errorMessageBuffer != nullptr) - { - dup2(child_write_pipe, STDERR_FILENO); - } - if (g_createdumpCallback != nullptr) - { - // Remove the signal handlers inherited from the runtime process - SEHCleanupSignals(true /* isChildProcess */); - - // Call the statically linked createdump code - int argc = 0; - while (argv[argc] != nullptr && argc < MAX_ARGV_ENTRIES) - { - argc++; - } - callbackResult = g_createdumpCallback(argc, argv); - // Set the shutdown callback to nullptr and exit - // If we don't exit, the child's execution will continue into the diagnostic server behavior - // which causes all sorts of problems. - g_shutdownCallback = nullptr; - exit(callbackResult); - } - else - { - // Execute the createdump program - if (execve(argv[0], (char**)argv, palEnvironment) == -1) - { - fprintf(stderr, "Problem launching createdump (may not have execute permissions): execve(%s) FAILED %s (%d)\n", argv[0], strerror(errno), errno); - exit(-1); - } - } - } - else - { - close(child_read_pipe); - close(child_write_pipe); -#if HAVE_PRCTL_H && HAVE_PR_SET_PTRACER - // Gives the child process permission to use /proc//mem and ptrace - if (prctl(PR_SET_PTRACER, childpid, 0, 0, 0) == -1) - { - // Ignore any error because on some CentOS and OpenSUSE distros, it isn't - // supported but createdump works just fine. - ERROR("PROCCreateCrashDump: prctl() FAILED %s (%d)\n", strerror(errno), errno); - } -#endif // HAVE_PRCTL_H && HAVE_PR_SET_PTRACER - // Signal child that prctl(PR_SET_PTRACER, childpid) is done - int bytesWritten; - while((bytesWritten = write(parent_write_pipe, "S", 1)) < 0 && errno == EINTR); - close(parent_write_pipe); - - if (bytesWritten != 1) - { - fprintf(stderr, "Problem writing to createdump parent_write_pipe: %s (%d)\n", strerror(errno), errno); - close(parent_read_pipe); - if (errorMessageBuffer != nullptr) - { - errorMessageBuffer[0] = 0; - } - return false; - } - - // Read createdump's stderr messages (if any) - if (errorMessageBuffer != nullptr) - { - // Read createdump's stderr - int bytesRead = 0; - int count = 0; - while ((count = read(parent_read_pipe, errorMessageBuffer + bytesRead, cbErrorMessageBuffer - bytesRead)) > 0) - { - bytesRead += count; - } - errorMessageBuffer[bytesRead] = 0; - if (bytesRead > 0) - { - fputs(errorMessageBuffer, stderr); - } - } - close(parent_read_pipe); - - // Parent waits until the child process is done - int wstatus = 0; - int result = waitpid(childpid, &wstatus, 0); - if (result != childpid) - { - fprintf(stderr, "Problem waiting for createdump: waitpid() FAILED result %d wstatus %08x errno %s (%d)\n", - result, wstatus, strerror(errno), errno); - return false; - } - else - { -#ifdef _DEBUG - fprintf(stderr, "waitpid() returned successfully (wstatus %08x) WEXITSTATUS %x WTERMSIG %x\n", wstatus, WEXITSTATUS(wstatus), WTERMSIG(wstatus)); -#endif - return !WIFEXITED(wstatus) || WEXITSTATUS(wstatus) == 0; - } - } - return true; -#endif // !TARGET_IOS && !TARGET_TVOS && !TARGET_WASM -} - -/*++ -Function - PROCAbortInitialize() - -Abstract - Initialize the process abort crash dump program file path and - name. Doing all of this ahead of time so nothing is allocated - or copied in PROCAbort/signal handler. - -Return - TRUE - succeeds, FALSE - fails - ---*/ -BOOL -PROCAbortInitialize() -{ - CLRConfigNoCache enabledCfg = CLRConfigNoCache::Get("DbgEnableMiniDump", /*noprefix*/ false, &getenv); - - DWORD enabled = 0; - if (enabledCfg.IsSet() && enabledCfg.TryAsInteger(10, enabled) && enabled) - { - CLRConfigNoCache dmpNameCfg = CLRConfigNoCache::Get("DbgMiniDumpName", /*noprefix*/ false, &getenv); - const char* dumpName = dmpNameCfg.IsSet() ? dmpNameCfg.AsString() : nullptr; - - CLRConfigNoCache dmpLogToFileCfg = CLRConfigNoCache::Get("CreateDumpLogToFile", /*noprefix*/ false, &getenv); - const char* logFilePath = dmpLogToFileCfg.IsSet() ? dmpLogToFileCfg.AsString() : nullptr; - - CLRConfigNoCache dmpTypeCfg = CLRConfigNoCache::Get("DbgMiniDumpType", /*noprefix*/ false, &getenv); - DWORD dumpType = DumpTypeUnknown; - if (dmpTypeCfg.IsSet()) - { - (void)dmpTypeCfg.TryAsInteger(10, dumpType); - if (dumpType <= DumpTypeUnknown || dumpType > DumpTypeMax) - { - dumpType = DumpTypeUnknown; - } - } - - ULONG32 flags = GenerateDumpFlagsNone; - CLRConfigNoCache createDumpDiag = CLRConfigNoCache::Get("CreateDumpDiagnostics", /*noprefix*/ false, &getenv); - DWORD val = 0; - if (createDumpDiag.IsSet() && createDumpDiag.TryAsInteger(10, val) && val == 1) - { - flags |= GenerateDumpFlagsLoggingEnabled; - } - CLRConfigNoCache createDumpVerboseDiag = CLRConfigNoCache::Get("CreateDumpVerboseDiagnostics", /*noprefix*/ false, &getenv); - val = 0; - if (createDumpVerboseDiag.IsSet() && createDumpVerboseDiag.TryAsInteger(10, val) && val == 1) - { - flags |= GenerateDumpFlagsVerboseLoggingEnabled; - } - CLRConfigNoCache enabledReportCfg = CLRConfigNoCache::Get("EnableCrashReport", /*noprefix*/ false, &getenv); - val = 0; - if (enabledReportCfg.IsSet() && enabledReportCfg.TryAsInteger(10, val) && val == 1) - { - flags |= GenerateDumpFlagsCrashReportEnabled; - } - CLRConfigNoCache enabledReportOnlyCfg = CLRConfigNoCache::Get("EnableCrashReportOnly", /*noprefix*/ false, &getenv); - val = 0; - if (enabledReportOnlyCfg.IsSet() && enabledReportOnlyCfg.TryAsInteger(10, val) && val == 1) - { - flags |= GenerateDumpFlagsCrashReportOnlyEnabled; - } - - char* program = nullptr; - char* pidarg = nullptr; - if (!PROCBuildCreateDumpCommandLine(g_argvCreateDump, &program, &pidarg, dumpName, logFilePath, dumpType, flags)) - { - return FALSE; - } - } - return TRUE; -} - -/*++ -Function: - PAL_GenerateCoreDump - -Abstract: - Public entry point to create a crash dump of the process. - -Parameters: - dumpName - dumpType: - Normal = 1, - WithHeap = 2, - Triage = 3, - Full = 4 - flags - See enum - -Return: - TRUE success - FALSE failed ---*/ -BOOL -PALAPI -PAL_GenerateCoreDump( - LPCSTR dumpName, - INT dumpType, - ULONG32 flags, - LPSTR errorMessageBuffer, - INT cbErrorMessageBuffer) -{ - const char* argvCreateDump[MAX_ARGV_ENTRIES] = { nullptr }; - - if (dumpType <= DumpTypeUnknown || dumpType > DumpTypeMax) - { - return FALSE; - } - if (dumpName != nullptr && dumpName[0] == '\0') - { - dumpName = nullptr; - } - char* program = nullptr; - char* pidarg = nullptr; - BOOL result = PROCBuildCreateDumpCommandLine(argvCreateDump, &program, &pidarg, dumpName, nullptr, dumpType, flags); - if (result) - { - result = PROCCreateCrashDump(argvCreateDump, errorMessageBuffer, cbErrorMessageBuffer, false); - } - free(program); - free(pidarg); - return result; -} - -// Helper function to prevent compiler from optimizing away a variable -__attribute__((noinline,NOOPT_ATTRIBUTE)) -static void DoNotOptimize(const void* p) -{ - // This function takes the address of a variable to ensure - // it's preserved and available in crash dumps - (void)p; -} - -static LPCWSTR GetSignalName(int signal) -{ - switch (signal) - { - case SIGSEGV: return W("SIGSEGV"); - case SIGBUS: return W("SIGBUS"); - case SIGFPE: return W("SIGFPE"); - case SIGILL: return W("SIGILL"); - case SIGABRT: return W("SIGABRT"); - case SIGTRAP: return W("SIGTRAP"); - case SIGTERM: return W("SIGTERM"); - default: return W("Unknown signal"); - } -} - -/*++ -Function: - PROCLogManagedCallstackForSignal - - Invokes the registered callback to log the managed callstack for a signal. - Used by Android since CreateDump is not supported there. - -Parameters: - signal - POSIX signal number - -(no return value) ---*/ -VOID -PROCLogManagedCallstackForSignal(int signal) -{ - if (g_logManagedCallstackForSignalCallback != nullptr) + else if (childpid == 0) { - LPCWSTR signalName = GetSignalName(signal); - g_logManagedCallstackForSignalCallback(signalName); - } -} - -/*++ -Function: - PROCCreateCrashDumpIfEnabled - - Creates crash dump of the process (if enabled). Can be - called from the unhandled native exception handler. - -Parameters: - signal - POSIX signal number - siginfo - POSIX signal info or nullptr - context - signal context or nullptr - serialize - allow only one thread to generate core dump - -(no return value) ---*/ -#ifdef HOST_ANDROID -#include -VOID -PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, bool serialize) -{ - // Preserve context pointer to prevent optimization - DoNotOptimize(&context); + int callbackResult = 0; - // TODO: Dump stress log into logcat and/or file when enabled? - minipal_log_write_fatal("Aborting process.\n"); -} -#else -VOID -PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, bool serialize) -{ - // Preserve context pointer to prevent optimization - DoNotOptimize(&context); + close(parent_read_pipe); + close(parent_write_pipe); - // If enabled, launch the create minidump utility and wait until it completes - if (g_argvCreateDump[0] != nullptr) - { - const char* argv[MAX_ARGV_ENTRIES]; - char* signalArg = nullptr; - char* crashThreadArg = nullptr; - char* signalCodeArg = nullptr; - char* signalErrnoArg = nullptr; - char* signalAddressArg = nullptr; + // Wait for prctl(PR_SET_PTRACER, childpid) in parent + char buffer; + int bytesRead; + while((bytesRead = read(child_read_pipe, &buffer, 1)) < 0 && errno == EINTR); + close(child_read_pipe); - // Copy the createdump argv - int argc = 0; - for (; argc < MAX_ARGV_ENTRIES && g_argvCreateDump[argc] != nullptr; argc++) + if (bytesRead != 1) { - argv[argc] = g_argvCreateDump[argc]; + fprintf(stderr, "Problem reading from createdump child_read_pipe: %s (%d)\n", strerror(errno), errno); + close(child_write_pipe); + exit(-1); } - if (signal != 0) + // Only dup the child's stderr if there is error buffer + if (errorMessageBuffer != nullptr) { - // Add the signal number to the command line - signalArg = PROCFormatInt(signal); - if (signalArg != nullptr) - { - argv[argc++] = "--signal"; - argv[argc++] = signalArg; - } + dup2(child_write_pipe, STDERR_FILENO); + } + if (g_createdumpCallback != nullptr) + { + // Remove the signal handlers inherited from the runtime process + SEHCleanupSignals(true /* isChildProcess */); - // Add the current thread id to the command line. This function is always called on the crashing thread. - crashThreadArg = PROCFormatInt(THREADSilentGetCurrentThreadId()); - if (crashThreadArg != nullptr) + // Call the statically linked createdump code + int argc = 0; + while (argv[argc] != nullptr && argc < MAX_ARGV_ENTRIES) { - argv[argc++] = "--crashthread"; - argv[argc++] = crashThreadArg; + argc++; } - - if (siginfo != nullptr) + callbackResult = g_createdumpCallback(argc, argv); + // Set the shutdown callback to nullptr and exit + // If we don't exit, the child's execution will continue into the diagnostic server behavior + // which causes all sorts of problems. + g_shutdownCallback = nullptr; + exit(callbackResult); + } + else + { + // Execute the createdump program + if (execve(argv[0], (char**)argv, palEnvironment) == -1) { - signalCodeArg = PROCFormatInt(siginfo->si_code); - if (signalCodeArg != nullptr) - { - argv[argc++] = "--code"; - argv[argc++] = signalCodeArg; - } - signalErrnoArg = PROCFormatInt(siginfo->si_errno); - if (signalErrnoArg != nullptr) - { - argv[argc++] = "--errno"; - argv[argc++] = signalErrnoArg; - } - signalAddressArg = PROCFormatInt64((ULONG64)siginfo->si_addr); - if (signalAddressArg != nullptr) - { - argv[argc++] = "--address"; - argv[argc++] = signalAddressArg; - } + fprintf(stderr, "Problem launching createdump (may not have execute permissions): execve(%s) FAILED %s (%d)\n", argv[0], strerror(errno), errno); + exit(-1); } } - - argv[argc] = nullptr; - _ASSERTE(argc < MAX_ARGV_ENTRIES); - - PROCCreateCrashDump(argv, nullptr, 0, serialize); - - free(signalArg); - free(crashThreadArg); - free(signalCodeArg); - free(signalErrnoArg); - free(signalAddressArg); - } -} -#endif - -/*++ -Function: - PROCAbort() - - Aborts the process after calling the shutdown cleanup handler. This function - should be called instead of calling abort() directly. - -Parameters: - signal - POSIX signal number - context - signal context or nullptr - - Does not return ---*/ -#if !defined(HOST_ARM) -PAL_NORETURN -#endif -VOID -PROCAbort(int signal, siginfo_t* siginfo, void* context) -{ - // Do any shutdown cleanup before aborting or creating a core dump - PROCNotifyProcessShutdown(); - - PROCCreateCrashDumpIfEnabled(signal, siginfo, context, true); - - // Restore all signals; the SIGABORT handler to prevent recursion and - // the others to prevent multiple core dumps from being generated. - SEHCleanupSignals(false /* isChildProcess */); - - // Abort the process after waiting for the core dump to complete - abort(); -} - -#define FATAL_ASSERT(e, msg) \ - do \ - { \ - if (!(e)) \ - { \ - fprintf(stderr, "FATAL ERROR: " msg); \ - PROCAbort(); \ - } \ - } \ - while(0) - -/*++ -Function: - PROCGetProcessIDFromHandle - -Abstract - Return the process ID from a process handle - -Parameter - hProcess: process handle - -Return - Return the process ID, or 0 if it's not a valid handle ---*/ -DWORD -PROCGetProcessIDFromHandle( - HANDLE hProcess) -{ - PAL_ERROR palError; - IPalObject *pobjProcess = NULL; - CPalThread *pThread = InternalGetCurrentThread(); - - DWORD dwProcessId = 0; - - if (hPseudoCurrentProcess == hProcess) - { - dwProcessId = gPID; - goto PROCGetProcessIDFromHandleExit; } - - - palError = g_pObjectManager->ReferenceObjectByHandle( - pThread, - hProcess, - &aotProcess, - &pobjProcess - ); - - if (NO_ERROR == palError) + else { - IDataLock *pDataLock; - CProcProcessLocalData *pLocalData; + close(child_read_pipe); + close(child_write_pipe); +#if HAVE_PRCTL_H && HAVE_PR_SET_PTRACER + // Gives the child process permission to use /proc//mem and ptrace + if (prctl(PR_SET_PTRACER, childpid, 0, 0, 0) == -1) + { + // Ignore any error because on some CentOS and OpenSUSE distros, it isn't + // supported but createdump works just fine. + ERROR("PROCCreateCrashDump: prctl() FAILED %s (%d)\n", strerror(errno), errno); + } +#endif // HAVE_PRCTL_H && HAVE_PR_SET_PTRACER + // Signal child that prctl(PR_SET_PTRACER, childpid) is done + int bytesWritten; + while((bytesWritten = write(parent_write_pipe, "S", 1)) < 0 && errno == EINTR); + close(parent_write_pipe); - palError = pobjProcess->GetProcessLocalData( - pThread, - ReadLock, - &pDataLock, - reinterpret_cast(&pLocalData) - ); + if (bytesWritten != 1) + { + fprintf(stderr, "Problem writing to createdump parent_write_pipe: %s (%d)\n", strerror(errno), errno); + close(parent_read_pipe); + if (errorMessageBuffer != nullptr) + { + errorMessageBuffer[0] = 0; + } + return false; + } - if (NO_ERROR == palError) + // Read createdump's stderr messages (if any) + if (errorMessageBuffer != nullptr) { - dwProcessId = pLocalData->dwProcessId; - pDataLock->ReleaseLock(pThread, FALSE); + // Read createdump's stderr + int bytesRead = 0; + int count = 0; + while ((count = read(parent_read_pipe, errorMessageBuffer + bytesRead, cbErrorMessageBuffer - bytesRead)) > 0) + { + bytesRead += count; + } + errorMessageBuffer[bytesRead] = 0; + if (bytesRead > 0) + { + fputs(errorMessageBuffer, stderr); + } } + close(parent_read_pipe); - pobjProcess->ReleaseReference(pThread); + // Parent waits until the child process is done + int wstatus = 0; + int result = waitpid(childpid, &wstatus, 0); + if (result != childpid) + { + fprintf(stderr, "Problem waiting for createdump: waitpid() FAILED result %d wstatus %08x errno %s (%d)\n", + result, wstatus, strerror(errno), errno); + return false; + } + else + { +#ifdef _DEBUG + fprintf(stderr, "waitpid() returned successfully (wstatus %08x) WEXITSTATUS %x WTERMSIG %x\n", wstatus, WEXITSTATUS(wstatus), WTERMSIG(wstatus)); +#endif + return !WIFEXITED(wstatus) || WEXITSTATUS(wstatus) == 0; + } } - -PROCGetProcessIDFromHandleExit: - - return dwProcessId; + return true; +#endif // !TARGET_IOS && !TARGET_TVOS && !TARGET_WASM } /*++ Function - InitializeProcessCommandLine + PROCAbortInitialize() Abstract - Initializes (or re-initializes) the saved command line and exe path. - -Parameter - lpwstrCmdLine - lpwstrFullPath + Initialize the process abort crash dump program file path and + name. Doing all of this ahead of time so nothing is allocated + or copied in PROCAbort/signal handler. Return - PAL_ERROR + TRUE - succeeds, FALSE - fails -Notes - This function takes ownership of lpwstrCmdLine, but not of lpwstrFullPath --*/ - -PAL_ERROR -CorUnix::InitializeProcessCommandLine( - LPWSTR lpwstrCmdLine, - LPWSTR lpwstrFullPath -) +BOOL +PROCAbortInitialize() { - PAL_ERROR palError = NO_ERROR; - LPWSTR initial_dir = NULL; - - // - // Save the command line and initial directory - // + CLRConfigNoCache enabledCfg = CLRConfigNoCache::Get("DbgEnableMiniDump", /*noprefix*/ false, &getenv); - if (lpwstrFullPath) + DWORD enabled = 0; + if (enabledCfg.IsSet() && enabledCfg.TryAsInteger(10, enabled) && enabled) { - LPWSTR lpwstr = PAL_wcsrchr(lpwstrFullPath, '/'); - if (!lpwstr) + CLRConfigNoCache dmpNameCfg = CLRConfigNoCache::Get("DbgMiniDumpName", /*noprefix*/ false, &getenv); + const char* dumpName = dmpNameCfg.IsSet() ? dmpNameCfg.AsString() : nullptr; + + CLRConfigNoCache dmpLogToFileCfg = CLRConfigNoCache::Get("CreateDumpLogToFile", /*noprefix*/ false, &getenv); + const char* logFilePath = dmpLogToFileCfg.IsSet() ? dmpLogToFileCfg.AsString() : nullptr; + + CLRConfigNoCache dmpTypeCfg = CLRConfigNoCache::Get("DbgMiniDumpType", /*noprefix*/ false, &getenv); + DWORD dumpType = DumpTypeUnknown; + if (dmpTypeCfg.IsSet()) { - ERROR("Invalid full path\n"); - palError = ERROR_INTERNAL_ERROR; - goto exit; + (void)dmpTypeCfg.TryAsInteger(10, dumpType); + if (dumpType <= DumpTypeUnknown || dumpType > DumpTypeMax) + { + dumpType = DumpTypeUnknown; + } } - lpwstr[0] = '\0'; - size_t n = PAL_wcslen(lpwstrFullPath) + 1; - size_t iLen = n; - initial_dir = reinterpret_cast(malloc(iLen*sizeof(WCHAR))); - if (NULL == initial_dir) + ULONG32 flags = GenerateDumpFlagsNone; + CLRConfigNoCache createDumpDiag = CLRConfigNoCache::Get("CreateDumpDiagnostics", /*noprefix*/ false, &getenv); + DWORD val = 0; + if (createDumpDiag.IsSet() && createDumpDiag.TryAsInteger(10, val) && val == 1) { - ERROR("malloc() failed! (initial_dir) \n"); - palError = ERROR_NOT_ENOUGH_MEMORY; - goto exit; + flags |= GenerateDumpFlagsLoggingEnabled; } - - if (wcscpy_s(initial_dir, iLen, lpwstrFullPath) != SAFECRT_SUCCESS) + CLRConfigNoCache createDumpVerboseDiag = CLRConfigNoCache::Get("CreateDumpVerboseDiagnostics", /*noprefix*/ false, &getenv); + val = 0; + if (createDumpVerboseDiag.IsSet() && createDumpVerboseDiag.TryAsInteger(10, val) && val == 1) { - ERROR("wcscpy_s failed!\n"); - free(initial_dir); - palError = ERROR_INTERNAL_ERROR; - goto exit; + flags |= GenerateDumpFlagsVerboseLoggingEnabled; + } + CLRConfigNoCache enabledReportCfg = CLRConfigNoCache::Get("EnableCrashReport", /*noprefix*/ false, &getenv); + val = 0; + if (enabledReportCfg.IsSet() && enabledReportCfg.TryAsInteger(10, val) && val == 1) + { + flags |= GenerateDumpFlagsCrashReportEnabled; + } + CLRConfigNoCache enabledReportOnlyCfg = CLRConfigNoCache::Get("EnableCrashReportOnly", /*noprefix*/ false, &getenv); + val = 0; + if (enabledReportOnlyCfg.IsSet() && enabledReportOnlyCfg.TryAsInteger(10, val) && val == 1) + { + flags |= GenerateDumpFlagsCrashReportOnlyEnabled; } - lpwstr[0] = '/'; - - free(g_lpwstrAppDir); - g_lpwstrAppDir = initial_dir; + char* program = nullptr; + char* pidarg = nullptr; + if (!PROCBuildCreateDumpCommandLine(g_argvCreateDump, &program, &pidarg, dumpName, logFilePath, dumpType, flags)) + { + return FALSE; + } } - - free(g_lpwstrCmdLine); - g_lpwstrCmdLine = lpwstrCmdLine; - -exit: - return palError; + return TRUE; } - /*++ Function: - CreateInitialProcessAndThreadObjects + PAL_GenerateCoreDump -Abstract - Creates the IPalObjects that represent the current process - and the initial thread +Abstract: + Public entry point to create a crash dump of the process. -Parameter - pThread - the initial thread +Parameters: + dumpName + dumpType: + Normal = 1, + WithHeap = 2, + Triage = 3, + Full = 4 + flags + See enum -Return - PAL_ERROR +Return: + TRUE success + FALSE failed --*/ +BOOL +PALAPI +PAL_GenerateCoreDump( + LPCSTR dumpName, + INT dumpType, + ULONG32 flags, + LPSTR errorMessageBuffer, + INT cbErrorMessageBuffer) +{ + const char* argvCreateDump[MAX_ARGV_ENTRIES] = { nullptr }; -PAL_ERROR -CorUnix::CreateInitialProcessAndThreadObjects( - CPalThread *pThread - ) -{ - PAL_ERROR palError = NO_ERROR; - HANDLE hThread; - IPalObject *pobjProcess = NULL; - IDataLock *pDataLock; - CProcProcessLocalData *pLocalData; - CObjectAttributes oa; - HANDLE hProcess; - - // - // Create initial thread object - // - - palError = CreateThreadObject(pThread, pThread, &hThread); - if (NO_ERROR != palError) - { - goto CreateInitialProcessAndThreadObjectsExit; - } - - // - // This handle isn't needed - // - - (void) g_pObjectManager->RevokeHandle(pThread, hThread); - - // - // Create and initialize process object - // - - palError = g_pObjectManager->AllocateObject( - pThread, - &otProcess, - &oa, - &pobjProcess - ); - - if (NO_ERROR != palError) - { - ERROR("Unable to allocate process object"); - goto CreateInitialProcessAndThreadObjectsExit; - } - - palError = pobjProcess->GetProcessLocalData( - pThread, - WriteLock, - &pDataLock, - reinterpret_cast(&pLocalData) - ); - - if (NO_ERROR != palError) + if (dumpType <= DumpTypeUnknown || dumpType > DumpTypeMax) { - ASSERT("Unable to access local data"); - goto CreateInitialProcessAndThreadObjectsExit; + return FALSE; } - - pLocalData->dwProcessId = gPID; - pLocalData->ps = PS_RUNNING; - pDataLock->ReleaseLock(pThread, TRUE); - - palError = g_pObjectManager->RegisterObject( - pThread, - pobjProcess, - &aotProcess, - &hProcess, - &g_pobjProcess - ); - - // - // pobjProcess is invalidated by the call to RegisterObject, so - // NULL it out here to prevent it from being released later - // - - pobjProcess = NULL; - - if (NO_ERROR != palError) + if (dumpName != nullptr && dumpName[0] == '\0') { - ASSERT("Failure registering process object"); - goto CreateInitialProcessAndThreadObjectsExit; + dumpName = nullptr; } - - // - // There's no need to keep this handle around, so revoke - // it now - // - - g_pObjectManager->RevokeHandle(pThread, hProcess); - -CreateInitialProcessAndThreadObjectsExit: - - if (NULL != pobjProcess) + char* program = nullptr; + char* pidarg = nullptr; + BOOL result = PROCBuildCreateDumpCommandLine(argvCreateDump, &program, &pidarg, dumpName, nullptr, dumpType, flags); + if (result) { - pobjProcess->ReleaseReference(pThread); + result = PROCCreateCrashDump(argvCreateDump, errorMessageBuffer, cbErrorMessageBuffer, false); } + free(program); + free(pidarg); + return result; +} - return palError; +// Helper function to prevent compiler from optimizing away a variable +__attribute__((noinline,NOOPT_ATTRIBUTE)) +static void DoNotOptimize(const void* p) +{ + // This function takes the address of a variable to ensure + // it's preserved and available in crash dumps + (void)p; } +static LPCWSTR GetSignalName(int signal) +{ + switch (signal) + { + case SIGSEGV: return W("SIGSEGV"); + case SIGBUS: return W("SIGBUS"); + case SIGFPE: return W("SIGFPE"); + case SIGILL: return W("SIGILL"); + case SIGABRT: return W("SIGABRT"); + case SIGTRAP: return W("SIGTRAP"); + case SIGTERM: return W("SIGTERM"); + default: return W("Unknown signal"); + } +} /*++ Function: - PROCCleanupInitialProcess - -Abstract - Cleanup all the structures for the initial process. + PROCLogManagedCallstackForSignal -Parameter - VOID + Invokes the registered callback to log the managed callstack for a signal. + Used by Android since CreateDump is not supported there. -Return - VOID +Parameters: + signal - POSIX signal number +(no return value) --*/ VOID -PROCCleanupInitialProcess(VOID) +PROCLogManagedCallstackForSignal(int signal) { - /* Free the application directory */ - free(g_lpwstrAppDir); - - /* Free the stored command line */ - free(g_lpwstrCmdLine); - - // - // Object manager shutdown will handle freeing the underlying - // thread and process data - // + if (g_logManagedCallstackForSignalCallback != nullptr) + { + LPCWSTR signalName = GetSignalName(signal); + g_logManagedCallstackForSignalCallback(signalName); + } } /*++ Function: - TerminateCurrentProcessNoExit + PROCCreateCrashDumpIfEnabled -Abstract: - Terminate current Process, but leave the caller alive + Creates crash dump of the process (if enabled). Can be + called from the unhandled native exception handler. Parameters: - BOOL bTerminateUnconditionally - If this is set, the PAL will exit as - quickly as possible. In particular, it will not unload DLLs. - -Return value : - No return - -Note: - This function is used in ExitThread and TerminateProcess + signal - POSIX signal number + siginfo - POSIX signal info or nullptr + context - signal context or nullptr + serialize - allow only one thread to generate core dump +(no return value) --*/ +#ifdef HOST_ANDROID +#include VOID -CorUnix::TerminateCurrentProcessNoExit(BOOL bTerminateUnconditionally) +PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, bool serialize) { - BOOL locked; - DWORD old_terminator; + // Preserve context pointer to prevent optimization + DoNotOptimize(&context); - old_terminator = InterlockedCompareExchange(&terminator, GetCurrentThreadId(), 0); + // TODO: Dump stress log into logcat and/or file when enabled? + minipal_log_write_fatal("Aborting process.\n"); +} +#else +VOID +PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, bool serialize) +{ + // Preserve context pointer to prevent optimization + DoNotOptimize(&context); - if (0 != old_terminator && GetCurrentThreadId() != old_terminator) + // If enabled, launch the create minidump utility and wait until it completes + if (g_argvCreateDump[0] != nullptr) { - /* another thread has already initiated the termination process. we - could just block on the PALInitLock critical section, but then - PROCSuspendOtherThreads would hang... so sleep forever here, we're - terminating anyway + const char* argv[MAX_ARGV_ENTRIES]; + char* signalArg = nullptr; + char* crashThreadArg = nullptr; + char* signalCodeArg = nullptr; + char* signalErrnoArg = nullptr; + char* signalAddressArg = nullptr; - Update: [TODO] PROCSuspendOtherThreads has been removed. Can this - code be changed? */ + // Copy the createdump argv + int argc = 0; + for (; argc < MAX_ARGV_ENTRIES && g_argvCreateDump[argc] != nullptr; argc++) + { + argv[argc] = g_argvCreateDump[argc]; + } - /* note that if *this* thread has already started the termination - process, we want to proceed. the only way this can happen is if a - call to DllMain (from ExitProcess) brought us here (because DllMain - called ExitProcess, or TerminateProcess, or ExitThread); - TerminateProcess won't call DllMain, so there's no danger to get - caught in an infinite loop */ - WARN("termination already started from another thread; blocking.\n"); - while (true) + if (signal != 0) { - poll(NULL, 0, INFTIM); + // Add the signal number to the command line + signalArg = PROCFormatInt(signal); + if (signalArg != nullptr) + { + argv[argc++] = "--signal"; + argv[argc++] = signalArg; + } + + // Add the current thread id to the command line. This function is always called on the crashing thread. + crashThreadArg = PROCFormatInt(THREADSilentGetCurrentThreadId()); + if (crashThreadArg != nullptr) + { + argv[argc++] = "--crashthread"; + argv[argc++] = crashThreadArg; + } + + if (siginfo != nullptr) + { + signalCodeArg = PROCFormatInt(siginfo->si_code); + if (signalCodeArg != nullptr) + { + argv[argc++] = "--code"; + argv[argc++] = signalCodeArg; + } + signalErrnoArg = PROCFormatInt(siginfo->si_errno); + if (signalErrnoArg != nullptr) + { + argv[argc++] = "--errno"; + argv[argc++] = signalErrnoArg; + } + signalAddressArg = PROCFormatInt64((ULONG64)siginfo->si_addr); + if (signalAddressArg != nullptr) + { + argv[argc++] = "--address"; + argv[argc++] = signalAddressArg; + } + } } + + argv[argc] = nullptr; + _ASSERTE(argc < MAX_ARGV_ENTRIES); + + PROCCreateCrashDump(argv, nullptr, 0, serialize); + + free(signalArg); + free(crashThreadArg); + free(signalCodeArg); + free(signalErrnoArg); + free(signalAddressArg); } +} +#endif - /* Try to lock the initialization count to prevent multiple threads from - terminating/initializing the PAL simultaneously */ +/*++ +Function: + PROCAbort() - /* note : it's also important to take this lock before the process lock, - because Init/Shutdown take the init lock, and the functions they call - may take the process lock. We must do it in the same order to avoid - deadlocks */ + Aborts the process after calling the shutdown cleanup handler. This function + should be called instead of calling abort() directly. + +Parameters: + signal - POSIX signal number + context - signal context or nullptr + + Does not return +--*/ +#if !defined(HOST_ARM) +PAL_NORETURN +#endif +VOID +PROCAbort(int signal, siginfo_t* siginfo, void* context) +{ + // Do any shutdown cleanup before aborting or creating a core dump + PROCNotifyProcessShutdown(); + + PROCCreateCrashDumpIfEnabled(signal, siginfo, context, true); + + // Restore all signals; the SIGABORT handler to prevent recursion and + // the others to prevent multiple core dumps from being generated. + SEHCleanupSignals(false /* isChildProcess */); - locked = PALInitLock(); - if(locked && PALIsInitialized()) - { - PROCNotifyProcessShutdown(); - PALCommonCleanup(); - } + // Abort the process after waiting for the core dump to complete + abort(); } +#define FATAL_ASSERT(e, msg) \ + do \ + { \ + if (!(e)) \ + { \ + fprintf(stderr, "FATAL ERROR: " msg); \ + PROCAbort(); \ + } \ + } \ + while(0) + /*++ Function: - PROCGetProcessStatus + PROCGetProcessIDFromHandle -Abstract: - Retrieve process state information (state & exit code). +Abstract + Return the process ID from a process handle -Parameters: - DWORD process_id : PID of process to retrieve state for - PROCESS_STATE *state : state of process (starting, running, done) - DWORD *exit_code : exit code of process (from ExitProcess, etc.) +Parameter + hProcess: process handle -Return value : - TRUE on success +Return + Return the process ID, or 0 if it's not a valid handle --*/ -PAL_ERROR -PROCGetProcessStatus( - CPalThread *pThread, - HANDLE hProcess, - PROCESS_STATE *pps, - DWORD *pdwExitCode - ) +DWORD +PROCGetProcessIDFromHandle( + HANDLE hProcess) { - PAL_ERROR palError = NO_ERROR; + PAL_ERROR palError; IPalObject *pobjProcess = NULL; - IDataLock *pDataLock; - CProcProcessLocalData *pLocalData; - pid_t wait_retval; - int status; + CPalThread *pThread = InternalGetCurrentThread(); + + DWORD dwProcessId = 0; + + if (hPseudoCurrentProcess == hProcess) + { + dwProcessId = gPID; + goto PROCGetProcessIDFromHandleExit; + } - // - // First, check if we already know the status of this process. This will be - // the case if this function has already been called for the same process. - // palError = g_pObjectManager->ReferenceObjectByHandle( pThread, @@ -3309,831 +2235,517 @@ PROCGetProcessStatus( &pobjProcess ); - if (NO_ERROR != palError) + if (NO_ERROR == palError) { - goto PROCGetProcessStatusExit; + IDataLock *pDataLock; + CProcProcessLocalData *pLocalData; + + palError = pobjProcess->GetProcessLocalData( + pThread, + ReadLock, + &pDataLock, + reinterpret_cast(&pLocalData) + ); + + if (NO_ERROR == palError) + { + dwProcessId = pLocalData->dwProcessId; + pDataLock->ReleaseLock(pThread, FALSE); + } + + pobjProcess->ReleaseReference(pThread); } - palError = pobjProcess->GetProcessLocalData( - pThread, - WriteLock, - &pDataLock, - reinterpret_cast(&pLocalData) - ); +PROCGetProcessIDFromHandleExit: - if (PS_DONE == pLocalData->ps) - { - TRACE("We already called waitpid() on process ID %#x; process has " - "terminated, exit code is %d\n", - pLocalData->dwProcessId, pLocalData->dwExitCode); + return dwProcessId; +} - *pps = pLocalData->ps; - *pdwExitCode = pLocalData->dwExitCode; +/*++ +Function + InitializeProcessCommandLine - pDataLock->ReleaseLock(pThread, FALSE); +Abstract + Initializes (or re-initializes) the saved command line and exe path. - goto PROCGetProcessStatusExit; - } +Parameter + lpwstrCmdLine + lpwstrFullPath - /* By using waitpid(), we can even retrieve the exit code of a non-PAL - process. However, note that waitpid() can only provide the low 8 bits - of the exit code. This is all that is required for the PAL spec. */ - TRACE("Looking for status of process; trying wait()"); +Return + PAL_ERROR - while(1) - { - /* try to get state of process, using non-blocking call */ - wait_retval = waitpid(pLocalData->dwProcessId, &status, WNOHANG); +Notes + This function takes ownership of lpwstrCmdLine, but not of lpwstrFullPath +--*/ - if ( wait_retval == (pid_t) pLocalData->dwProcessId ) - { - /* success; get the exit code */ - if ( WIFEXITED( status ) ) - { - *pdwExitCode = WEXITSTATUS(status); - TRACE("Exit code was %d\n", *pdwExitCode); - } - else if ( WIFSIGNALED( status ) ) - { - *pdwExitCode = 128 + WTERMSIG(status); - TRACE("Exit code was signal %d = exit code %d\n", WTERMSIG(status), *pdwExitCode); - } - else - { - WARN("process terminated without exiting; can't get exit " - "code. faking it.\n"); - *pdwExitCode = EXIT_FAILURE; - } - *pps = PS_DONE; - } - else if (0 == wait_retval) - { - // The process is still running. - TRACE("Process %#x is still active.\n", pLocalData->dwProcessId); - *pps = PS_RUNNING; - *pdwExitCode = 0; - } - else if (-1 == wait_retval) +PAL_ERROR +CorUnix::InitializeProcessCommandLine( + LPWSTR lpwstrCmdLine, + LPWSTR lpwstrFullPath +) +{ + PAL_ERROR palError = NO_ERROR; + LPWSTR initial_dir = NULL; + + // + // Save the command line and initial directory + // + + if (lpwstrFullPath) + { + LPWSTR lpwstr = PAL_wcsrchr(lpwstrFullPath, '/'); + if (!lpwstr) { - // This might happen if waitpid() had already been called, but - // this shouldn't happen - we call waitpid once, store the - // result, and use that afterwards. - // One legitimate cause of failure is EINTR; if this happens we - // have to try again. A second legitimate cause is ECHILD, which - // happens if we're trying to retrieve the status of a currently- - // running process that isn't a child of this process. - if (EINTR == errno) - { - TRACE("waitpid() failed with EINTR; re-waiting"); - continue; - } - else if (ECHILD == errno) - { - TRACE("waitpid() failed with ECHILD; calling kill instead"); - if (kill(pLocalData->dwProcessId, 0) != 0) - { - if(ESRCH == errno) - { - WARN("kill() failed with ESRCH, i.e. target " - "process exited and it wasn't a child, " - "so can't get the exit code, assuming " - "it was 0.\n"); - *pdwExitCode = 0; - } - else - { - ERROR("kill(pid, 0) failed; errno is %d (%s)\n", - errno, strerror(errno)); - *pdwExitCode = EXIT_FAILURE; - } - *pps = PS_DONE; - } - else - { - *pps = PS_RUNNING; - *pdwExitCode = 0; - } - } - else - { - // Ignoring unexpected waitpid errno and assuming that - // the process is still running - ERROR("waitpid(pid=%u) failed with unexpected errno=%d (%s)\n", - pLocalData->dwProcessId, errno, strerror(errno)); - *pps = PS_RUNNING; - *pdwExitCode = 0; - } + ERROR("Invalid full path\n"); + palError = ERROR_INTERNAL_ERROR; + goto exit; } - else + lpwstr[0] = '\0'; + size_t n = PAL_wcslen(lpwstrFullPath) + 1; + + size_t iLen = n; + initial_dir = reinterpret_cast(malloc(iLen*sizeof(WCHAR))); + if (NULL == initial_dir) { - ASSERT("waitpid returned unexpected value %d\n",wait_retval); - *pdwExitCode = EXIT_FAILURE; - *pps = PS_DONE; + ERROR("malloc() failed! (initial_dir) \n"); + palError = ERROR_NOT_ENOUGH_MEMORY; + goto exit; } - // Break out of the loop in all cases except EINTR. - break; - } - - // Save the exit code for future reference (waitpid will only work once). - if(PS_DONE == *pps) - { - pLocalData->ps = PS_DONE; - pLocalData->dwExitCode = *pdwExitCode; - } - - TRACE( "State of process 0x%08x : %d (exit code %d)\n", - pLocalData->dwProcessId, *pps, *pdwExitCode ); - pDataLock->ReleaseLock(pThread, TRUE); + if (wcscpy_s(initial_dir, iLen, lpwstrFullPath) != SAFECRT_SUCCESS) + { + ERROR("wcscpy_s failed!\n"); + free(initial_dir); + palError = ERROR_INTERNAL_ERROR; + goto exit; + } -PROCGetProcessStatusExit: + lpwstr[0] = '/'; - if (NULL != pobjProcess) - { - pobjProcess->ReleaseReference(pThread); + free(g_lpwstrAppDir); + g_lpwstrAppDir = initial_dir; } - return palError; -} - -#ifdef __APPLE__ -bool GetApplicationContainerFolder(PathCharString& buffer, const char *applicationGroupId, int applicationGroupIdLength) -{ - const char *homeDir = getpwuid(getuid())->pw_dir; - int homeDirLength = strlen(homeDir); + free(g_lpwstrCmdLine); + g_lpwstrCmdLine = lpwstrCmdLine; - // The application group container folder is defined as: - // /user/{loginname}/Library/Group Containers/{AppGroupId}/ - return buffer.Set(homeDir, homeDirLength) - && buffer.Append(APPLICATION_CONTAINER_BASE_PATH_SUFFIX) - && buffer.Append(applicationGroupId, applicationGroupIdLength) - && buffer.Append('/'); +exit: + return palError; } -#endif // __APPLE__ -/* Internal function definitions **********************************************/ /*++ Function: - getFileName - -Abstract: - Helper function for CreateProcessW, it retrieves the executable filename - from the application name, and the command line. + CreateInitialProcessAndThreadObjects -Parameters: - IN lpApplicationName: first parameter from CreateProcessW (an unicode string) - IN lpCommandLine: second parameter from CreateProcessW (an unicode string) - OUT lpFileName: file to be executed (the new process) +Abstract + Creates the IPalObjects that represent the current process + and the initial thread -Return: - TRUE: if the file name is retrieved - FALSE: otherwise +Parameter + pThread - the initial thread +Return + PAL_ERROR --*/ -static -BOOL -getFileName( - LPCWSTR lpApplicationName, - LPWSTR lpCommandLine, - PathCharString& lpPathFileName) + +PAL_ERROR +CorUnix::CreateInitialProcessAndThreadObjects( + CPalThread *pThread + ) { - LPWSTR lpEnd; - WCHAR wcEnd; - char * lpFileName; - PathCharString lpFileNamePS; - char *lpTemp; + PAL_ERROR palError = NO_ERROR; + HANDLE hThread; + IPalObject *pobjProcess = NULL; + IDataLock *pDataLock; + CProcProcessLocalData *pLocalData; + CObjectAttributes oa; + HANDLE hProcess; + + // + // Create initial thread object + // - if (lpApplicationName) + palError = CreateThreadObject(pThread, pThread, &hThread); + if (NO_ERROR != palError) { - int length = WideCharToMultiByte(CP_ACP, 0, lpApplicationName, -1, - NULL, 0, NULL, NULL); + goto CreateInitialProcessAndThreadObjectsExit; + } - /* if only a file name is specified, prefix it with "./" */ - if ((*lpApplicationName != '.') && (*lpApplicationName != '/')) - { - length += 2; - lpTemp = lpPathFileName.OpenStringBuffer(length); + // + // This handle isn't needed + // - if (strcpy_s(lpTemp, length, "./") != SAFECRT_SUCCESS) - { - ERROR("strcpy_s failed!\n"); - return FALSE; - } - lpTemp+=2; - - } - else - { - lpTemp = lpPathFileName.OpenStringBuffer(length); - } - - /* Convert to ASCII */ - length = WideCharToMultiByte(CP_ACP, 0, lpApplicationName, -1, - lpTemp, length, NULL, NULL); - if (length == 0) - { - lpPathFileName.CloseBuffer(0); - ASSERT("WideCharToMultiByte failure\n"); - return FALSE; - } + (void) g_pObjectManager->RevokeHandle(pThread, hThread); - lpPathFileName.CloseBuffer(length -1); + // + // Create and initialize process object + // - return TRUE; + palError = g_pObjectManager->AllocateObject( + pThread, + &otProcess, + &oa, + &pobjProcess + ); + + if (NO_ERROR != palError) + { + ERROR("Unable to allocate process object"); + goto CreateInitialProcessAndThreadObjectsExit; } - else + + palError = pobjProcess->GetProcessLocalData( + pThread, + WriteLock, + &pDataLock, + reinterpret_cast(&pLocalData) + ); + + if (NO_ERROR != palError) { - /* use the Command line */ + ASSERT("Unable to access local data"); + goto CreateInitialProcessAndThreadObjectsExit; + } - /* filename should be the first token of the command line */ + pLocalData->dwProcessId = gPID; + pLocalData->ps = PS_RUNNING; + pDataLock->ReleaseLock(pThread, TRUE); - /* first skip all leading whitespace */ - lpCommandLine = UTIL_inverse_wcspbrk(lpCommandLine,W16_WHITESPACE); - if(NULL == lpCommandLine) - { - ERROR("CommandLine contains only whitespace!\n"); - return FALSE; - } + palError = g_pObjectManager->RegisterObject( + pThread, + pobjProcess, + &aotProcess, + &hProcess, + &g_pobjProcess + ); - /* check if it is starting with a quote (") character */ - if (*lpCommandLine == 0x0022) - { - lpCommandLine++; /* skip the quote */ + // + // pobjProcess is invalidated by the call to RegisterObject, so + // NULL it out here to prevent it from being released later + // - /* file name ends with another quote */ - lpEnd = PAL_wcschr(lpCommandLine+1, 0x0022); + pobjProcess = NULL; - /* if no quotes found, set lpEnd to the end of the Command line */ - if (lpEnd == NULL) - lpEnd = lpCommandLine + PAL_wcslen(lpCommandLine); - } - else - { - /* filename is end out by a whitespace */ - lpEnd = PAL_wcspbrk(lpCommandLine, W16_WHITESPACE); + if (NO_ERROR != palError) + { + ASSERT("Failure registering process object"); + goto CreateInitialProcessAndThreadObjectsExit; + } - /* if no whitespace found, set lpEnd to end of the Command line */ - if (lpEnd == NULL) - { - lpEnd = lpCommandLine + PAL_wcslen(lpCommandLine); - } - } + // + // There's no need to keep this handle around, so revoke + // it now + // - if (lpEnd == lpCommandLine) - { - ERROR("application name and command line are both empty!\n"); - return FALSE; - } + g_pObjectManager->RevokeHandle(pThread, hProcess); - /* replace the last character by a null */ - wcEnd = *lpEnd; - *lpEnd = 0x0000; +CreateInitialProcessAndThreadObjectsExit: - /* Convert to UTF-8 */ - int size = 0; - int length = WideCharToMultiByte(CP_ACP, 0, lpCommandLine, -1, NULL, 0, NULL, NULL); - if (length == 0) - { - ERROR("Failed to calculate the required buffer length.\n"); - return FALSE; - }; + if (NULL != pobjProcess) + { + pobjProcess->ReleaseReference(pThread); + } - lpFileName = lpFileNamePS.OpenStringBuffer(length - 1); - if (NULL == lpFileName) - { - ERROR("Not Enough Memory!\n"); - return FALSE; - } - if (!(size = WideCharToMultiByte(CP_ACP, 0, lpCommandLine, -1, - lpFileName, length, NULL, NULL))) - { - ASSERT("WideCharToMultiByte failure\n"); - return FALSE; - } + return palError; +} - lpFileNamePS.CloseBuffer(size - 1); - /* restore last character */ - *lpEnd = wcEnd; - if (!getPath(lpFileNamePS, lpPathFileName)) - { - /* file is not in the path */ - return FALSE; - } - } - return TRUE; +/*++ +Function: + PROCCleanupInitialProcess + +Abstract + Cleanup all the structures for the initial process. + +Parameter + VOID + +Return + VOID + +--*/ +VOID +PROCCleanupInitialProcess(VOID) +{ + /* Free the application directory */ + free(g_lpwstrAppDir); + + /* Free the stored command line */ + free(g_lpwstrCmdLine); + + // + // Object manager shutdown will handle freeing the underlying + // thread and process data + // } /*++ Function: - checkFileType + TerminateCurrentProcessNoExit Abstract: - Return the type of the file. + Terminate current Process, but leave the caller alive Parameters: - IN lpFileName: file name + BOOL bTerminateUnconditionally - If this is set, the PAL will exit as + quickly as possible. In particular, it will not unload DLLs. + +Return value : + No return + +Note: + This function is used in ExitThread and TerminateProcess -Return: - FILE_DIR: Directory - FILE_UNIX: Unix executable file - FILE_ERROR: Error --*/ -static -int -checkFileType( LPCSTR lpFileName) +VOID +CorUnix::TerminateCurrentProcessNoExit(BOOL bTerminateUnconditionally) { - struct stat stat_data; + BOOL locked; + DWORD old_terminator; - /* check if the file exist */ - if ( access(lpFileName, F_OK) != 0 ) - { - return FILE_ERROR; - } + old_terminator = InterlockedCompareExchange(&terminator, GetCurrentThreadId(), 0); - /* if it's not a PE/COFF file, check if it is executable */ - if ( -1 != stat( lpFileName, &stat_data ) ) + if (0 != old_terminator && GetCurrentThreadId() != old_terminator) { - if((stat_data.st_mode & S_IFMT) == S_IFDIR ) - { - /*The given file is a directory*/ - return FILE_DIR; - } - if ( UTIL_IsExecuteBitsSet( &stat_data ) ) - { - return FILE_UNIX; - } - else + /* another thread has already initiated the termination process. we + could just block on the PALInitLock critical section, but then + PROCSuspendOtherThreads would hang... so sleep forever here, we're + terminating anyway + + Update: [TODO] PROCSuspendOtherThreads has been removed. Can this + code be changed? */ + + /* note that if *this* thread has already started the termination + process, we want to proceed. the only way this can happen is if a + call to DllMain (from ExitProcess) brought us here (because DllMain + called ExitProcess, or TerminateProcess, or ExitThread); + TerminateProcess won't call DllMain, so there's no danger to get + caught in an infinite loop */ + WARN("termination already started from another thread; blocking.\n"); + while (true) { - return FILE_ERROR; + poll(NULL, 0, INFTIM); } } - return FILE_ERROR; -} + /* Try to lock the initialization count to prevent multiple threads from + terminating/initializing the PAL simultaneously */ + + /* note : it's also important to take this lock before the process lock, + because Init/Shutdown take the init lock, and the functions they call + may take the process lock. We must do it in the same order to avoid + deadlocks */ + locked = PALInitLock(); + if(locked && PALIsInitialized()) + { + PROCNotifyProcessShutdown(); + PALCommonCleanup(); + } +} /*++ Function: - buildArgv + PROCGetProcessStatus Abstract: - Helper function for CreateProcessW, it builds the array of argument in - a format than can be passed to execve function.lppArgv is allocated - in this function and must be freed by the caller. + Retrieve process state information (state & exit code). Parameters: - IN lpCommandLine: second parameter from CreateProcessW (an unicode string) - IN lpAppPath: canonical name of the application to launched - OUT lppArgv: array of arguments to be passed to the new process + DWORD process_id : PID of process to retrieve state for + PROCESS_STATE *state : state of process (starting, running, done) + DWORD *exit_code : exit code of process (from ExitProcess, etc.) -Return: - the number of arguments - -note: this doesn't yet match precisely the behavior of Windows, but should be -sufficient. -what's here: -1) stripping nonquoted whitespace -2) handling of quoted parameters and quoted "parts" of parameters, removal of - doublequotes ( becomes ) -3) \" as an escaped doublequote, both within doublequoted sequences and out -what's known missing : -1) \\ as an escaped backslash, but only if the string of '\' - is followed by a " (escaped or not) -2) "alternate" escape sequence : double-doublequote within a double-quoted - argument (<"aaa a""aa aaa">) expands to a single-doublequote() -note that there may be other special cases +Return value : + TRUE on success --*/ -static -char ** -buildArgv( - LPCWSTR lpCommandLine, - PathCharString& lpAppPath, - UINT *pnArg) +PAL_ERROR +PROCGetProcessStatus( + CPalThread *pThread, + HANDLE hProcess, + PROCESS_STATE *pps, + DWORD *pdwExitCode + ) { - CPalThread *pThread = NULL; - UINT iWlen; - char *lpAsciiCmdLine; - char *pChar; - char **lppArgv; - char **lppTemp; - UINT i,j; + PAL_ERROR palError = NO_ERROR; + IPalObject *pobjProcess = NULL; + IDataLock *pDataLock; + CProcProcessLocalData *pLocalData; + pid_t wait_retval; + int status; - *pnArg = 0; + // + // First, check if we already know the status of this process. This will be + // the case if this function has already been called for the same process. + // - iWlen = WideCharToMultiByte(CP_ACP,0,lpCommandLine,-1,NULL,0,NULL,NULL); + palError = g_pObjectManager->ReferenceObjectByHandle( + pThread, + hProcess, + &aotProcess, + &pobjProcess + ); - if(0 == iWlen) + if (NO_ERROR != palError) { - ASSERT("Can't determine length of command line\n"); - return NULL; + goto PROCGetProcessStatusExit; } - pThread = InternalGetCurrentThread(); - /* make sure to allocate enough space, up for the worst case scenario */ - int iLength = (iWlen + lpAppPath.GetCount() + 2); - lpAsciiCmdLine = (char *) malloc(iLength); + palError = pobjProcess->GetProcessLocalData( + pThread, + WriteLock, + &pDataLock, + reinterpret_cast(&pLocalData) + ); - if (lpAsciiCmdLine == NULL) + if (PS_DONE == pLocalData->ps) { - ERROR("Unable to allocate memory\n"); - return NULL; - } - - pChar = lpAsciiCmdLine; + TRACE("We already called waitpid() on process ID %#x; process has " + "terminated, exit code is %d\n", + pLocalData->dwProcessId, pLocalData->dwExitCode); - /* put the canonical name of the application as the first parameter */ - if ((strcpy_s(lpAsciiCmdLine, iLength, "\"") != SAFECRT_SUCCESS) || - (strcat_s(lpAsciiCmdLine, iLength, lpAppPath) != SAFECRT_SUCCESS) || - (strcat_s(lpAsciiCmdLine, iLength, "\"") != SAFECRT_SUCCESS) || - (strcat_s(lpAsciiCmdLine, iLength, " ") != SAFECRT_SUCCESS)) - { - ERROR("strcpy_s/strcat_s failed!\n"); - free(lpAsciiCmdLine); - return NULL; - } + *pps = pLocalData->ps; + *pdwExitCode = pLocalData->dwExitCode; - pChar = lpAsciiCmdLine + strlen (lpAsciiCmdLine); + pDataLock->ReleaseLock(pThread, FALSE); - /* let's skip the first argument in the command line */ + goto PROCGetProcessStatusExit; + } - /* strip leading whitespace; function returns NULL if there's only - whitespace, so the if statement below will work correctly */ - lpCommandLine = UTIL_inverse_wcspbrk((LPWSTR)lpCommandLine, W16_WHITESPACE); + /* By using waitpid(), we can even retrieve the exit code of a non-PAL + process. However, note that waitpid() can only provide the low 8 bits + of the exit code. This is all that is required for the PAL spec. */ + TRACE("Looking for status of process; trying wait()"); - if (lpCommandLine) + while(1) { - LPCWSTR stringstart = lpCommandLine; + /* try to get state of process, using non-blocking call */ + wait_retval = waitpid(pLocalData->dwProcessId, &status, WNOHANG); - do + if ( wait_retval == (pid_t) pLocalData->dwProcessId ) { - /* find first whitespace or dquote character */ - lpCommandLine = PAL_wcspbrk(lpCommandLine,W16_WHITESPACE_DQUOTE); - if(NULL == lpCommandLine) + /* success; get the exit code */ + if ( WIFEXITED( status ) ) { - /* no whitespace or dquote found : first arg is only arg */ - break; + *pdwExitCode = WEXITSTATUS(status); + TRACE("Exit code was %d\n", *pdwExitCode); } - else if('"' == *lpCommandLine) + else if ( WIFSIGNALED( status ) ) { - /* got a dquote; skip over it if it's escaped; make sure we - don't try to look before the first character in the - string */ - if(lpCommandLine > stringstart && '\\' == lpCommandLine[-1]) - { - lpCommandLine++; - continue; - } - - /* found beginning of dquoted sequence, run to the end */ - /* don't stop if we hit an escaped dquote */ - lpCommandLine++; - while( *lpCommandLine ) - { - lpCommandLine = PAL_wcschr(lpCommandLine, '"'); - if(NULL == lpCommandLine) - { - /* no ending dquote, arg runs to end of string */ - break; - } - if('\\' != lpCommandLine[-1]) - { - /* dquote is not escaped, dquoted sequence is over*/ - break; - } - lpCommandLine++; - } - if(NULL == lpCommandLine || '\0' == *lpCommandLine) - { - /* no terminating dquote */ - break; - } - - /* step over dquote, keep looking for end of arg */ - lpCommandLine++; + *pdwExitCode = 128 + WTERMSIG(status); + TRACE("Exit code was signal %d = exit code %d\n", WTERMSIG(status), *pdwExitCode); } else { - /* found whitespace : end of arg. */ - lpCommandLine++; - break; + WARN("process terminated without exiting; can't get exit " + "code. faking it.\n"); + *pdwExitCode = EXIT_FAILURE; } - }while(lpCommandLine); - } - - /* Convert to ASCII */ - if (lpCommandLine) - { - if (!WideCharToMultiByte(CP_ACP, 0, lpCommandLine, -1, - pChar, iWlen+1, NULL, NULL)) - { - ASSERT("Unable to convert to a multibyte string\n"); - free(lpAsciiCmdLine); - return NULL; + *pps = PS_DONE; } - } - - pChar = lpAsciiCmdLine; - - /* loops through all the arguments, to find out how many arguments there - are; while looping replace whitespace by \0 */ - - /* skip leading whitespace (and replace by '\0') */ - /* note : there shouldn't be any, command starts either with PE loader name - or computed application path, but this won't hurt */ - while (*pChar) - { - if (!isspace((unsigned char) *pChar)) + else if (0 == wait_retval) { - break; + // The process is still running. + TRACE("Process %#x is still active.\n", pLocalData->dwProcessId); + *pps = PS_RUNNING; + *pdwExitCode = 0; } - WARN("unexpected whitespace in command line!\n"); - *pChar++ = '\0'; - } - - while (*pChar) - { - (*pnArg)++; - - /* find end of current arg */ - while(*pChar && !isspace((unsigned char) *pChar)) + else if (-1 == wait_retval) { - if('"' == *pChar) + // This might happen if waitpid() had already been called, but + // this shouldn't happen - we call waitpid once, store the + // result, and use that afterwards. + // One legitimate cause of failure is EINTR; if this happens we + // have to try again. A second legitimate cause is ECHILD, which + // happens if we're trying to retrieve the status of a currently- + // running process that isn't a child of this process. + if (EINTR == errno) { - /* skip over dquote if it's escaped; make sure we don't try to - look before the start of the string for the \ */ - if(pChar > lpAsciiCmdLine && '\\' == pChar[-1]) - { - pChar++; - continue; - } - - /* found leading dquote : look for ending dquote */ - pChar++; - while (*pChar) + TRACE("waitpid() failed with EINTR; re-waiting"); + continue; + } + else if (ECHILD == errno) + { + TRACE("waitpid() failed with ECHILD; calling kill instead"); + if (kill(pLocalData->dwProcessId, 0) != 0) { - pChar = strchr(pChar,'"'); - if(NULL == pChar) + if(ESRCH == errno) { - /* no ending dquote found : argument extends to the end - of the string*/ - break; + WARN("kill() failed with ESRCH, i.e. target " + "process exited and it wasn't a child, " + "so can't get the exit code, assuming " + "it was 0.\n"); + *pdwExitCode = 0; } - if('\\' != pChar[-1]) + else { - /* found a dquote, and it's not escaped : quoted - sequence is over*/ - break; + ERROR("kill(pid, 0) failed; errno is %d (%s)\n", + errno, strerror(errno)); + *pdwExitCode = EXIT_FAILURE; } - /* found a dquote, but it was escaped : skip over it, keep - looking */ - pChar++; - } - if(NULL == pChar || '\0' == *pChar) - { - /* reached the end of the string : we're done */ - break; + *pps = PS_DONE; } - } - pChar++; - } - if(NULL == pChar) - { - /* reached the end of the string : we're done */ - break; - } - /* reached end of arg; replace trailing whitespace by '\0', to split - arguments into separate strings */ - while (isspace((unsigned char) *pChar)) - { - *pChar++ = '\0'; - } - } - - /* allocate lppargv according to the number of arguments - in the command line */ - lppArgv = (char **) malloc((((*pnArg)+1) * sizeof(char *))); - - if (lppArgv == NULL) - { - free(lpAsciiCmdLine); - return NULL; - } - - lppTemp = lppArgv; - - /* at this point all parameters are separated by NULL - we need to fill the array of arguments; we must also remove all dquotes - from arguments (new process shouldn't see them) */ - for (i = *pnArg, pChar = lpAsciiCmdLine; i; i--) - { - /* skip NULLs */ - while (!*pChar) - { - pChar++; - } - - *lppTemp = pChar; - - /* go to the next parameter, removing dquotes as we go along */ - j = 0; - while (*pChar) - { - /* copy character if it's not a dquote */ - if('"' != *pChar) - { - /* if it's the \ of an escaped dquote, skip over it, we'll - copy the " instead */ - if( '\\' == pChar[0] && '"' == pChar[1] ) + else { - pChar++; + *pps = PS_RUNNING; + *pdwExitCode = 0; } - (*lppTemp)[j++] = *pChar; } - pChar++; - } - /* re-NULL terminate the argument */ - (*lppTemp)[j] = '\0'; - - lppTemp++; - } - - *lppTemp = NULL; - - return lppArgv; -} - - -/*++ -Function: - getPath - -Abstract: - Helper function for CreateProcessW, it looks in the path environment - variable to find where the process to executed is. - -Parameters: - IN lpFileName: file name to search in the path - OUT lpPathFileName: returned string containing the path and the filename - -Return: - TRUE if found - FALSE otherwise ---*/ -static -BOOL -getPath( - PathCharString& lpFileNameString, - PathCharString& lpPathFileName) -{ - LPSTR lpPath; - LPSTR lpNext; - LPSTR lpCurrent; - LPWSTR lpwstr; - INT n; - INT nextLen; - INT slashLen; - CPalThread *pThread = NULL; - LPCSTR lpFileName = lpFileNameString.GetString(); - - /* if a path is specified, only look there */ - if(strchr(lpFileName, '/')) - { - if (access (lpFileName, F_OK) == 0) - { - if (!lpPathFileName.Set(lpFileNameString)) + else { - TRACE("Set of StackString failed!\n"); - return FALSE; + // Ignoring unexpected waitpid errno and assuming that + // the process is still running + ERROR("waitpid(pid=%u) failed with unexpected errno=%d (%s)\n", + pLocalData->dwProcessId, errno, strerror(errno)); + *pps = PS_RUNNING; + *pdwExitCode = 0; } - - TRACE("file %s exists\n", lpFileName); - return TRUE; } else { - TRACE("file %s doesn't exist.\n", lpFileName); - return FALSE; - } - } - - /* first look in directory from which the application loaded */ - lpwstr = g_lpwstrAppDir; - - if (lpwstr) - { - /* convert path to multibyte, check buffer size */ - n = WideCharToMultiByte(CP_ACP, 0, lpwstr, -1, NULL, 0, - NULL, NULL); - - if (!lpPathFileName.Reserve(n + lpFileNameString.GetCount() + 1 )) - { - ERROR("StackString Reserve failed!\n"); - return FALSE; - } - - lpPath = lpPathFileName.OpenStringBuffer(n); - - n = WideCharToMultiByte(CP_ACP, 0, lpwstr, -1, lpPath, n, - NULL, NULL); - - if (n == 0) - { - lpPathFileName.CloseBuffer(0); - ASSERT("WideCharToMultiByte failure!\n"); - return FALSE; - } - - lpPathFileName.CloseBuffer(n - 1); - - lpPathFileName.Append("/", 1); - lpPathFileName.Append(lpFileNameString); - - if (access(lpPathFileName, F_OK) == 0) - { - TRACE("found %s in application directory (%s)\n", lpFileName, lpPathFileName.GetString()); - return TRUE; + ASSERT("waitpid returned unexpected value %d\n",wait_retval); + *pdwExitCode = EXIT_FAILURE; + *pps = PS_DONE; } + // Break out of the loop in all cases except EINTR. + break; } - /* then try the current directory */ - if (!lpPathFileName.Reserve(lpFileNameString.GetCount() + 2)) + // Save the exit code for future reference (waitpid will only work once). + if(PS_DONE == *pps) { - ERROR("StackString Reserve failed!\n"); - return FALSE; + pLocalData->ps = PS_DONE; + pLocalData->dwExitCode = *pdwExitCode; } - lpPathFileName.Set("./", 2); - lpPathFileName.Append(lpFileNameString); - - if (access (lpPathFileName, R_OK) == 0) - { - TRACE("found %s in current directory.\n", lpFileName); - return TRUE; - } + TRACE( "State of process 0x%08x : %d (exit code %d)\n", + pLocalData->dwProcessId, *pps, *pdwExitCode ); - pThread = InternalGetCurrentThread(); + pDataLock->ReleaseLock(pThread, TRUE); - /* Then try to look in the path */ - lpPath = EnvironGetenv("PATH"); +PROCGetProcessStatusExit: - if (!lpPath) + if (NULL != pobjProcess) { - ERROR("EnvironGetenv returned NULL for $PATH\n"); - return FALSE; + pobjProcess->ReleaseReference(pThread); } - lpNext = lpPath; - - /* search in every path directory */ - TRACE("looking for file %s in $PATH (%s)\n", lpFileName, lpPath); - while (lpNext) - { - /* skip all leading ':' */ - while(*lpNext==':') - { - lpNext++; - } - - /* search for ':' */ - lpCurrent = strchr(lpNext, ':'); - if (lpCurrent) - { - *lpCurrent++ = '\0'; - } - - nextLen = strlen(lpNext); - slashLen = (lpNext[nextLen-1] == '/') ? 0:1; - - if (!lpPathFileName.Reserve(nextLen + lpFileNameString.GetCount() + 1)) - { - free(lpPath); - ERROR("StackString ran out of memory for full path\n"); - return FALSE; - } - - lpPathFileName.Set(lpNext, nextLen); - - if( slashLen == 1) - { - /* append a '/' if there's no '/' at the end of the path */ - lpPathFileName.Append("/", 1); - } - - lpPathFileName.Append(lpFileNameString); - - if ( access (lpPathFileName, F_OK) == 0) - { - TRACE("Found %s in $PATH element %s\n", lpFileName, lpNext); - free(lpPath); - return TRUE; - } + return palError; +} - lpNext = lpCurrent; /* search in the next directory */ - } +#ifdef __APPLE__ +bool GetApplicationContainerFolder(PathCharString& buffer, const char *applicationGroupId, int applicationGroupIdLength) +{ + const char *homeDir = getpwuid(getuid())->pw_dir; + int homeDirLength = strlen(homeDir); - free(lpPath); - TRACE("File %s not found in $PATH\n", lpFileName); - return FALSE; + // The application group container folder is defined as: + // /user/{loginname}/Library/Group Containers/{AppGroupId}/ + return buffer.Set(homeDir, homeDirLength) + && buffer.Append(APPLICATION_CONTAINER_BASE_PATH_SUFFIX) + && buffer.Append(applicationGroupId, applicationGroupIdLength) + && buffer.Append('/'); } +#endif // __APPLE__ diff --git a/src/coreclr/pal/tests/palsuite/CMakeLists.txt b/src/coreclr/pal/tests/palsuite/CMakeLists.txt index 1bb9d98676da2e..c32f205182c5e0 100644 --- a/src/coreclr/pal/tests/palsuite/CMakeLists.txt +++ b/src/coreclr/pal/tests/palsuite/CMakeLists.txt @@ -140,8 +140,6 @@ add_executable_clr(paltests c_runtime/_wcsnicmp/test1/test1.cpp c_runtime/_wtoi/test1/test1.cpp #debug_api/DebugBreak/test1/test1.cpp - debug_api/OutputDebugStringA/test1/helper.cpp - debug_api/OutputDebugStringA/test1/test1.cpp debug_api/OutputDebugStringW/test1/test1.cpp #debug_api/WriteProcessMemory/test1/helper.cpp #debug_api/WriteProcessMemory/test1/test1.cpp @@ -361,10 +359,6 @@ add_executable_clr(paltests # pal_specific/PAL_RegisterLibraryW_UnregisterLibraryW/test2_neg/reg_unreg_libraryw_neg.cpp samples/test1/test.cpp samples/test2/test.cpp - threading/CreateProcessW/test1/childProcess.cpp - threading/CreateProcessW/test1/parentProcess.cpp - threading/CreateProcessW/test2/childprocess.cpp - threading/CreateProcessW/test2/parentprocess.cpp threading/CreateSemaphoreW_ReleaseSemaphore/test1/CreateSemaphore.cpp threading/CreateSemaphoreW_ReleaseSemaphore/test2/CreateSemaphore.cpp threading/CreateSemaphoreW_ReleaseSemaphore/test3/createsemaphore.cpp @@ -383,22 +377,14 @@ add_executable_clr(paltests threading/ExitProcess/test2/test2.cpp threading/ExitProcess/test3/test3.cpp threading/ExitThread/test1/test1.cpp - threading/ExitThread/test2/childprocess.cpp - threading/ExitThread/test2/test2.cpp threading/GetCurrentProcess/test1/process.cpp threading/GetCurrentProcessId/test1/processId.cpp threading/GetCurrentThread/test1/thread.cpp threading/GetCurrentThread/test2/test2.cpp threading/GetCurrentThreadId/test1/threadId.cpp - threading/GetExitCodeProcess/test1/childProcess.cpp - threading/GetExitCodeProcess/test1/test1.cpp threading/OpenEventW/test1/test1.cpp threading/OpenEventW/test2/test2.cpp - threading/OpenEventW/test3/childprocess.cpp - threading/OpenEventW/test3/test3.cpp threading/OpenEventW/test5/test5.cpp - threading/OpenProcess/test1/childProcess.cpp - threading/OpenProcess/test1/test1.cpp threading/QueryThreadCycleTime/test1/test1.cpp threading/releasesemaphore/test1/test.cpp threading/ResetEvent/test1/test1.cpp @@ -418,11 +404,7 @@ add_executable_clr(paltests threading/ThreadPriority/test1/ThreadPriority.cpp threading/WaitForMultipleObjects/test1/test1.cpp threading/WaitForMultipleObjectsEx/test1/test1.cpp - threading/WaitForMultipleObjectsEx/test5/helper.cpp - threading/WaitForMultipleObjectsEx/test5/test5.cpp threading/WaitForSingleObject/test1/test1.cpp - threading/WaitForSingleObject/WFSOProcessTest/ChildProcess.cpp - threading/WaitForSingleObject/WFSOProcessTest/WFSOProcessTest.cpp threading/WaitForSingleObject/WFSOSemaphoreTest/WFSOSemaphoreTest.cpp threading/WaitForSingleObject/WFSOThreadTest/WFSOThreadTest.cpp threading/YieldProcessor/test1/test1.cpp diff --git a/src/coreclr/pal/tests/palsuite/compilableTests.txt b/src/coreclr/pal/tests/palsuite/compilableTests.txt index 2b781f28d32adc..5553e413c6cccb 100644 --- a/src/coreclr/pal/tests/palsuite/compilableTests.txt +++ b/src/coreclr/pal/tests/palsuite/compilableTests.txt @@ -92,7 +92,6 @@ c_runtime/_wcsicmp/test1/paltest_wcsicmp_test1 c_runtime/_wcslwr_s/test1/paltest_wcslwr_s_test1 c_runtime/_wcsnicmp/test1/paltest_wcsnicmp_test1 c_runtime/_wtoi/test1/paltest_wtoi_test1 -debug_api/OutputDebugStringA/test1/paltest_outputdebugstringa_test1 debug_api/OutputDebugStringW/test1/paltest_outputdebugstringw_test1 exception_handling/RaiseException/test1/paltest_raiseexception_test1 exception_handling/RaiseException/test2/paltest_raiseexception_test2 @@ -269,8 +268,6 @@ pal_specific/PAL_RegisterLibraryW_UnregisterLibraryW/test1/paltest_pal_registerl pal_specific/PAL_RegisterLibraryW_UnregisterLibraryW/test2_neg/paltest_reg_unreg_libraryw_neg samples/test1/paltest_samples_test1 samples/test2/paltest_samples_test2 -threading/CreateProcessW/test1/paltest_createprocessw_test1 -threading/CreateProcessW/test2/paltest_createprocessw_test2 threading/CreateSemaphoreW_ReleaseSemaphore/test1/paltest_createsemaphorew_releasesemaphore_test1 threading/CreateSemaphoreW_ReleaseSemaphore/test2/paltest_createsemaphorew_releasesemaphore_test2 threading/CreateSemaphoreW_ReleaseSemaphore/test3/paltest_createsemaphorew_releasesemaphore_test3 @@ -289,18 +286,14 @@ threading/ExitProcess/test1/paltest_exitprocess_test1 threading/ExitProcess/test2/paltest_exitprocess_test2 threading/ExitProcess/test3/paltest_exitprocess_test3 threading/ExitThread/test1/paltest_exitthread_test1 -threading/ExitThread/test2/paltest_exitthread_test2 threading/GetCurrentProcess/test1/paltest_getcurrentprocess_test1 threading/GetCurrentProcessId/test1/paltest_getcurrentprocessid_test1 threading/GetCurrentThread/test1/paltest_getcurrentthread_test1 threading/GetCurrentThread/test2/paltest_getcurrentthread_test2 threading/GetCurrentThreadId/test1/paltest_getcurrentthreadid_test1 -threading/GetExitCodeProcess/test1/paltest_getexitcodeprocess_test1 threading/OpenEventW/test1/paltest_openeventw_test1 threading/OpenEventW/test2/paltest_openeventw_test2 -threading/OpenEventW/test3/paltest_openeventw_test3 threading/OpenEventW/test5/paltest_openeventw_test5 -threading/OpenProcess/test1/paltest_openprocess_test1 threading/QueryThreadCycleTime/test1/paltest_querythreadcycletime_test1 threading/releasesemaphore/test1/paltest_releasesemaphore_test1 threading/ResetEvent/test1/paltest_resetevent_test1 @@ -320,9 +313,7 @@ threading/TerminateProcess/test1/paltest_terminateprocess_test1 threading/ThreadPriority/test1/paltest_threadpriority_test1 threading/WaitForMultipleObjects/test1/paltest_waitformultipleobjects_test1 threading/WaitForMultipleObjectsEx/test1/paltest_waitformultipleobjectsex_test1 -threading/WaitForMultipleObjectsEx/test5/paltest_waitformultipleobjectsex_test5 threading/WaitForSingleObject/test1/paltest_waitforsingleobject_test1 -threading/WaitForSingleObject/WFSOProcessTest/paltest_waitforsingleobject_wfsoprocesstest threading/WaitForSingleObject/WFSOSemaphoreTest/paltest_waitforsingleobject_wfsosemaphoretest threading/WaitForSingleObject/WFSOThreadTest/paltest_waitforsingleobject_wfsothreadtest threading/YieldProcessor/test1/paltest_yieldprocessor_test1 diff --git a/src/coreclr/pal/tests/palsuite/debug_api/OutputDebugStringA/test1/helper.cpp b/src/coreclr/pal/tests/palsuite/debug_api/OutputDebugStringA/test1/helper.cpp deleted file mode 100644 index e47029fa7865c4..00000000000000 --- a/src/coreclr/pal/tests/palsuite/debug_api/OutputDebugStringA/test1/helper.cpp +++ /dev/null @@ -1,34 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================= -** -** Source: helper.c -** -** Purpose: Intended to be the child process of a debugger. Calls -** OutputDebugStringA once with a normal string, once with an empty -** string -** -** -**============================================================*/ - -#include - -PALTEST(debug_api_OutputDebugStringA_test1_paltest_outputdebugstringa_test1_helper, "debug_api/OutputDebugStringA/test1/paltest_outputdebugstringa_test1_helper") -{ - if(0 != (PAL_Initialize(argc, argv))) - { - return FAIL; - } - - OutputDebugStringA("Foo!\n"); - - OutputDebugStringA(""); - - /* give a chance to the debugger process to read the debug string before - exiting */ - Sleep(1000); - - PAL_Terminate(); - return PASS; -} diff --git a/src/coreclr/pal/tests/palsuite/debug_api/OutputDebugStringA/test1/test1.cpp b/src/coreclr/pal/tests/palsuite/debug_api/OutputDebugStringA/test1/test1.cpp deleted file mode 100644 index a8f55d7f9c04c6..00000000000000 --- a/src/coreclr/pal/tests/palsuite/debug_api/OutputDebugStringA/test1/test1.cpp +++ /dev/null @@ -1,95 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================= -** -** Source: test1.c -** -** Purpose: Debugs the helper application. Checks that certain events, in -** particular the OUTPUT_DEBUG_STRING_EVENT, is generated correctly -** and gives the correct values. -** -** -**============================================================*/ - -#include - -const int DELAY_MS = 2000; - -struct OutputCheck -{ - DWORD ExpectedEventCode; - DWORD ExpectedUnicode; - char *ExpectedStr; -}; - -PALTEST(debug_api_OutputDebugStringA_test1_paltest_outputdebugstringa_test1, "debug_api/OutputDebugStringA/test1/paltest_outputdebugstringa_test1") -{ - - PROCESS_INFORMATION pi; - STARTUPINFO si; - - if(0 != (PAL_Initialize(argc, argv))) - { - return FAIL; - } - - ZeroMemory( &si, sizeof(si) ); - si.cb = sizeof(si); - ZeroMemory( &pi, sizeof(pi) ); - - WCHAR name[] = {'h','e','l', 'p', 'e', 'r', '\0'}; - /* Create a new process. This is the process to be Debugged */ - if(!CreateProcessW( NULL, name, NULL, NULL, - FALSE, 0, NULL, NULL, &si, &pi)) - { - DWORD dwError = GetLastError(); - Fail("ERROR: CreateProcess failed to load executable 'helper'. " - "GetLastError() returned %d.\n", dwError); - } - - /* This is the main loop. It exits when the process which is being - debugged is finished executing. - */ - - while(1) - { - DWORD dwRet = 0; - dwRet = WaitForSingleObject(pi.hProcess, - DELAY_MS /* Wait for 2 seconds max*/ - ); - - if (dwRet != WAIT_OBJECT_0) - { - Trace("WaitForSingleObjectTest:WaitForSingleObject " - "failed (%x) after waiting %d seconds for the helper\n", - GetLastError(), DELAY_MS / 1000); - } - else - { - DWORD dwExitCode; - - /* check the exit code from the process */ - if( ! GetExitCodeProcess( pi.hProcess, &dwExitCode ) ) - { - DWORD dwError; - - dwError = GetLastError(); - CloseHandle ( pi.hProcess ); - CloseHandle ( pi.hThread ); - Fail( "GetExitCodeProcess call failed with error code %d\n", - dwError ); - } - - if(dwExitCode != STILL_ACTIVE) { - CloseHandle(pi.hThread); - CloseHandle(pi.hProcess); - break; - } - Trace("still executing %d..\n", dwExitCode); - } - } - - PAL_Terminate(); - return PASS; -} diff --git a/src/coreclr/pal/tests/palsuite/paltestlist.txt b/src/coreclr/pal/tests/palsuite/paltestlist.txt index 111ef1d8fee380..650e28f0e2d00f 100644 --- a/src/coreclr/pal/tests/palsuite/paltestlist.txt +++ b/src/coreclr/pal/tests/palsuite/paltestlist.txt @@ -239,8 +239,6 @@ miscellaneous/SetLastError/test1/paltest_setlasterror_test1 pal_specific/PAL_Initialize_Terminate/test1/paltest_pal_initialize_terminate_test1 pal_specific/PAL_Initialize_Terminate/test2/paltest_pal_initialize_terminate_test2 samples/test1/paltest_samples_test1 -threading/CreateProcessW/test1/paltest_createprocessw_test1 -threading/CreateProcessW/test2/paltest_createprocessw_test2 threading/CreateSemaphoreW_ReleaseSemaphore/test1/paltest_createsemaphorew_releasesemaphore_test1 threading/CreateSemaphoreW_ReleaseSemaphore/test2/paltest_createsemaphorew_releasesemaphore_test2 threading/CreateThread/test1/paltest_createthread_test1 diff --git a/src/coreclr/pal/tests/palsuite/threading/CreateProcessW/test1/childProcess.cpp b/src/coreclr/pal/tests/palsuite/threading/CreateProcessW/test1/childProcess.cpp deleted file mode 100644 index dd0e35f5311f35..00000000000000 --- a/src/coreclr/pal/tests/palsuite/threading/CreateProcessW/test1/childProcess.cpp +++ /dev/null @@ -1,109 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================ -** -** Source: CreateProcessW/test1/childprocess.c -** -** Purpose: Test to ensure CreateProcessW starts a new process. This test -** launches a child process, and examines a file written by the child. -** This code is the child code. -** -** Dependencies: GetTempPath -** MultiByteToWideChar -** wcslen -** strlen -** WideCharToMultiByte -** fopen -** fclose -** fprintf -** - -** -**=========================================================*/ - -#define UNICODE -#include - -const WCHAR szCommonFileW[] = - {'c','h','i','l','d','d','a','t','a','.','t','m','p','\0'}; - - -#define szCommonStringA "058d2d057111a313aa82401c2e856002\0" - -PALTEST(threading_CreateProcessW_test1_paltest_createprocessw_test1_child, "threading/CreateProcessW/test1/paltest_createprocessw_test1_child") -{ - - static FILE * fp; - - DWORD dwFileLength; - DWORD dwDirLength; - DWORD dwSize; - - char *szAbsPathNameA; - WCHAR szDirNameW[_MAX_DIR]; - WCHAR szAbsPathNameW[MAX_PATH]; - - if(0 != (PAL_Initialize(argc, argv))) - { - return ( FAIL ); - } - - dwDirLength = GetTempPath(MAX_PATH, szDirNameW); - - if (0 == dwDirLength) - { - Fail ("GetTempPath call failed. Could not get " - "temp directory\n. Exiting.\n"); - } - - dwFileLength = wcslen( szCommonFileW ); - - dwSize = mkAbsoluteFilenameW( szDirNameW, dwDirLength, szCommonFileW, - dwFileLength, szAbsPathNameW ); - - if (0 == dwSize) - { - Fail ("Palsuite Code: mkAbsoluteFilename() call failed. Could " - "not build absolute path name to file\n. Exiting.\n"); - } - - /* set the string length for the open call */ - szAbsPathNameA = (char*)malloc(dwSize +1); - - if (NULL == szAbsPathNameA) - { - Fail ("Unable to malloc (%d) bytes. Exiting\n", (dwSize +1) ); - } - - WideCharToMultiByte (CP_ACP, 0, szAbsPathNameW, -1, szAbsPathNameA, - (dwSize + 1), NULL, NULL); - - if ( NULL == ( fp = fopen ( szAbsPathNameA , "w+" ) ) ) - { - /* - * A return value of NULL indicates an error condition or an - * EOF condition - */ - Fail ("%s unable to open %s for writing. Exiting.\n", argv[0] - , szAbsPathNameA ); - } - - free (szAbsPathNameA); - - if ( 0 >= ( fprintf ( fp, "%s", szCommonStringA ))) - { - Fail("%s unable to write to %s. Exiting.\n", argv[0] - , szAbsPathNameA ); - } - - if (0 != (fclose ( fp ))) - { - Fail ("%s unable to close file %s. Pid may not be " - "written to file. Exiting.\n", argv[0], szAbsPathNameA ); - } - - PAL_Terminate(); - return ( PASS ); - -} diff --git a/src/coreclr/pal/tests/palsuite/threading/CreateProcessW/test1/parentProcess.cpp b/src/coreclr/pal/tests/palsuite/threading/CreateProcessW/test1/parentProcess.cpp deleted file mode 100644 index 00e6f2c30147ea..00000000000000 --- a/src/coreclr/pal/tests/palsuite/threading/CreateProcessW/test1/parentProcess.cpp +++ /dev/null @@ -1,168 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================ -** -** Source: CreateProcessW/test1/parentprocess.c -** -** Purpose: Test to ensure CreateProcessW starts a new process. This test -** launches a child process, and examines a file written by the child. -** This process (the parent process) reads the file created by the child and -** compares the value the child wrote to the file. (a const char *) -** -** Dependencies: GetTempPath -** MultiByteToWideChar -** wcslen -** strlen -** WideCharToMultiByte -** WaitForSingleObject -** fopen -** fclose -** Fail -** - -** -**=========================================================*/ - -#define UNICODE -#include - -const WCHAR szCommonFileW[] = - {'c','h','i','l','d','d','a','t','a','.','t','m','p','\0'}; - -const WCHAR szChildFileW[] = u"threading/CreateProcessW/test1/paltest_createprocessw_test1_child"; - -#define szCommonStringA "058d2d057111a313aa82401c2e856002\0" - -PALTEST(threading_CreateProcessW_test1_paltest_createprocessw_test1, "threading/CreateProcessW/test1/paltest_createprocessw_test1") - -{ - - STARTUPINFOW si; - PROCESS_INFORMATION pi; - - static FILE * fp; - - DWORD dwFileLength; - DWORD dwDirLength; - DWORD dwSize; - - size_t cslen; - - char szReadStringA[256]; - - char szAbsPathNameA[MAX_PATH]; - WCHAR szDirNameW[_MAX_DIR]; - WCHAR absPathBuf[MAX_PATH]; - WCHAR *szAbsPathNameW; - - - if(0 != (PAL_Initialize(argc, argv))) - { - return ( FAIL ); - } - - ZeroMemory ( &si, sizeof(si) ); - si.cb = sizeof(si); - ZeroMemory ( &pi, sizeof(pi) ); - - szAbsPathNameW=&absPathBuf[0]; - - dwDirLength = GetTempPath(MAX_PATH, szDirNameW); - - if (0 == dwDirLength) - { - Fail ("GetTempPath call failed. Could not get " - "temp directory\n. Exiting.\n"); - } - - int mbwcResult = MultiByteToWideChar(CP_ACP, 0, argv[0], -1, szAbsPathNameW, sizeof(absPathBuf)); - - if (0 == mbwcResult) - { - Fail ("Palsuite Code: MultiByteToWideChar() call failed. Exiting.\n"); - } - - wcscat(szAbsPathNameW, u" "); - wcscat(szAbsPathNameW, szChildFileW); - - if ( !CreateProcessW ( NULL, - szAbsPathNameW, - NULL, - NULL, - FALSE, - CREATE_NEW_CONSOLE, - NULL, - NULL, - &si, - &pi ) - ) - { - Fail ( "CreateProcess call failed. GetLastError returned %d\n", - GetLastError() ); - } - - WaitForSingleObject ( pi.hProcess, INFINITE ); - - szAbsPathNameW=&absPathBuf[0]; - - dwFileLength = wcslen( szCommonFileW ); - - dwSize = mkAbsoluteFilenameW( szDirNameW, dwDirLength, szCommonFileW, - dwFileLength, szAbsPathNameW ); - - /* set the string length for the open call*/ - - if (0 == dwSize) - { - Fail ("Palsuite Code: mkAbsoluteFilename() call failed. Could " - "not build absolute path name to file\n. Exiting.\n"); - } - - WideCharToMultiByte (CP_ACP, 0, szAbsPathNameW, -1, szAbsPathNameA, - (dwSize + 1), NULL, NULL); - - if ( NULL == ( fp = fopen ( szAbsPathNameA , "r" ) ) ) - { - Fail ("%s\nunable to open %s\nfor reading. Exiting.\n", argv[0], - szAbsPathNameA ); - } - - cslen = strlen ( szCommonStringA ); - - if ( NULL == fgets( szReadStringA, (cslen + 1), fp )) - { - /* - * A return value of NULL indicates an error condition or an - * EOF condition - */ - Fail ("%s\nunable to read file\n%s\nszReadStringA is %s\n" - "Exiting.\n", argv[0], szAbsPathNameA, - szReadStringA ); - } - - if ( 0 != strncmp( szReadStringA, szCommonStringA, cslen )) - { - Fail ("string comparison failed.\n szReadStringA is %s and\n" - "szCommonStringA is %s\n", szReadStringA, - szCommonStringA ); - } - else - { - Trace ("string comparison passed.\n"); - } - - if (0 != (fclose ( fp ))) - { - Trace ("%s unable to close file %s. This may cause a file pointer " - "leak. Continuing.\n", argv[0], szAbsPathNameA ); - } - - /* Close process and thread handle */ - CloseHandle ( pi.hProcess ); - CloseHandle ( pi.hThread ); - - PAL_Terminate(); - return ( PASS ); - -} diff --git a/src/coreclr/pal/tests/palsuite/threading/CreateProcessW/test2/childprocess.cpp b/src/coreclr/pal/tests/palsuite/threading/CreateProcessW/test2/childprocess.cpp deleted file mode 100644 index 72ce2a4e67e1bf..00000000000000 --- a/src/coreclr/pal/tests/palsuite/threading/CreateProcessW/test2/childprocess.cpp +++ /dev/null @@ -1,77 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================ -** -** Source: createprocessw/test2/childprocess.c -** -** Purpose: This child process reads a string from stdin -** and writes it out to stdout & stderr -** -** Dependencies: memset -** fgets -** gputs -** - -** -**=========================================================*/ - -#define UNICODE -#include -#include "test2.h" - - -PALTEST(threading_CreateProcessW_test2_paltest_createprocessw_test2_child, "threading/CreateProcessW/test2/paltest_createprocessw_test2_child") -{ - int iRetCode = EXIT_OK_CODE; /* preset exit code to OK */ - char szBuf[BUF_LEN]; - - WCHAR *swzParam1, *swzParam2, *swzParam3 = NULL; - - - if(0 != (PAL_Initialize(argc, argv))) - { - return FAIL; - } - if (argc != 4) - { - return EXIT_ERR_CODE3; - } - - swzParam1 = convert(argv[1]); - swzParam2 = convert(argv[2]); - swzParam3 = convert(argv[3]); - - if (wcscmp(swzParam1, szArg1) != 0 - || wcscmp(swzParam2, szArg2) != 0 - || wcscmp(swzParam3, szArg3) != 0) - { - return EXIT_ERR_CODE4; - } - - free(swzParam1); - free(swzParam2); - free(swzParam3); - - memset(szBuf, 0, BUF_LEN); - - /* Read the string that was written by the parent */ - if (fgets(szBuf, BUF_LEN, stdin) == NULL) - { - return EXIT_ERR_CODE1; - } - - - /* Write the string out to the stdout & stderr pipes */ - if (fputs(szBuf, stdout) == EOF - || fputs(szBuf, stderr) == EOF) - { - return EXIT_ERR_CODE2; - } - - /* The exit code will indicate success or failure */ - PAL_TerminateEx(iRetCode); - - /* Return special exit code to indicate success or failure */ - return iRetCode; -} diff --git a/src/coreclr/pal/tests/palsuite/threading/CreateProcessW/test2/parentprocess.cpp b/src/coreclr/pal/tests/palsuite/threading/CreateProcessW/test2/parentprocess.cpp deleted file mode 100644 index 7c580a9ea9bd91..00000000000000 --- a/src/coreclr/pal/tests/palsuite/threading/CreateProcessW/test2/parentprocess.cpp +++ /dev/null @@ -1,253 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================ -** -** Source: createprocessw/test2/parentprocess.c -** -** Purpose: Test the following features of CreateProcessW: -** - Check to see if hProcess & hThread are set in -** return PROCESS_INFORMATION structure -** - Check to see if stdin, stdout, & stderr handles -** are used when STARTF_USESTDHANDLES is specified -** in STARUPINFO flags and bInheritHandles = TRUE -** - Check to see that proper arguments are passed to -** child process -** -** Dependencies: CreatePipe -** strcpy, strlen, strncmp, memset -** WaitForSingleObject -** WriteFile, ReadFile -** GetExitCodeProcess -** - -** -**=========================================================*/ - -#define UNICODE -#include -#include "test2.h" - - - -PALTEST(threading_CreateProcessW_test2_paltest_createprocessw_test2, "threading/CreateProcessW/test2/paltest_createprocessw_test2") -{ - - /******************************************* - * Declarations - *******************************************/ - STARTUPINFO si; - PROCESS_INFORMATION pi; - - HANDLE hTestStdInR = NULL; - HANDLE hTestStdInW = NULL; - HANDLE hTestStdOutR = NULL; - HANDLE hTestStdOutW = NULL; - HANDLE hTestStdErrR = NULL; - HANDLE hTestStdErrW = NULL; - - BOOL bRetVal = FALSE; - DWORD dwBytesWritten = 0; - DWORD dwBytesRead = 0; - DWORD dwExitCode = 0; - - SECURITY_ATTRIBUTES pipeAttributes; - - char szStdOutBuf[BUF_LEN]; - char szStdErrBuf[BUF_LEN]; - WCHAR szFullPathNameW[MAX_PATH]; - - - /******************************************* - * Initialization - *******************************************/ - - if(0 != (PAL_Initialize(argc, argv))) - { - return ( FAIL ); - } - - /*Setup SECURITY_ATTRIBUTES structure for CreatePipe*/ - pipeAttributes.nLength = sizeof(SECURITY_ATTRIBUTES); - pipeAttributes.lpSecurityDescriptor = NULL; - pipeAttributes.bInheritHandle = TRUE; - - - /*Create a StdIn pipe for child*/ - bRetVal = CreatePipe(&hTestStdInR, /* read handle*/ - &hTestStdInW, /* write handle */ - &pipeAttributes, /* security attributes*/ - 1024); /* pipe size*/ - - if (bRetVal == FALSE) - { - Fail("ERROR: %ld :Unable to create stdin pipe\n", GetLastError()); - } - - - /*Create a StdOut pipe for child*/ - bRetVal = CreatePipe(&hTestStdOutR, /* read handle*/ - &hTestStdOutW, /* write handle */ - &pipeAttributes, /* security attributes*/ - 0); /* pipe size*/ - - if (bRetVal == FALSE) - { - Fail("ERROR: %ld :Unable to create stdout pipe\n", GetLastError()); - } - - - /*Create a StdErr pipe for child*/ - bRetVal = CreatePipe(&hTestStdErrR, /* read handle*/ - &hTestStdErrW, /* write handle */ - &pipeAttributes, /* security attributes*/ - 0); /* pipe size*/ - - if (bRetVal == FALSE) - { - Fail("ERROR: %ld :Unable to create stderr pipe\n", GetLastError()); - } - - /* Zero the data structure space */ - ZeroMemory ( &pi, sizeof(pi) ); - ZeroMemory ( &si, sizeof(si) ); - - /* Set the process flags and standard io handles */ - si.cb = sizeof(si); - si.dwFlags = STARTF_USESTDHANDLES; - si.hStdInput = hTestStdInR; - si.hStdOutput = hTestStdOutW; - si.hStdError = hTestStdErrW; - - int mbwcResult = MultiByteToWideChar(CP_ACP, 0, argv[0], -1, szFullPathNameW, sizeof(szFullPathNameW)); - - if (0 == mbwcResult) - { - Fail ("Palsuite Code: MultiByteToWideChar() call failed. Exiting.\n"); - } - - wcscat(szFullPathNameW, u" "); - wcscat(szFullPathNameW, szChildFileW); - - wcscat(szFullPathNameW, szArgs); - - /******************************************* - * Start Testing - *******************************************/ - - /* Launch the child */ - if ( !CreateProcess (NULL, szFullPathNameW, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi )) - { - Fail("ERROR: CreateProcess call failed. GetLastError returned %d\n", - GetLastError() ); - } - - /* Check the returned process information for validity */ - if (pi.hProcess == 0 || pi.hThread == 0) - { - Fail("ERROR: CreateProcess Error: Process Handle = %u, Thread Handle = %u\n", - pi.hProcess, pi.hThread); - } - - - /* Write the Constructed string to stdin pipe for the child process */ - if (WriteFile(hTestStdInW, szTestString, strlen(szTestString), &dwBytesWritten, NULL) == FALSE - || WriteFile(hTestStdInW, "\n", strlen("\n"), &dwBytesWritten, NULL) == FALSE) - { - Fail("ERROR: %ld :unable to write to write pipe handle " - "hTestStdInW=0x%lx\n", GetLastError(), hTestStdInW); - } - - /* Wait for the child to finish, Max 20 seconds */ - dwExitCode = WaitForSingleObject(pi.hProcess, 20000); - - /* If the child failed then whole thing fails */ - if (dwExitCode != WAIT_OBJECT_0) - { - TerminateProcess(pi.hProcess, 0); - Fail("ERROR: The child failed to run properly.\n"); - } - - /* Check for problems in the child process */ - if (GetExitCodeProcess(pi.hProcess, &dwExitCode) == FALSE) - { - Fail("ERROR: Call to GetExitCodeProcess failed.\n"); - } - else if (dwExitCode == EXIT_ERR_CODE1) - { - Fail("ERROR: The Child process could not reead the string " - "written to the stdin pipe.\n"); - } - else if (dwExitCode == EXIT_ERR_CODE2) - { - Fail("ERROR: The Child process could not write the string " - "the stdout pipe or stderr pipe.\n"); - } - else if (dwExitCode == EXIT_ERR_CODE3) - { - Fail("ERROR: The Child received the wrong number of " - "command line arguments.\n"); - } - else if (dwExitCode == EXIT_ERR_CODE4) - { - Fail("ERROR: The Child received the wrong " - "command line arguments.\n"); - } - else if (dwExitCode != EXIT_OK_CODE) - { - Fail("ERROR: Unexpected exit code returned: %u. Child process " - "did not complete its part of the test.\n", dwExitCode); - } - - - /* The child ran ok, so check to see if we received the proper */ - /* strings through the pipes. */ - - /* clear our buffers */ - memset(szStdOutBuf, 0, BUF_LEN); - memset(szStdErrBuf, 0, BUF_LEN); - - /* Read the data back from the child process stdout */ - bRetVal = ReadFile(hTestStdOutR, /* handle to read pipe*/ - szStdOutBuf, /* buffer to write to*/ - BUF_LEN, /* number of bytes to read*/ - &dwBytesRead, /* number of bytes read*/ - NULL); /* overlapped buffer*/ - - /*Read the data back from the child process stderr */ - bRetVal = ReadFile(hTestStdErrR, /* handle to read pipe*/ - szStdErrBuf, /* buffer to write to*/ - BUF_LEN, /* number of bytes to read*/ - &dwBytesRead, /* number of bytes read*/ - NULL); /* overlapped buffer*/ - - - /* Confirm that we received the same string that we originally */ - /* wrote to the child and was received on both stdout & stderr.*/ - if (strncmp(szTestString, szStdOutBuf, strlen(szTestString)) != 0 - || strncmp(szTestString, szStdErrBuf, strlen(szTestString)) != 0) - { - Fail("ERROR: The data read back from child does not match " - "what was written. STDOUT: %s STDERR: %s\n", - szStdOutBuf, szStdErrBuf); - } - - - /******************************************* - * Clean Up - *******************************************/ - - /* Close process and thread handle */ - CloseHandle ( pi.hProcess ); - CloseHandle ( pi.hThread ); - - CloseHandle(hTestStdInR); - CloseHandle(hTestStdInW); - CloseHandle(hTestStdOutR); - CloseHandle(hTestStdOutW); - CloseHandle(hTestStdErrR); - CloseHandle(hTestStdErrW); - - PAL_Terminate(); - return ( PASS ); -} diff --git a/src/coreclr/pal/tests/palsuite/threading/CreateProcessW/test2/test2.h b/src/coreclr/pal/tests/palsuite/threading/CreateProcessW/test2/test2.h deleted file mode 100644 index 3036e7dae9414c..00000000000000 --- a/src/coreclr/pal/tests/palsuite/threading/CreateProcessW/test2/test2.h +++ /dev/null @@ -1,30 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================ -** -** Source: test2.h -** -** - -** -**=========================================================*/ - - -const WCHAR szChildFileW[] = u"threading/CreateProcessW/test2/paltest_createprocessw_test2_child"; -const WCHAR szArgs[] = {' ',0x41,' ','B',' ','C','\0'}; -const WCHAR szArg1[] = {0x41,'\0'}; -const WCHAR szArg2[] = {'B','\0'}; -const WCHAR szArg3[] = {'C','\0'}; - -#define szTestString "An uninteresting test string (it works though)" - -const DWORD EXIT_OK_CODE = 100; -const DWORD EXIT_ERR_CODE1 = 101; -const DWORD EXIT_ERR_CODE2 = 102; -const DWORD EXIT_ERR_CODE3 = 103; -const DWORD EXIT_ERR_CODE4 = 104; -const DWORD EXIT_ERR_CODE5 = 105; - -#define BUF_LEN 128 - diff --git a/src/coreclr/pal/tests/palsuite/threading/ExitThread/test2/childprocess.cpp b/src/coreclr/pal/tests/palsuite/threading/ExitThread/test2/childprocess.cpp deleted file mode 100644 index 2cd743fbd37721..00000000000000 --- a/src/coreclr/pal/tests/palsuite/threading/ExitThread/test2/childprocess.cpp +++ /dev/null @@ -1,40 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================ -** -** Source: childprocess.c -** -** Purpose: Test to ensure ExitThread returns the right -** value when shutting down the last thread of a process. -** All this program does is call ExitThread() with a predefined -** value. -** -** Dependencies: none -** - -** -**=========================================================*/ - -#include -#include "myexitcode.h" - -PALTEST(threading_ExitThread_test2_paltest_exitthread_test2_child, "threading/ExitThread/test2/paltest_exitthread_test2_child") -{ - /* initialize the PAL */ - if( PAL_Initialize(argc, argv) != 0 ) - { - return( FAIL ); - } - - /* exit the current thread with a magic test value -- it should */ - /* terminate the process and return that test value from this */ - /* program. */ - ExitThread( TEST_EXIT_CODE ); - - /* technically we should never get here */ - PAL_Terminate(); - - /* return failure */ - return FAIL; -} diff --git a/src/coreclr/pal/tests/palsuite/threading/ExitThread/test2/myexitcode.h b/src/coreclr/pal/tests/palsuite/threading/ExitThread/test2/myexitcode.h deleted file mode 100644 index f753316e23bebb..00000000000000 --- a/src/coreclr/pal/tests/palsuite/threading/ExitThread/test2/myexitcode.h +++ /dev/null @@ -1,13 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================================ -** -** Source: myexitcode.h -** -** Purpose: Define an exit code. -** -** -**==========================================================================*/ - -#define TEST_EXIT_CODE 316 diff --git a/src/coreclr/pal/tests/palsuite/threading/ExitThread/test2/test2.cpp b/src/coreclr/pal/tests/palsuite/threading/ExitThread/test2/test2.cpp deleted file mode 100644 index c14bd6ae731379..00000000000000 --- a/src/coreclr/pal/tests/palsuite/threading/ExitThread/test2/test2.cpp +++ /dev/null @@ -1,133 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================================= -** -** Source: test2.c -** -** Purpose: Test to ensure ExitThread() called from the last thread of -** a process shuts down that process and returns the proper -** exit code as specified in the ExitThread() call. -** -** Dependencies: PAL_Initialize -** PAL_Terminate -** Fail -** ZeroMemory -** GetCurrentDirectoryW -** CreateProcessW -** WaitForSingleObject -** GetLastError -** strlen -** strncpy -** - -** -**===========================================================================*/ -#include -#include "myexitcode.h" - -PALTEST(threading_ExitThread_test2_paltest_exitthread_test2, "threading/ExitThread/test2/paltest_exitthread_test2") -{ - const char* rgchChildFile = "childprocess"; - - STARTUPINFO si; - PROCESS_INFORMATION pi; - - DWORD dwError; - DWORD dwExitCode; - DWORD dwFileLength; - DWORD dwDirLength; - DWORD dwSize; - DWORD dwExpected = TEST_EXIT_CODE; - - char rgchDirName[_MAX_DIR]; - char absPathBuf[MAX_PATH]; - char* rgchAbsPathName; - - /* initialize the PAL */ - if( PAL_Initialize(argc, argv) != 0 ) - { - return( FAIL ); - } - - /* zero our process and startup info structures */ - ZeroMemory( &si, sizeof(si) ); - si.cb = sizeof( si ); - ZeroMemory( &pi, sizeof(pi) ); - - /* build the absolute path to the child process */ - rgchAbsPathName = &absPathBuf[0]; - dwFileLength = strlen( rgchChildFile ); - - strcpy(rgchDirName, ".\\"); - dwDirLength = strlen(rgchDirName); - - dwSize = mkAbsoluteFilename( rgchDirName, - dwDirLength, - rgchChildFile, - dwFileLength, - rgchAbsPathName ); - if( dwSize == 0 ) - { - Fail( "Palsuite Code: mkAbsoluteFilename() call failed. Could ", - "not build absolute path name to file\n. Exiting.\n" ); - } - - LPWSTR rgchAbsPathNameW = convert(rgchAbsPathName); - /* launch the child process */ - if( !CreateProcess( NULL, /* module name to execute */ - rgchAbsPathNameW, /* command line */ - NULL, /* process handle not */ - /* inheritable */ - NULL, /* thread handle not */ - /* inheritable */ - FALSE, /* handle inheritance */ - CREATE_NEW_CONSOLE, /* dwCreationFlags */ - NULL, /* use parent's environment */ - NULL, /* use parent's starting */ - /* directory */ - &si, /* startup info struct */ - &pi ) /* process info struct */ - ) - { - dwError = GetLastError(); - free(rgchAbsPathNameW); - Fail( "CreateProcess call failed with error code %d\n", - dwError ); - } - - free(rgchAbsPathNameW); - - /* wait for the child process to complete */ - WaitForSingleObject ( pi.hProcess, INFINITE ); - - /* check the exit code from the process */ - if( ! GetExitCodeProcess( pi.hProcess, &dwExitCode ) ) - { - dwError = GetLastError(); - CloseHandle ( pi.hProcess ); - CloseHandle ( pi.hThread ); - Fail( "GetExitCodeProcess call failed with error code %d\n", - dwError ); - } - - /* close process and thread handle */ - CloseHandle ( pi.hProcess ); - CloseHandle ( pi.hThread ); - - /* check for the expected exit code */ - /* exit code for some systems is as small as a char, so that's all */ - /* we'll compare for checking success */ - if( LOBYTE(LOWORD(dwExitCode)) != LOBYTE(LOWORD(dwExpected)) ) - { - Fail( "GetExitCodeProcess returned an incorrect exit code %d, " - "expected value is %d\n", - LOWORD(dwExitCode), dwExpected ); - } - - /* terminate the PAL */ - PAL_Terminate(); - - /* return success */ - return PASS; -} diff --git a/src/coreclr/pal/tests/palsuite/threading/GetExitCodeProcess/test1/childProcess.cpp b/src/coreclr/pal/tests/palsuite/threading/GetExitCodeProcess/test1/childProcess.cpp deleted file mode 100644 index 4848bb499804a0..00000000000000 --- a/src/coreclr/pal/tests/palsuite/threading/GetExitCodeProcess/test1/childProcess.cpp +++ /dev/null @@ -1,31 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================ -** -** Source: childprocess.c -** -** Purpose: Test to ensure GetExitCodeProcess returns the right -** value. All this program does is return a predefined value. -** -** Dependencies: none -** - -** -**=========================================================*/ - -#include -#include "myexitcode.h" -#include - -PALTEST(threading_GetExitCodeProcess_test1_paltest_getexitcodeprocess_test1_child, "threading/GetExitCodeProcess/test1/paltest_getexitcodeprocess_test1_child") -{ - int i; - - // simulate some activity - for( i=0; i<10000; i++ ) - ; - - // return the predefined exit code - return TEST_EXIT_CODE; -} diff --git a/src/coreclr/pal/tests/palsuite/threading/GetExitCodeProcess/test1/myexitcode.h b/src/coreclr/pal/tests/palsuite/threading/GetExitCodeProcess/test1/myexitcode.h deleted file mode 100644 index ddf0fb2a050b40..00000000000000 --- a/src/coreclr/pal/tests/palsuite/threading/GetExitCodeProcess/test1/myexitcode.h +++ /dev/null @@ -1,13 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================================ -** -** Source: myexitcode.h -** -** Purpose: Define an exit code. -** -** -**==========================================================================*/ - -#define TEST_EXIT_CODE 104 diff --git a/src/coreclr/pal/tests/palsuite/threading/GetExitCodeProcess/test1/test1.cpp b/src/coreclr/pal/tests/palsuite/threading/GetExitCodeProcess/test1/test1.cpp deleted file mode 100644 index 7fa6ddc993a08d..00000000000000 --- a/src/coreclr/pal/tests/palsuite/threading/GetExitCodeProcess/test1/test1.cpp +++ /dev/null @@ -1,128 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================================= -** -** Source: test1.c -** -** Purpose: Test to ensure GetExitCodeProcess works properly. -** -** Dependencies: PAL_Initialize -** PAL_Terminate -** Fail -** ZeroMemory -** GetCurrentDirectoryW -** CreateProcessW -** WaitForSingleObject -** GetLastError -** strlen -** strncpy -** - -** -**===========================================================================*/ -#include -#include "myexitcode.h" - -PALTEST(threading_GetExitCodeProcess_test1_paltest_getexitcodeprocess_test1, "threading/GetExitCodeProcess/test1/paltest_getexitcodeprocess_test1") -{ - const char* rgchChildFile = "childprocess"; - - STARTUPINFO si; - PROCESS_INFORMATION pi; - - DWORD dwError; - DWORD dwExitCode; - DWORD dwFileLength; - DWORD dwDirLength; - DWORD dwSize; - - char rgchDirName[_MAX_DIR]; - char absPathBuf[MAX_PATH]; - char* rgchAbsPathName; - - /* initialize the PAL */ - if( PAL_Initialize(argc, argv) != 0 ) - { - return( FAIL ); - } - - /* zero our process and startup info structures */ - ZeroMemory( &si, sizeof(si) ); - si.cb = sizeof( si ); - ZeroMemory( &pi, sizeof(pi) ); - - /* build the absolute path to the child process */ - rgchAbsPathName = &absPathBuf[0]; - dwFileLength = strlen( rgchChildFile ); - - strcpy(rgchDirName, ".\\"); - dwDirLength = strlen(rgchDirName); - - dwSize = mkAbsoluteFilename( rgchDirName, - dwDirLength, - rgchChildFile, - dwFileLength, - rgchAbsPathName ); - if( dwSize == 0 ) - { - Fail( "Palsuite Code: mkAbsoluteFilename() call failed. Could ", - "not build absolute path name to file\n. Exiting.\n" ); - } - - LPWSTR rgchAbsPathNameW = convert(rgchAbsPathName); - /* launch the child process */ - if( !CreateProcess( NULL, /* module name to execute */ - rgchAbsPathNameW, /* command line */ - NULL, /* process handle not */ - /* inheritable */ - NULL, /* thread handle not */ - /* inheritable */ - FALSE, /* handle inheritance */ - CREATE_NEW_CONSOLE, /* dwCreationFlags */ - NULL, /* use parent's environment */ - NULL, /* use parent's starting */ - /* directory */ - &si, /* startup info struct */ - &pi ) /* process info struct */ - ) - { - dwError = GetLastError(); - free(rgchAbsPathNameW); - Fail( "CreateProcess call failed with error code %d\n", - dwError ); - } - - free(rgchAbsPathNameW); - - /* wait for the child process to complete */ - WaitForSingleObject ( pi.hProcess, INFINITE ); - - /* check the exit code from the process */ - if( ! GetExitCodeProcess( pi.hProcess, &dwExitCode ) ) - { - dwError = GetLastError(); - CloseHandle ( pi.hProcess ); - CloseHandle ( pi.hThread ); - Fail( "GetExitCodeProcess call failed with error code %d\n", - dwError ); - } - - /* close process and thread handle */ - CloseHandle ( pi.hProcess ); - CloseHandle ( pi.hThread ); - - /* check for the expected exit code */ - if( dwExitCode != TEST_EXIT_CODE ) - { - Fail( "GetExitCodeProcess returned an incorrect exit code %d, " - "expected value is %d\n", - dwExitCode, TEST_EXIT_CODE ); - } - - /* terminate the PAL */ - PAL_Terminate(); - - /* return success */ - return PASS; -} diff --git a/src/coreclr/pal/tests/palsuite/threading/OpenEventW/test3/childprocess.cpp b/src/coreclr/pal/tests/palsuite/threading/OpenEventW/test3/childprocess.cpp deleted file mode 100644 index a868b7da4d69da..00000000000000 --- a/src/coreclr/pal/tests/palsuite/threading/OpenEventW/test3/childprocess.cpp +++ /dev/null @@ -1,80 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================ -** -** Source: childprocess.c -** -** Purpose: Test to ensure that OpenEventW() works when -** opening an event created by another process. The test -** program launches this program as a child, which creates -** a named, initially-unset event. The child waits up to -** 10 seconds for the parent process to open that event -** and set it, and returns PASS if the event was set or FAIL -** otherwise. The parent process checks the return value -** from the child to verify that the opened event was -** properly used across processes. -** -** Dependencies: PAL_Initialize -** PAL_Terminate -** CreateEventW -** WaitForSingleObject -** CloseHandle -** -** -**=========================================================*/ - -#include - -PALTEST(threading_OpenEventW_test3_paltest_openeventw_test3_child, "threading/OpenEventW/test3/paltest_openeventw_test3_child") -{ - /* local variables */ - HANDLE hEvent = NULL; - WCHAR wcName[] = {'P','A','L','R','o','c','k','s','\0'}; - LPWSTR lpName = wcName; - - int result = PASS; - - /* initialize the PAL */ - if( PAL_Initialize(argc, argv) != 0 ) - { - return( FAIL ); - } - - - /* open a handle to the event created in the child process */ - hEvent = OpenEventW( EVENT_ALL_ACCESS, /* we want all rights */ - FALSE, /* no inherit */ - lpName ); - - if( hEvent == NULL ) - { - /* ERROR */ - Trace( "ERROR:%lu:OpenEventW() call failed\n", GetLastError() ); - result = FAIL; - goto parentwait; - } - - /* set the event -- should take effect in the child process */ - if( ! SetEvent( hEvent ) ) - { - /* ERROR */ - Trace( "ERROR:%lu:SetEvent() call failed\n", GetLastError() ); - result = FAIL; - } - -parentwait: - /* close the event handle */ - if( ! CloseHandle( hEvent ) ) - { - /* ERROR */ - Fail( "ERROR:%lu:CloseHandle() call failed in child\n", - GetLastError()); - } - - /* terminate the PAL */ - PAL_TerminateEx(result); - - /* return success or failure */ - return result; -} diff --git a/src/coreclr/pal/tests/palsuite/threading/OpenEventW/test3/test3.cpp b/src/coreclr/pal/tests/palsuite/threading/OpenEventW/test3/test3.cpp deleted file mode 100644 index 58e21465f9a25f..00000000000000 --- a/src/coreclr/pal/tests/palsuite/threading/OpenEventW/test3/test3.cpp +++ /dev/null @@ -1,191 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================================= -** -** Source: test3.c -** -** Purpose: Test to ensure that OpenEventW() works when -** opening an event created by another process. This test -** program launches a child process which creates a -** named, initially-unset event. The child waits up to -** 10 seconds for the parent process to open that event -** and set it, and returns PASS if the event was set or FAIL -** otherwise. The parent process checks the return value -** from the child to verify that the opened event was -** properly used across processes. -** -** Dependencies: PAL_Initialize -** PAL_Terminate -** Fail -** ZeroMemory -** GetCurrentDirectoryW -** CreateProcessW -** WaitForSingleObject -** GetExitCodeProcess -** GetLastError -** strlen -** strncpy -** -** -**===========================================================================*/ -#include - -#define TIMEOUT 60000 - -PALTEST(threading_OpenEventW_test3_paltest_openeventw_test3, "threading/OpenEventW/test3/paltest_openeventw_test3") -{ - BOOL ret = FAIL; - LPSECURITY_ATTRIBUTES lpEventAttributes = NULL; - - STARTUPINFO si; - PROCESS_INFORMATION pi; - - DWORD dwExitCode; - - DWORD dwRet = 0; - HANDLE hEvent = NULL; - WCHAR wcName[] = {'P','A','L','R','o','c','k','s','\0'}; - LPWSTR lpName = wcName; - char lpCommandLine[MAX_PATH] = ""; - - /* initialize the PAL */ - if( PAL_Initialize(argc, argv) != 0 ) - { - return( FAIL ); - } - - /* zero our process and startup info structures */ - ZeroMemory( &si, sizeof(si) ); - si.cb = sizeof( si ); - ZeroMemory( &pi, sizeof(pi) ); - - /* create an event which we can use with SetEvent */ - hEvent = CreateEventW( lpEventAttributes, - TRUE, /* manual reset */ - FALSE, /* unsignalled */ - lpName ); - - if( hEvent == NULL ) - { - /* ERROR */ - Fail( "ERROR:%lu:CreateEventW() call failed in child\n", - GetLastError()); - } - - ZeroMemory( lpCommandLine, MAX_PATH ); - if ( sprintf_s( lpCommandLine, MAX_PATH-1, "childprocess ") < 0 ) - { - Fail ("Error: Insufficient lpCommandline for\n"); - } - - LPWSTR lpCommandLineW = convert(lpCommandLine); - /* launch the child process */ - if( !CreateProcess( NULL, /* module name to execute */ - lpCommandLineW, /* command line */ - NULL, /* process handle not */ - /* inheritable */ - NULL, /* thread handle not */ - /* inheritable */ - FALSE, /* handle inheritance */ - CREATE_NEW_CONSOLE, /* dwCreationFlags */ - NULL, /* use parent's environment */ - NULL, /* use parent's starting */ - /* directory */ - &si, /* startup info struct */ - &pi ) /* process info struct */ - ) - { - DWORD dwError = GetLastError(); - free(lpCommandLineW); - Fail( "ERROR:%lu:CreateProcess call failed\n", - dwError); - } - - free(lpCommandLineW); - - /* verify that the event is signalled by the child process */ - dwRet = WaitForSingleObject( hEvent, TIMEOUT ); - if( dwRet != WAIT_OBJECT_0 ) - { - ret = FAIL; - /* ERROR */ - Trace( "ERROR:WaitForSingleObject() call returned %lu, " - "expected WAIT_TIMEOUT\n", - "expected WAIT_OBJECT_0\n", - dwRet ); - - goto cleanup; - - if( !CloseHandle( hEvent ) ) - { - Trace( "ERROR:%lu:CloseHandle() call failed in child\n", - GetLastError()); - } - goto cleanup; - } - - /* wait for the child process to complete */ - dwRet = WaitForSingleObject ( pi.hProcess, TIMEOUT ); - if( dwRet != WAIT_OBJECT_0 ) - { - ret = FAIL; - Trace( "ERROR:WaitForSingleObject() returned %lu, " - "expected %lu\n", - dwRet, - WAIT_OBJECT_0 ); - goto cleanup; - } - - /* check the exit code from the process */ - if( ! GetExitCodeProcess( pi.hProcess, &dwExitCode ) ) - { - ret = FAIL; - Trace( "ERROR:%lu:GetExitCodeProcess call failed\n", - GetLastError() ); - goto cleanup; - } - - /* check for success */ - ret = (dwExitCode == PASS) ? PASS : FAIL; - -cleanup: - if( hEvent != NULL ) - { - if( ! CloseHandle ( hEvent ) ) - { - Trace( "ERROR:%lu:CloseHandle call failed on event handle\n", - GetLastError() ); - ret = FAIL; - } - } - - - /* close process and thread handle */ - if( ! CloseHandle ( pi.hProcess ) ) - { - Trace( "ERROR:%lu:CloseHandle call failed on process handle\n", - GetLastError() ); - ret = FAIL; - } - - if( ! CloseHandle ( pi.hThread ) ) - { - Trace( "ERROR:%lu:CloseHandle call failed on thread handle\n", - GetLastError() ); - ret = FAIL; - } - - /* output a convenient error message and exit if we failed */ - if( ret == FAIL ) - { - Fail( "test failed\n" ); - } - - - /* terminate the PAL */ - PAL_Terminate(); - - /* return success */ - return ret; -} diff --git a/src/coreclr/pal/tests/palsuite/threading/OpenProcess/test1/childProcess.cpp b/src/coreclr/pal/tests/palsuite/threading/OpenProcess/test1/childProcess.cpp deleted file mode 100644 index 9c12e981ec3663..00000000000000 --- a/src/coreclr/pal/tests/palsuite/threading/OpenProcess/test1/childProcess.cpp +++ /dev/null @@ -1,44 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================ -** -** Source: childprocess.c -** -** Purpose: Test to ensure OpenProcess works properly. -** All this program does is return a predefined value. -** -** Dependencies: PAL_Initialize -** PAL_Terminate -** CreateMutexW -** WaitForSingleObject -** CloseHandle -** -** -**=========================================================*/ - -#include -#include "myexitcode.h" - - -PALTEST(threading_OpenProcess_test1_paltest_openprocess_test1_child, "threading/OpenProcess/test1/paltest_openprocess_test1_child") -{ - DWORD dwRet; - int i; - - /* initialize the PAL */ - if( PAL_Initialize(argc, argv) != 0 ) - { - return( FAIL ); - } - - /* simulate some activity */ - for( i=0; i<50000; i++ ) - ; - - /* terminate the PAL */ - PAL_Terminate(); - - /* return the predefined exit code */ - return TEST_EXIT_CODE; -} diff --git a/src/coreclr/pal/tests/palsuite/threading/OpenProcess/test1/myexitcode.h b/src/coreclr/pal/tests/palsuite/threading/OpenProcess/test1/myexitcode.h deleted file mode 100644 index 89d0be7cb32a8d..00000000000000 --- a/src/coreclr/pal/tests/palsuite/threading/OpenProcess/test1/myexitcode.h +++ /dev/null @@ -1,13 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================================ -** -** Source: myexitcode.h -** -** Purpose: Define an exit code. -** -** -**==========================================================================*/ - -#define TEST_EXIT_CODE 317 diff --git a/src/coreclr/pal/tests/palsuite/threading/OpenProcess/test1/test1.cpp b/src/coreclr/pal/tests/palsuite/threading/OpenProcess/test1/test1.cpp deleted file mode 100644 index 5c5e2d3e5d19bf..00000000000000 --- a/src/coreclr/pal/tests/palsuite/threading/OpenProcess/test1/test1.cpp +++ /dev/null @@ -1,198 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================================= -** -** Source: test1.c -** -** Purpose: Test to ensure OpenProcess works properly. -** -** Dependencies: PAL_Initialize -** PAL_Terminate -** Fail -** ZeroMemory -** GetCurrentDirectoryW -** CreateProcessW -** WaitForSingleObject -** CreateMutexW -** ReleaseMutex -** CloseHandle -** GetLastError -** strlen -** strncpy -** -** -**===========================================================================*/ -#include -#include "myexitcode.h" - -PALTEST(threading_OpenProcess_test1_paltest_openprocess_test1, "threading/OpenProcess/test1/paltest_openprocess_test1") -{ - const char* rgchChildFile = "childprocess"; - - STARTUPINFO si; - PROCESS_INFORMATION pi; - - DWORD dwError; - DWORD dwExitCode; - DWORD dwFileLength; - DWORD dwDirLength; - DWORD dwSize; - DWORD dwRet; - - HANDLE hChildProcess; - - char rgchDirName[_MAX_DIR]; - char absPathBuf[MAX_PATH]; - char* rgchAbsPathName; - - BOOL ret = FAIL; - BOOL bChildDone = FALSE; - - /* initialize the PAL */ - if( PAL_Initialize(argc, argv) != 0 ) - { - return( FAIL ); - } - - /* zero our process and startup info structures */ - ZeroMemory( &si, sizeof(si) ); - si.cb = sizeof( si ); - ZeroMemory( &pi, sizeof(pi) ); - - /* build the absolute path to the child process */ - rgchAbsPathName = &absPathBuf[0]; - dwFileLength = strlen( rgchChildFile ); - - strcpy(rgchDirName, ".\\"); - dwDirLength = strlen(rgchDirName); - - dwSize = mkAbsoluteFilename( rgchDirName, - dwDirLength, - rgchChildFile, - dwFileLength, - rgchAbsPathName ); - if( dwSize == 0 ) - { - Fail( "Palsuite Code: mkAbsoluteFilename() call failed. Could ", - "not build absolute path name to file\n. Exiting.\n" ); - } - - LPWSTR rgchAbsPathNameW = convert(rgchAbsPathName); - /* launch the child process */ - if( !CreateProcess( NULL, /* module name to execute */ - rgchAbsPathNameW, /* command line */ - NULL, /* process handle not */ - /* inheritable */ - NULL, /* thread handle not */ - /*inheritable */ - FALSE, /* handle inheritance */ - CREATE_NEW_CONSOLE, /* dwCreationFlags */ - NULL, /* use parent's environment */ - NULL, /* use parent's starting */ - /* directory */ - &si, /* startup info struct */ - &pi ) /* process info struct */ - ) - { - dwError = GetLastError(); - free(rgchAbsPathNameW); - Fail( "CreateProcess call failed with error code %d\n", - dwError ); - } - - free(rgchAbsPathNameW); - - /* open another handle to the child process */ - hChildProcess = OpenProcess( PROCESS_ALL_ACCESS, /* access */ - FALSE, /* inheritable */ - pi.dwProcessId /* process id */ - ); - if( hChildProcess == NULL ) - { - dwError = GetLastError(); - Trace( "ERROR:%lu:OpenProcess call failed\n", dwError ); - goto cleanup2; - } - - /* wait for the child process to complete, using the new handle */ - dwRet = WaitForSingleObject( hChildProcess, 10000 ); - if( dwRet != WAIT_OBJECT_0 ) - { - Trace( "ERROR:WaitForSingleObject call returned %lu, " - "expected WAIT_OBJECT_0", - dwRet ); - goto cleanup; - } - - /* remember that we waited until the child was finished */ - bChildDone = TRUE; - - /* check the exit code from the process -- this is a bit of an */ - /* extra verification that we opened the correct process handle */ - if( ! GetExitCodeProcess( hChildProcess, &dwExitCode ) ) - { - Trace( "ERROR:%lu:GetExitCodeProcess call failed\n", GetLastError() ); - goto cleanup; - } - - /* verification */ - if( (dwExitCode & 0xFF) != (TEST_EXIT_CODE & 0xFF) ) - { - Trace( "GetExitCodeProcess returned an incorrect exit code %d, " - "expected value is %d\n", - (dwExitCode & 0xFF), - (TEST_EXIT_CODE & 0xFF)); - goto cleanup; - } - - /* success if we get here */ - ret = PASS; - - -cleanup: - /* wait on the child process to complete if necessary */ - if( ! bChildDone ) - { - dwRet = WaitForSingleObject( hChildProcess, 10000 ); - if( dwRet != WAIT_OBJECT_0 ) - { - Trace( "ERROR:WaitForSingleObject call returned %lu, " - "expected WAIT_OBJECT_0", - dwRet ); - ret = FAIL; - } - } - - /* close all our handles */ - if( CloseHandle ( hChildProcess ) == 0 ) - { - Trace( "ERROR:%lu:CloseHandle() call failed\n", GetLastError() ); - ret = FAIL; - } - -cleanup2: - if( CloseHandle ( pi.hProcess ) == 0 ) - { - Trace( "ERROR:%lu:CloseHandle() call failed\n", GetLastError() ); - ret = FAIL; - } - if( CloseHandle ( pi.hThread ) == 0 ) - { - Trace( "ERROR:%lu:CloseHandle() call failed\n", GetLastError() ); - ret = FAIL; - } - - if( ret == FAIL ) - { - Fail( "test failed\n" ); - } - - - - /* terminate the PAL */ - PAL_Terminate(); - - /* return success */ - return PASS; -} diff --git a/src/coreclr/pal/tests/palsuite/threading/WaitForMultipleObjectsEx/test5/commonconsts.h b/src/coreclr/pal/tests/palsuite/threading/WaitForMultipleObjectsEx/test5/commonconsts.h deleted file mode 100644 index d4e26e4d478307..00000000000000 --- a/src/coreclr/pal/tests/palsuite/threading/WaitForMultipleObjectsEx/test5/commonconsts.h +++ /dev/null @@ -1,41 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================= -** -** Source: commonconsts.h -** -** -**============================================================*/ - -#ifndef _COMMONCONSTS_H_ -#define _COMMONCONSTS_H_ - -#include - -const int TIMEOUT = 60 * 5 * 1000; - -#define szcHelperProcessStartEvName "start" -#define szcHelperProcessReadyEvName "ready" -#define szcHelperProcessFinishEvName "finish" - -/* PEDANTIC and PEDANTIC0 is a helper macro that just grumps about any - * zero return codes in a generic way. with little typing */ -#define PEDANTIC(function, parameters) \ -{ \ - if (! (function parameters) ) \ - { \ - Trace("%s: NonFatal failure of %s%s for reasons %u and %u\n", \ - __FILE__, #function, #parameters, GetLastError(), errno); \ - } \ -} -#define PEDANTIC1(function, parameters) \ -{ \ - if ( (function parameters) ) \ - { \ - Trace("%s: NonFatal failure of %s%s for reasons %u and %u\n", \ - __FILE__, #function, #parameters, GetLastError(), errno); \ - } \ -} - -#endif diff --git a/src/coreclr/pal/tests/palsuite/threading/WaitForMultipleObjectsEx/test5/helper.cpp b/src/coreclr/pal/tests/palsuite/threading/WaitForMultipleObjectsEx/test5/helper.cpp deleted file mode 100644 index de39bad98a3c7b..00000000000000 --- a/src/coreclr/pal/tests/palsuite/threading/WaitForMultipleObjectsEx/test5/helper.cpp +++ /dev/null @@ -1,121 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================= -** -** Source: helper.c -** -** Purpose: This helper process sets up signals to communicate -** with the test thread in the parent process, and let the test -** thread signal this process when to exit. -** -** -**============================================================*/ - -#include "commonconsts.h" - -#include - -HANDLE hProcessStartEvent_WFMO_test5_helper; -HANDLE hProcessReadyEvent; -HANDLE hProcessFinishEvent; -HANDLE hProcessCleanupEvent; - - -PALTEST(threading_WaitForMultipleObjectsEx_test5_paltest_waitformultipleobjectsex_test5_helper, "threading/WaitForMultipleObjectsEx/test5/paltest_waitformultipleobjectsex_test5_helper") -{ - - BOOL success = TRUE; /* assume success */ - DWORD dwRet; - DWORD dwProcessId; - char szEventName[MAX_LONGPATH]; - PWCHAR uniString; - - if(0 != (PAL_Initialize(argc, argv))) - { - return FAIL; - } - - /* Open the event to let test thread tell us to get started. */ - uniString = convert(szcHelperProcessStartEvName); - hProcessStartEvent_WFMO_test5_helper = OpenEventW(EVENT_ALL_ACCESS, 0, uniString); - free(uniString); - if (!hProcessStartEvent_WFMO_test5_helper) - { - Fail("helper.main: OpenEvent of '%S' failed (%u). " - "(the event should already exist!)\n", - szcHelperProcessStartEvName, GetLastError()); - } - - /* Wait for signal from test thread. */ - dwRet = WaitForSingleObject(hProcessStartEvent_WFMO_test5_helper, TIMEOUT); - if (dwRet != WAIT_OBJECT_0) - { - Fail("helper.main: WaitForSingleObject '%s' failed\n" - "LastError:(%u)\n", szcHelperProcessStartEvName, GetLastError()); - } - - dwProcessId = GetCurrentProcessId(); - - if ( 0 >= dwProcessId ) - { - Fail ("helper.main: %s has invalid pid %d\n", argv[0], dwProcessId ); - } - - /* Open the event to tell test thread we are ready. */ - if (sprintf_s(szEventName, MAX_LONGPATH-1, "%s%d", szcHelperProcessReadyEvName, dwProcessId) < 0) - { - Fail ("helper.main: Insufficient event name string length for pid=%d\n", dwProcessId); - } - - uniString = convert(szEventName); - - hProcessReadyEvent = OpenEventW(EVENT_ALL_ACCESS, 0, uniString); - free(uniString); - if (!hProcessReadyEvent) - { - Fail("helper.main: OpenEvent of '%s' failed (%u). " - "(the event should already exist!)\n", - szEventName, GetLastError()); - } - - /* Open the event to let test thread tell us to exit. */ - if (sprintf_s(szEventName, MAX_LONGPATH-1, "%s%d", szcHelperProcessFinishEvName, dwProcessId) < 0) - { - Fail ("helper.main: Insufficient event name string length for pid=%d\n", dwProcessId); - } - - uniString = convert(szEventName); - - hProcessFinishEvent = OpenEventW(EVENT_ALL_ACCESS, 0, uniString); - free(uniString); - if (!hProcessFinishEvent) - { - Fail("helper.main: OpenEvent of '%s' failed LastError:(%u).\n", - szEventName, GetLastError()); - } - - /* Tell the test thread we are ready. */ - if (!SetEvent(hProcessReadyEvent)) - { - Fail("helper.main: SetEvent '%s' failed LastError:(%u)\n", - hProcessReadyEvent, GetLastError()); - } - - /* Wait for signal from test thread before exit. */ - dwRet = WaitForSingleObject(hProcessFinishEvent, TIMEOUT); - if (WAIT_OBJECT_0 != dwRet) - { - Fail("helper.main: WaitForSingleObject '%s' failed pid=%d\n" - "LastError:(%u)\n", - szcHelperProcessFinishEvName, dwProcessId, GetLastError()); - } - - PEDANTIC(CloseHandle, (hProcessStartEvent_WFMO_test5_helper)); - PEDANTIC(CloseHandle, (hProcessReadyEvent)); - PEDANTIC(CloseHandle, (hProcessFinishEvent)); - - PAL_TerminateEx(success ? PASS : FAIL); - - return success ? PASS : FAIL; -} diff --git a/src/coreclr/pal/tests/palsuite/threading/WaitForMultipleObjectsEx/test5/test5.cpp b/src/coreclr/pal/tests/palsuite/threading/WaitForMultipleObjectsEx/test5/test5.cpp deleted file mode 100644 index ab943b11a17060..00000000000000 --- a/src/coreclr/pal/tests/palsuite/threading/WaitForMultipleObjectsEx/test5/test5.cpp +++ /dev/null @@ -1,505 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================= -** -** Source: test5.c -** -** Purpose: Test the functionality of simultaneously waiting -** on multiple processes. Create the same number of helper -** processes and helper threads. -** Helper threads wait on helper processes to finish. -** Helper processes wait on the event signal from test -** thread before exit. -** The test thread can wake up one helper -** thread at a time by signaling the corresponding helper -** process to finish. -** The test thread can also wake up all helper threads at once -** by signaling help process 0 to exit. -** -** -**============================================================*/ - -#define UNICODE - -#include "commonconsts.h" - -#include - -/* The maximum number of objects a thread can wait is MAXIMUM_WAIT_OBJECTS. - The last helper thread in this test case will wait on all helper processes - plus a thread finish event so the maximum number of helper processes - can be created in this test case is (MAXIMUM_WAIT_OBJECTS-1). */ -#define MAX_HELPER_PROCESS (MAXIMUM_WAIT_OBJECTS-1) - -int MaxNumHelperProcess = MAX_HELPER_PROCESS; - -/* indicate how the test thread wake up helper thread. */ -typedef enum _TestCaseType { - WakeUpOneThread, /* wake up one helper thread at a time. */ - WakeUpAllThread /* wake up all helper threads at once */ -} TestCaseType; - -TestCaseType TestCase = WakeUpOneThread; - -/* When the test thread wakes up one thread at a time, - ThreadIndexOfThreadFinishEvent specifies the index of the thread that - should be waked up using hThreadFinishEvent instead of helper process. */ -DWORD ThreadIndexOfThreadFinishEvent = 0; - -struct helper_process_t -{ - PROCESS_INFORMATION pi; - HANDLE hProcessReadyEvent; - HANDLE hProcessFinishEvent; -} helper_process[MAX_HELPER_PROCESS]; - -HANDLE hProcessStartEvent_WFMO_test5; - -struct helper_thread_t -{ - HANDLE hThread; - DWORD dwThreadId; - HANDLE hThreadReadyEvent; - HANDLE hThreadFinishEvent; -} helper_thread[MAX_HELPER_PROCESS]; - -/* - * Entry Point for helper thread. - */ -DWORD PALAPI WaitForProcess(LPVOID lpParameter) -{ - DWORD index, i; - DWORD dwRet; - HANDLE handles[MAX_HELPER_PROCESS+1]; - - index = (DWORD)(SIZE_T) lpParameter; - - /* The helper thread 0 will wait for helper process 0, helper thread 1 will - wait for helper process 0 and 1, helper thread 2 will wait for helper - process 0, 1, and 2, and so on ..., and the last helper thread will wait - on all helper processes. - Each helper thread also waits on hThreadFinishEvent so that - it can exit without waiting on any process to finish. */ - - for (i = 0; i <= index; i++) - { - handles[i] = helper_process[i].pi.hProcess; - } - - handles[index+1] = helper_thread[index].hThreadFinishEvent; - - if(!SetEvent(helper_thread[index].hThreadReadyEvent)) - { - Fail("test5.WaitProcess: SetEvent of hThreadReadyEvent failed for thread %d. " - "GetLastError() returned %d.\n", index, - GetLastError()); - } - - dwRet = WaitForMultipleObjectsEx(index+2, &handles[0], FALSE, TIMEOUT, TRUE); - if (WakeUpAllThread == TestCase) - { - /* If the test thread signals helper process 0 to exit, all threads will be waked up, - and the return value must be (WAIT_OBJECT_0+0) because the handle of helper process 0 - is in handle[0]. */ - if (dwRet != (WAIT_OBJECT_0+0)) - { - Fail("test5.WaitForProcess: invalid return value %d for WakupAllThread from WaitForMultipleObjectsEx for thread %d\n" - "LastError:(%u)\n", - dwRet, index, - GetLastError()); - } - } - else if (WakeUpOneThread == TestCase) - { - /* If the test thread wakes up one helper thread at a time, - the return value must be either (WAIT_OBJECT_0+index) if the helper thread - wakes up because the corresponding help process exits, - or (index+1) if the helper thread wakes up because of hThreadReadyEvent. */ - if ((index != ThreadIndexOfThreadFinishEvent && dwRet != (WAIT_OBJECT_0+index)) || - (index == ThreadIndexOfThreadFinishEvent && dwRet != (index+1))) - { - Fail("test5.WaitForProcess: invalid return value %d for WakupOneThread from WaitForMultipleObjectsEx for thread %d\n" - "LastError:(%u)\n", - dwRet, index, - GetLastError()); - } - } - else - { - Fail("Unknown TestCase %d\n", TestCase); - } - return 0; -} - -/* - * Setup the helper processes and helper threads. - */ -void -Setup() -{ - - STARTUPINFO si; - DWORD dwRet; - int i; - - char szEventName[MAX_PATH]; - PWCHAR uniStringHelper; - PWCHAR uniString; - - /* Create the event to start helper process after it was created. */ - uniString = convert(szcHelperProcessStartEvName); - hProcessStartEvent_WFMO_test5 = CreateEvent(NULL, TRUE, FALSE, uniString); - free(uniString); - if (!hProcessStartEvent_WFMO_test5) - { - Fail("test5.Setup: CreateEvent of '%s' failed. " - "GetLastError() returned %d.\n", szcHelperProcessStartEvName, - GetLastError()); - } - - /* Create the helper processes. */ - ZeroMemory( &si, sizeof(si) ); - si.cb = sizeof(si); - uniStringHelper = convert("helper"); - for (i = 0; i < MaxNumHelperProcess; i++) - { - ZeroMemory( &helper_process[i].pi, sizeof(PROCESS_INFORMATION)); - - if(!CreateProcess( NULL, uniStringHelper, NULL, NULL, - FALSE, 0, NULL, NULL, &si, &helper_process[i].pi)) - { - Fail("test5.Setup: CreateProcess failed to load executable for helper process %d. " - "GetLastError() returned %u.\n", - i, GetLastError()); - } - - /* Create the event to let helper process tell us it is ready. */ - if (sprintf_s(szEventName, MAX_PATH-1, "%s%d", - szcHelperProcessReadyEvName, helper_process[i].pi.dwProcessId) < 0) - { - Fail ("test5.Setup: Insufficient event name string length for %s\n", szcHelperProcessReadyEvName); - } - - uniString = convert(szEventName); - - helper_process[i].hProcessReadyEvent = CreateEvent(NULL, FALSE, FALSE, uniString); - free(uniString); - if (!helper_process[i].hProcessReadyEvent) - { - Fail("test5.Setup: CreateEvent of '%s' failed. " - "GetLastError() returned %d.\n", szEventName, - GetLastError()); - } - - /* Create the event to tell helper process to exit. */ - if (sprintf_s(szEventName, MAX_PATH-1, "%s%d", - szcHelperProcessFinishEvName, helper_process[i].pi.dwProcessId) < 0) - { - Fail ("test5.Setup: Insufficient event name string length for %s\n", szcHelperProcessFinishEvName); - } - - uniString = convert(szEventName); - - helper_process[i].hProcessFinishEvent = CreateEvent(NULL, TRUE, FALSE, uniString); - free(uniString); - if (!helper_process[i].hProcessFinishEvent) - { - Fail("test5.Setup: CreateEvent of '%s' failed. " - "GetLastError() returned %d.\n", szEventName, - GetLastError()); - } - - } - free(uniStringHelper); - - /* Signal all helper processes to start. */ - if (!SetEvent(hProcessStartEvent_WFMO_test5)) - { - Fail("test5.Setup: SetEvent '%s' failed\n", - "LastError:(%u)\n", - szcHelperProcessStartEvName, GetLastError()); - } - - /* Wait for ready signals from all helper processes. */ - for (i = 0; i < MaxNumHelperProcess; i++) - { - dwRet = WaitForSingleObject(helper_process[i].hProcessReadyEvent, TIMEOUT); - if (dwRet != WAIT_OBJECT_0) - { - Fail("test5.Setup: WaitForSingleObject %s failed for helper process %d\n" - "LastError:(%u)\n", - szcHelperProcessReadyEvName, i, GetLastError()); - } - } - - /* Create the same number of helper threads as helper processes. */ - for (i = 0; i < MaxNumHelperProcess; i++) - { - /* Create the event to let helper thread tell us it is ready. */ - helper_thread[i].hThreadReadyEvent = CreateEvent(NULL, FALSE, FALSE, NULL); - if (!helper_thread[i].hThreadReadyEvent) - { - Fail("test5.Setup: CreateEvent of hThreadReadyEvent failed for thread %d\n" - "LastError:(%u)\n", i, GetLastError()); - } - - /* Create the event to tell helper thread to exit without waiting for helper process. */ - helper_thread[i].hThreadFinishEvent = CreateEvent(NULL, FALSE, FALSE, NULL); - if (!helper_thread[i].hThreadFinishEvent) - { - Fail("test5.Setup: CreateEvent of hThreadFinishEvent failed for thread %d\n" - "LastError:(%u)\n", i, GetLastError()); - } - - /* Create the helper thread. */ - helper_thread[i].hThread = CreateThread( NULL, - 0, - (LPTHREAD_START_ROUTINE)WaitForProcess, - (LPVOID)i, - 0, - &helper_thread[i].dwThreadId); - if (NULL == helper_thread[i].hThread) - { - Fail("test5.Setup: Unable to create the helper thread %d\n" - "LastError:(%u)\n", i, GetLastError()); - } - } - - /* Wait for ready signals from all helper threads. */ - for (i = 0; i < MaxNumHelperProcess; i++) - { - dwRet = WaitForSingleObject(helper_thread[i].hThreadReadyEvent, TIMEOUT); - if (dwRet != WAIT_OBJECT_0) - { - Fail("test5.Setup: WaitForSingleObject hThreadReadyEvent for thread %d\n" - "LastError:(%u)\n", i, GetLastError()); - } - } -} - -/* - * Cleanup the helper processes and helper threads. - */ -DWORD -Cleanup_WFMO_test5() -{ - DWORD dwExitCode; - DWORD dwRet; - int i; - - /* Wait for all helper process to finish and close their handles - and associated events. */ - for (i = 0; i < MaxNumHelperProcess; i++) - { - - /* wait for the child process to complete */ - dwRet = WaitForSingleObject ( helper_process[i].pi.hProcess, TIMEOUT ); - if (WAIT_OBJECT_0 != dwRet) - { - Fail("test5.Cleanup: WaitForSingleObject hThreadReadyEvent failed for thread %d\n" - "LastError:(%u)\n", i, GetLastError()); - } - - /* check the exit code from the process */ - if (!GetExitCodeProcess(helper_process[i].pi.hProcess, &dwExitCode)) - { - Trace( "test5.Cleanup: GetExitCodeProcess %d call failed LastError:(%u)\n", - i, GetLastError()); - dwExitCode = FAIL; - } - PEDANTIC(CloseHandle, (helper_process[i].pi.hThread)); - PEDANTIC(CloseHandle, (helper_process[i].pi.hProcess)); - PEDANTIC(CloseHandle, (helper_process[i].hProcessReadyEvent)); - PEDANTIC(CloseHandle, (helper_process[i].hProcessFinishEvent)); - } - - /* Close all helper threads' handles */ - for (i = 0; i < MaxNumHelperProcess; i++) - { - PEDANTIC(CloseHandle, (helper_thread[i].hThread)); - PEDANTIC(CloseHandle, (helper_thread[i].hThreadReadyEvent)); - PEDANTIC(CloseHandle, (helper_thread[i].hThreadFinishEvent)); - } - - /* Close all process start event. */ - PEDANTIC(CloseHandle, (hProcessStartEvent_WFMO_test5)); - - return dwExitCode; -} - -/* - * In this test case, the test thread will signal one helper - * process to exit at a time starting from the last helper - * process and then wait for the corresponding helper thread to exit. - * The ThreadIndexOfThreadFinishEvent specifies the index of the thread that - * should be waked up using hThreadFinishEvent instead of helper process. - */ -void -TestWakeupOneThread() -{ - DWORD dwRet; - int i; - - TestCase = WakeUpOneThread; - - if (((LONG)ThreadIndexOfThreadFinishEvent) < 0 || - ThreadIndexOfThreadFinishEvent >= MAX_HELPER_PROCESS) - Fail("test5.TestWaitOnOneThread: Invalid ThreadIndexOfThreadFinishEvent %d\n", ThreadIndexOfThreadFinishEvent); - - /* Since helper thread 0 waits on helper process 0, - thread 1 waits on process 0, and 1, - thread 2 waits on process 0, 1, and 2, and so on ..., - and the last helper thread will wait on all helper processes, - the helper thread can be waked up one at a time by - waking up the help process one at a time starting from the - last helper process. */ - for (i = MaxNumHelperProcess-1; i >= 0; i--) - { - /* make sure the helper thread has not exited yet. */ - dwRet = WaitForSingleObject(helper_thread[i].hThread, 0); - if (WAIT_TIMEOUT != dwRet) - { - Fail("test5.TestWaitOnOneThread: helper thread %d already exited %d\n", i); - } - - /* Decide how to wakeup the helper thread: - using event or using helper process. */ - if (i == ThreadIndexOfThreadFinishEvent) - { - if (!SetEvent(helper_thread[i].hThreadFinishEvent)) - { - Fail("test5.TestWaitOnOneThread: SetEvent hThreadFinishEvent failed for thread %d\n", - "LastError:(%u)\n", i, GetLastError()); - } - } - else - { - if (!SetEvent(helper_process[i].hProcessFinishEvent)) - { - Fail("test5.TestWaitOnOneThread: SetEvent %s%d failed for helper process %d\n", - "LastError:(%u)\n", - szcHelperProcessFinishEvName, helper_process[i].pi.dwProcessId, i, - GetLastError()); - } - } - - dwRet = WaitForSingleObject(helper_thread[i].hThread, TIMEOUT); - if (WAIT_OBJECT_0 != dwRet) - { - Fail("test5.TestWaitOnOneThread: WaitForSingleObject helper thread %d" - "LastError:(%u)\n", - i, GetLastError()); - } - } - - /* Finally, need to wake up the helper process which the test thread - skips waking up in the last loop. */ - if (!SetEvent(helper_process[ThreadIndexOfThreadFinishEvent].hProcessFinishEvent)) - { - Fail("test5.TestWaitOnOneThread: SetEvent %s%d failed\n", - "LastError:(%u)\n", - szcHelperProcessFinishEvName, helper_process[ThreadIndexOfThreadFinishEvent].pi.dwProcessId, - GetLastError()); - } -} - -/* - * In this test case, the test thread will signal the helper - * process 0 to exit. Since all helper threads wait on process 0, - * all helper threads will wake up and exit, and the test thread - * will wait for all of them to exit. - */ -void -TestWakeupAllThread() -{ - DWORD dwRet; - int i; - - TestCase = WakeUpAllThread; - - /* make sure none of the helper thread exits. */ - for (i = 0; i < MaxNumHelperProcess; i++) - { - dwRet = WaitForSingleObject(helper_thread[i].hThread, 0); - if (WAIT_TIMEOUT != dwRet) - { - Fail("test5.TestWaitOnAllThread: helper thread %d already exited %d\n", i); - } - } - - /* Signal helper process 0 to exit. */ - if (!SetEvent(helper_process[0].hProcessFinishEvent)) - { - Fail("test5.TestWaitOnAllThread: SetEvent %s%d failed\n", - "LastError:(%u)\n", - szcHelperProcessFinishEvName, helper_process[0].pi.dwProcessId, - GetLastError()); - } - - /* Wait for all helper threads to exit. */ - for (i = 0; i < MaxNumHelperProcess; i++) - { - - dwRet = WaitForSingleObject(helper_thread[i].hThread, TIMEOUT); - if (WAIT_OBJECT_0 != dwRet) - { - Fail("test5.TestWaitOnAllThread: WaitForSingleObject failed for helper thread %d\n" - "LastError:(%u)\n", - i, GetLastError()); - } - } - - /* Signal the rest of helper processes to exit. */ - for (i = 1; i < MaxNumHelperProcess; i++) - { - if (!SetEvent(helper_process[i].hProcessFinishEvent)) - { - Fail("test5.TestWaitOnAllThread: SetEvent %s%d failed\n", - "LastError:(%u)\n", - szcHelperProcessFinishEvName, helper_process[i].pi.dwProcessId, - GetLastError()); - } - } -} - -PALTEST(threading_WaitForMultipleObjectsEx_test5_paltest_waitformultipleobjectsex_test5, "threading/WaitForMultipleObjectsEx/test5/paltest_waitformultipleobjectsex_test5") -{ - DWORD dwExitCode; - - if(0 != (PAL_Initialize(argc, argv))) - { - return FAIL; - } - - switch (argc) - { - case 1: - MaxNumHelperProcess = MAX_HELPER_PROCESS; - break; - case 2: - MaxNumHelperProcess = atoi(argv[1]); - break; - default: - Fail("Invalid number of arguments\n"); - } - - if (MaxNumHelperProcess < 1 || - MaxNumHelperProcess > MAX_HELPER_PROCESS) - Fail("test5.main: Invalid MaxNumHelperProcess %d\n", MaxNumHelperProcess); - - Setup(); - ThreadIndexOfThreadFinishEvent = 3; - TestWakeupOneThread(); - dwExitCode = Cleanup_WFMO_test5(); - - if (PASS == dwExitCode) - { - Setup(); - TestWakeupAllThread(); - dwExitCode = Cleanup_WFMO_test5(); - } - - PAL_TerminateEx(dwExitCode); - return dwExitCode; -} diff --git a/src/coreclr/pal/tests/palsuite/threading/WaitForSingleObject/WFSOProcessTest/ChildProcess.cpp b/src/coreclr/pal/tests/palsuite/threading/WaitForSingleObject/WFSOProcessTest/ChildProcess.cpp deleted file mode 100644 index b8c15d07a4aa4d..00000000000000 --- a/src/coreclr/pal/tests/palsuite/threading/WaitForSingleObject/WFSOProcessTest/ChildProcess.cpp +++ /dev/null @@ -1,49 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================ -** -** Source: ChildProcess.c -** -** Purpose: Dummy Process which does some work on which the Main Test case waits -** -** - -** -**=========================================================*/ - - - -#include - -PALTEST(threading_WaitForSingleObject_WFSOProcessTest_paltest_waitforsingleobject_wfsoprocesstest_child, "threading/WaitForSingleObject/WFSOProcessTest/paltest_waitforsingleobject_wfsoprocesstest_child") -{ - -//Declare local variables -int i =0; - - - -//Initialize PAL -if(0 != (PAL_Initialize(argc, argv))) - { - return ( FAIL ); - } - -//Do some work -for (i=0; i<100000; i++); - -Trace("Counter Value was incremented to %d \n ",i); - -PAL_Terminate(); -return ( PASS ); - -} - - - - - - - - diff --git a/src/coreclr/pal/tests/palsuite/threading/WaitForSingleObject/WFSOProcessTest/WFSOProcessTest.cpp b/src/coreclr/pal/tests/palsuite/threading/WaitForSingleObject/WFSOProcessTest/WFSOProcessTest.cpp deleted file mode 100644 index f850973a4cdea2..00000000000000 --- a/src/coreclr/pal/tests/palsuite/threading/WaitForSingleObject/WFSOProcessTest/WFSOProcessTest.cpp +++ /dev/null @@ -1,115 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -/*============================================================ -** -** Source: WFSOProcessTest.c -** -** Purpose: Test for WaitForSingleObjectTest. -** Create One Process and do some work -** Use WFSO For the Process to finish -** -** Test Passes if the above operations are successful -** -** -** -**=========================================================*/ - - - -#include - -PALTEST(threading_WaitForSingleObject_WFSOProcessTest_paltest_waitforsingleobject_wfsoprocesstest, "threading/WaitForSingleObject/WFSOProcessTest/paltest_waitforsingleobject_wfsoprocesstest") -{ - -//Declare local variables -STARTUPINFO si; -PROCESS_INFORMATION pi; - -DWORD dwWaitResult=0; - -//Initialize PAL -if(0 != (PAL_Initialize(argc, argv))) - { - return ( FAIL ); - } - - -ZeroMemory( &si, sizeof(si) ); -si.cb = sizeof(si); -ZeroMemory( &pi, sizeof(pi) ); - -LPWSTR nameW = convert("childprocess"); -// Start the child process. -if( !CreateProcess( NULL, // No module name (use command line). - nameW, // Command line. - NULL, // Process handle not inheritable. - NULL, // Thread handle not inheritable. - FALSE, // Set handle inheritance to FALSE. - 0, // No creation flags. - NULL, // Use parent's environment block. - NULL, // Use parent's starting directory. - &si, // Pointer to STARTUPINFO structure. - &pi ) // Pointer to PROCESS_INFORMATION structure. -) - -{ -DWORD dwError = GetLastError(); -free(nameW); -Fail ( "Create Process Failed. Failing test.\n" - "GetLastError returned %d\n", GetLastError()); -} - -free(nameW); - -// Wait until child process exits. - dwWaitResult = WaitForSingleObject( pi.hProcess, INFINITE ); -switch (dwWaitResult) - { - // The Process wait was successful - case WAIT_OBJECT_0: - { - - Trace("Wait for Process was successful\n"); - break; - } - - // Time-out. - case WAIT_TIMEOUT: - { - Fail ( "Time -out. Failing test.\n" - "GetLastError returned %d\n", GetLastError()); - return FALSE; - } - - //Error condition - case WAIT_FAILED: - { - Fail ( "Wait for Process Failed. Failing test.\n" - "GetLastError returned %d\n", GetLastError()); - return FALSE; - } - -} - - - -// Close process handle -if (0==CloseHandle(pi.hProcess)) - { - Trace("Could not close process handle\n"); - Fail ( "GetLastError returned %d\n", GetLastError()); - } - - -PAL_Terminate(); -return ( PASS ); - -} - - - - - - - diff --git a/src/coreclr/utilcode/winfix.cpp b/src/coreclr/utilcode/winfix.cpp index 9738475350c94b..2c5290b750acd8 100644 --- a/src/coreclr/utilcode/winfix.cpp +++ b/src/coreclr/utilcode/winfix.cpp @@ -1,15 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -//***************************************************************************** -// WinWrap.cpp -// -//***************************************************************************** #include "stdafx.h" // Precompiled header key. #include "winwrap.h" // Header for macros and functions. #include "utilcode.h" #include "holder.h" +#ifndef HOST_UNIX + // The only purpose of this function is to make a local copy of lpCommandLine. // Because windows implementation of CreateProcessW can actually change lpCommandLine, // but we'd like to keep it const. @@ -60,9 +58,6 @@ WszCreateProcess( return fResult; } -#ifndef HOST_UNIX - - #include "psapi.h" #include "winnls.h" From 5761661dc931d1f14531b8c91bf88344d2a1dc09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Strehovsk=C3=BD?= Date: Tue, 5 May 2026 10:10:35 +0900 Subject: [PATCH 100/115] Enable EventSource in System.Threading.Tasks.Tests (#127744) Might fix native AOT outerloops: ``` [FAIL] System.Threading.Tasks.Tests.AsyncProfilerTests.RuntimeAsync_CallstackSimulation_HandledException Expected at least one ResumeAsyncCallstack event at System.Threading.Tasks.Tests.AsyncProfilerTests.AssertCallstackSimulationReachesZero(ConcurrentQueue`1 events) in /_/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs:line 653 at System.Reflection.DynamicInvokeInfo.Invoke(Object, IntPtr, Object[], BinderBundle, Boolean) in /_/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/DynamicInvokeInfo.cs:line 230 [FAIL] System.Threading.Tasks.Tests.AsyncProfilerTests.RuntimeAsync_WrapperIndexMatchesCallstack Assert.All() Failure: 3 out of 3 items in the collection did not pass. [0]: Item: Tuple ("WrapperTestC", -1) Error: WrapperTestC did not find Continuation_Wrapper_N on stack (slot=-1) [1]: Item: Tuple ("WrapperTestB", -1) Error: WrapperTestB did not find Continuation_Wrapper_N on stack (slot=-1) [2]: Item: Tuple ("WrapperTestA", -1) Error: WrapperTestA did not find Continuation_Wrapper_N on stack (slot=-1) at System.Threading.Tasks.Tests.AsyncProfilerTests.RuntimeAsync_WrapperIndexMatchesCallstack() in /_/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs:line 1281 at System.Reflection.DynamicInvokeInfo.Invoke(Object, IntPtr, Object[], BinderBundle, Boolean) in /_/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/DynamicInvokeInfo.cs:line 230 [FAIL] System.Threading.Tasks.Tests.AsyncProfilerTests.RuntimeAsync_UnhandledExceptionUnwind Assert.Contains() Failure: Item not found in collection Collection: [] Not found: ResumeAsyncContext at System.Threading.Tasks.Tests.AsyncProfilerTests.RuntimeAsync_UnhandledExceptionUnwind() in /_/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs:line 1217 at System.Reflection.DynamicInvokeInfo.Invoke(Object, IntPtr, Object[], BinderBundle, Boolean) in /_/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/DynamicInvokeInfo.cs:line 230 [FAIL] System.Threading.Tasks.Tests.AsyncProfilerTests.RuntimeAsync_PeriodicTimerFlush_PreservesOwnerThreadId Expected periodic timer to flush core lifecycle events within timeout at System.Threading.Tasks.Tests.AsyncProfilerTests.<>c__DisplayClass102_0.b__1() in /_/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs:line 1439 at System.Diagnostics.Tracing.TestEventListener.RunWithCallback(Action`1 handler, Action body) in /_/src/libraries/Common/tests/System/Diagnostics/Tracing/TestEventListener.cs:line 116 at System.Threading.Tasks.Tests.AsyncProfilerTests.RuntimeAsync_PeriodicTimerFlush_PreservesOwnerThreadId() in /_/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs:line 1396 at System.Reflection.DynamicInvokeInfo.Invoke(Object, IntPtr, Object[], BinderBundle, Boolean) in /_/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/DynamicInvokeInfo.cs:line 230 [FAIL] System.Threading.Tasks.Tests.AsyncProfilerTests.RuntimeAsync_ResetAsyncThreadContextEvent Assert.Contains() Failure: Item not found in collection Collection: [] Not found: ResetAsyncThreadContext at System.Reflection.DynamicInvokeInfo.Invoke(Object, IntPtr, Object[], BinderBundle, Boolean) in /_/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/DynamicInvokeInfo.cs:line 230 Unhandled exception. Xunit.Sdk.TrueException: Expected first flush of core lifecycle events within timeout at Xunit.Assert.True(Nullable`1, String) in /_/src/arcade/src/Microsoft.DotNet.XUnitAssert/src/BooleanAsserts.cs:line 135 at System.Threading.Tasks.Tests.AsyncProfilerTests.<>c__DisplayClass102_0.b__2() in /_/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs:line 1425 at System.Threading.Thread.StartThread(IntPtr) in /_/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs:line 443 at System.Threading.Thread.ThreadEntryPoint(IntPtr) in /_/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Unix.cs:line 92 DOTNET_DbgEnableMiniDump is set and the createdump binary does not exist: ./createdump [FAIL] System.Threading.Tasks.Tests.AsyncProfilerTests.RuntimeAsync_CallstackNativeIPDeltaRoundtrip Assert.NotEmpty() Failure: Collection was empty at System.Threading.Tasks.Tests.AsyncProfilerTests.RuntimeAsync_CallstackNativeIPDeltaRoundtrip() in /_/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs:line 1715 at System.Reflection.DynamicInvokeInfo.Invoke(Object, IntPtr, Object[], BinderBundle, Boolean) in /_/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/DynamicInvokeInfo.cs:line 230 [FAIL] System.Threading.Tasks.Tests.AsyncProfilerTests.RuntimeAsync_CreateAndFirstResumeCallstacksMatch Assert.NotEmpty() Failure: Collection was empty at System.Threading.Tasks.Tests.AsyncProfilerTests.RuntimeAsync_CreateAndFirstResumeCallstacksMatch() in /_/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs:line 1071 at System.Reflection.DynamicInvokeInfo.Invoke(Object, IntPtr, Object[], BinderBundle, Boolean) in /_/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/DynamicInvokeInfo.cs:line 230 [FAIL] System.Threading.Tasks.Tests.AsyncProfilerTests.RuntimeAsync_CallstackSimulation_UnhandledException Expected at least one ResumeAsyncCallstack event at System.Threading.Tasks.Tests.AsyncProfilerTests.AssertCallstackSimulationReachesZero(ConcurrentQueue`1 events) in /_/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs:line 653 at System.Reflection.DynamicInvokeInfo.Invoke(Object, IntPtr, Object[], BinderBundle, Boolean) in /_/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/DynamicInvokeInfo.cs:line 230 [FAIL] System.Threading.Tasks.Tests.AsyncProfilerTests.RuntimeAsync_ContextEventIdLifecycle Expected at least one CreateAsyncContext with id at System.Threading.Tasks.Tests.AsyncProfilerTests.RuntimeAsync_ContextEventIdLifecycle() in /_/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs:line 781 at System.Reflection.DynamicInvokeInfo.Invoke(Object, IntPtr, Object[], BinderBundle, Boolean) in /_/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/DynamicInvokeInfo.cs:line 230 ./RunTests.sh: line 173: 18 Aborted (core dumped) ./System.Threading.Tasks.Tests -notrait category=IgnoreForCI -notrait category=OuterLoop -notrait category=failing -xml testResults.xml $RSP_FILE /root/helix/work/workitem/e ----- end Mon May 4 11:48:57 AM UTC 2026 ----- exit code 134 ---------------------------------------------------------- exit code 134 means SIGABRT Abort. Managed or native assert, or runtime check such as heap corruption, caused call to abort(). Core dumped. ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lateralusX <11529140+lateralusX@users.noreply.github.com> --- .../System.Runtime.CompilerServices/AsyncProfilerTests.cs | 3 ++- .../System.Threading.Tasks.Tests.csproj | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs index b493de4e757978..2917970417434e 100644 --- a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs +++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs @@ -39,8 +39,9 @@ public class AsyncProfilerTests // single-threaded WASM this throws PlatformNotSupportedException from // RuntimeFeature.ThrowIfMultithreadingIsNotSupported(), so gate the tests on both // runtime async support and threading support. + // Some tests rely on GetMethodFromNativeIP which is not supported on NativeAOT. public static bool IsRuntimeAsyncAndThreadingSupported => - PlatformDetection.IsRuntimeAsyncSupported && PlatformDetection.IsMultithreadingSupported; + PlatformDetection.IsRuntimeAsyncSupported && PlatformDetection.IsMultithreadingSupported && PlatformDetection.IsNotNativeAot; private const string AsyncProfilerEventSourceName = "System.Runtime.CompilerServices.AsyncProfilerEventSource"; private const int AsyncEventsId = 1; diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csproj b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csproj index a8c936a9e0093c..84894a3857d957 100644 --- a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csproj @@ -7,6 +7,7 @@ false false + true <_WasmPThreadPoolUnusedSize>10 From 969ffc7391712e791d3107176fc77e23563856cd Mon Sep 17 00:00:00 2001 From: SingleAccretion <62474226+SingleAccretion@users.noreply.github.com> Date: Tue, 5 May 2026 07:52:44 +0300 Subject: [PATCH 101/115] [RyuJit] Decouple `igNum` from strict layout order (#125554) The ultimate goal here is to remove the single-IG prolog constraint. Currently, the `igNum` order matches the linked list order precisely (and thus the final layout order) precisely. It means we cannot generate any IGs "out of order". This change removes this restriction for a subset of IGs, those representing epilogs and funclet prologs, by hiding `igNum` behind and abstraction of "before [in layout order] / after [in layout order]" APIs. These APIs rely in the linked list to ascertain ordering between out-of-order groups. Currently this is only exercised under a stress mode which splits all IGs into single-instruction groups. To this end, validation has been performed **with this stress mode uncoditionally turned on for all methods** [and all eligible IGs]. Without stress, this is a zero-diff change since all our epilogs and funclet prologs fit into a single IG currently. --------- Co-authored-by: Andy Ayers --- src/coreclr/jit/emit.cpp | 356 +++++++++++++++++++--------- src/coreclr/jit/emit.h | 34 ++- src/coreclr/jit/emitarm64.cpp | 2 +- src/coreclr/jit/emitloongarch64.cpp | 11 +- src/coreclr/jit/emitxarch.cpp | 7 +- src/native/watchdog/watchdog.cpp | 9 +- 6 files changed, 277 insertions(+), 142 deletions(-) diff --git a/src/coreclr/jit/emit.cpp b/src/coreclr/jit/emit.cpp index bb1794404c67b7..ea343f496cce66 100644 --- a/src/coreclr/jit/emit.cpp +++ b/src/coreclr/jit/emit.cpp @@ -71,20 +71,26 @@ int emitLocation::GetInsOffset() const return emitGetInsOfsFromCodePos(codePos); } -// Get the instruction offset in the current instruction group, which must be a funclet prolog group. +// Get the instruction offset in the current instruction region, which must be a funclet prolog. // This is used to find an instruction offset used in unwind data. -// TODO-AMD64-Bug?: We only support a single main function prolog group, but allow for multiple funclet prolog -// groups (not that we actually use that flexibility, since the funclet prolog will be small). How to -// handle that? UNATIVE_OFFSET emitLocation::GetFuncletPrologOffset(emitter* emit) const { assert(ig->igFuncIdx != 0); assert((ig->igFlags & IGF_FUNCLET_PROLOG) != 0); - assert(ig == emit->emitCurIG); + assert((ig->igFlags & IGF_OUT_OF_ORDER_HEAD) != 0); + assert(GetInsOffset() == 0); - return emit->emitCurIGsize; -} + unsigned offset = 0; + insGroup* lastIG = ig; + while (lastIG != emit->emitCurIG) + { + offset += lastIG->igSize; + lastIG = lastIG->igNext; + } + assert((lastIG->igFlags & IGF_FUNCLET_PROLOG) != 0); + return offset + emit->emitCurIGsize; +} //------------------------------------------------------------------------ // IsPreviousInsNum: Returns true if the emitter is on the next instruction // of the same group as this emitLocation. @@ -116,10 +122,142 @@ void emitLocation::Print(LONG compMethodID) const { unsigned insNum = emitGetInsNumFromCodePos(codePos); unsigned insOfs = emitGetInsOfsFromCodePos(codePos); - printf("(G_M%03u_IG%02u,ins#%d,ofs#%d)", compMethodID, ig->igNum, insNum, insOfs); + printf("(G_M%03u_IG%02u,ins#%d,ofs#%d)", compMethodID, ig->GetDisplayId(), insNum, insOfs); } #endif // DEBUG +//------------------------------------------------------------------------ +// InitializeNum: Initialize this IG's order/display number. +// +// Each subsequently allocated group should be assigned a monotonically +// increasing number. +// +// Arguments: +// num - The number +// +void insGroup::InitializeNum(unsigned num) +{ + igNum = num; +} + +//------------------------------------------------------------------------ +// GetDisplayId: Get the ID of this group used for display. +// +// Return Value: +// A unique ID for this group. +// +unsigned insGroup::GetDisplayId() const +{ + return igNum; +} + +//------------------------------------------------------------------------ +// IsBefore: Is this group before 'ig' in layout (IG list) order? +// +// Most groups are generated in-order, such that their 'numbers' reflect +// both the layout and IG linked list order. However, for prologs/epilogs, +// we can have extend groups that are generated after the IR-driven code, +// and this function accounts for that. +// +// Arguments: +// ig - The group to compare to +// +// Return Value: +// Whether 'this' is before 'ig'. +// +bool insGroup::IsBefore(const insGroup* ig) const +{ + assert(ig != nullptr); + + // All IGs are generated in order, except the extend groups that may hangs off prologs and epilogs. + // In turn, those groups themselves are generated in order within their respective regions. + unsigned positionOfThis; + if ((igFlags & IGF_OUT_OF_ORDER_MASK) != 0) + { + const insGroup* nextIG = igNext; + while (true) + { + if (nextIG == nullptr) + { + return false; + } + if (((nextIG->igFlags & IGF_OUT_OF_ORDER_MASK) == 0) || ((nextIG->igFlags & IGF_OUT_OF_ORDER_HEAD) != 0)) + { + positionOfThis = nextIG->igNum - 1; // Position equal to the in-order 'head' of the region. + break; + } + if (nextIG == ig) + { + return true; + } + + nextIG = nextIG->igNext; + } + } + else + { + positionOfThis = igNum; + } + + unsigned positionOfIG; + if ((ig->igFlags & IGF_OUT_OF_ORDER_MASK) != 0) + { + const insGroup* nextIG = ig->igNext; + while (true) + { + if (nextIG == nullptr) + { + return true; + } + if (((nextIG->igFlags & IGF_OUT_OF_ORDER_MASK) == 0) || ((nextIG->igFlags & IGF_OUT_OF_ORDER_HEAD) != 0)) + { + positionOfIG = nextIG->igNum - 1; + break; + } + if (nextIG == this) + { + return false; + } + + nextIG = nextIG->igNext; + } + } + else + { + positionOfIG = ig->igNum; + } + + return positionOfThis < positionOfIG; +} + +//------------------------------------------------------------------------ +// IsBefore: Is this group before 'ig' in layout order, or equal to it? +// +// Arguments: +// ig - The group to compare to +// +// Return Value: +// Whether 'this' is before 'ig' or is equal to it. +// +bool insGroup::IsBeforeOrEqual(const insGroup* ig) const +{ + return !IsAfter(ig); +} + +//------------------------------------------------------------------------ +// IsBefore: Is this group after 'ig' in layout order? +// +// Arguments: +// ig - The group to compare to +// +// Return Value: +// Whether 'this' is after 'ig'. +// +bool insGroup::IsAfter(const insGroup* ig) const +{ + return ig->IsBefore(this); +} + /***************************************************************************** * * Return the name of an instruction format. @@ -788,9 +926,6 @@ void emitter::emitGenIG(insGroup* ig) { IMPL_LIMITATION("Too many arguments pushed on stack"); } - - // printf("Start IG #%02u [stk=%02u]\n", ig->igNum, emitCurStackLvl); - #endif if (emitNoGCIG) @@ -1223,9 +1358,9 @@ void emitter::emitBegFN(bool hasFramePtr emitIGbuffSize = 0; #if FEATURE_LOOP_ALIGN - emitLastAlignedIgNum = 0; - emitLastLoopStart = 0; - emitLastLoopEnd = 0; + emitLastAlignedIG = nullptr; + emitLastLoopStart = nullptr; + emitLastLoopEnd = nullptr; #endif /* Record stack frame info (the temp size is just an estimate) */ @@ -1444,8 +1579,6 @@ void emitter::perfScoreUnhandledInstruction(instrDesc* id, insExecutionCharacter pResult->insLatency = PERFSCORE_LATENCY_1C; } -#endif // defined(DEBUG) || defined(LATE_DISASM) - //---------------------------------------------------------------------------------------- // getCurrentBlockWeight: Return the block weight for the currently active block // @@ -1467,12 +1600,19 @@ weight_t emitter::getCurrentBlockWeight() { return m_compiler->compCurBB->getBBWeight(m_compiler); } - else // we have a null compCurBB + else if (emitCurIG != nullptr) + { + // Prolog or epilog case, use the weight of the head group or its extensions. + assert((emitCurIG->igFlags & IGF_OUT_OF_ORDER_MASK) != 0); + return emitCurIG->igWeight; + } + else { - // prolog or epilog case, so just use the standard weight + // This is the prolog case (when we're just initializing the IG list). return BB_UNITY_WEIGHT; } } +#endif // defined(DEBUG) || defined(LATE_DISASM) #if defined(TARGET_LOONGARCH64) void emitter::dispIns(instrDesc* id) @@ -1585,16 +1725,8 @@ void* emitter::emitAllocAnyInstr(size_t sz, emitAttr opsz) { #ifdef DEBUG // Under STRESS_EMITTER, put every instruction in its own instruction group. - // We can't do this for a prolog, epilog, funclet prolog, or funclet epilog, - // because those are generated out of order. We currently have a limitation - // where the jump shortening pass uses the instruction group number to determine - // if something is earlier or later in the code stream. This implies that - // these groups cannot be more than a single instruction group. Note that - // the prolog/epilog placeholder groups ARE generated in order, and are - // re-used. But generating additional groups would not work. if (m_compiler->compStressCompile(Compiler::STRESS_EMITTER, 1) && emitCurIGinsCnt && !emitIGisInProlog(emitCurIG) && - !emitIGisInEpilog(emitCurIG) && !emitCurIG->endsWithAlignInstr() && !emitIGisInFuncletProlog(emitCurIG) && - !emitIGisInFuncletEpilog(emitCurIG)) + !emitIGisInFuncletProlog(emitCurIG) && !emitCurIG->endsWithAlignInstr()) { emitNxtIG(true); } @@ -1775,7 +1907,7 @@ void emitter::emitCheckIGList() if (currIG->igOffs != currentOffset) { - printf("IG%02u has offset %08X, expected %08X\n", currIG->igNum, currIG->igOffs, currentOffset); + printf("IG%02u has offset %08X, expected %08X\n", currIG->GetDisplayId(), currIG->igOffs, currentOffset); assert(!"bad block offset"); } @@ -2090,6 +2222,7 @@ void emitter::emitCreatePlaceholderIG(insGroupPlaceholderType igType, { igPh->igFlags |= IGF_FUNCLET_EPILOG; } + igPh->igFlags |= IGF_OUT_OF_ORDER_HEAD; /* Link it into the placeholder list */ @@ -2331,7 +2464,7 @@ void emitter::emitBegPrologEpilog(insGroup* igPh) m_compiler->funSetCurrentFunc(ig->igFuncIdx); /* Set the new IG as the place to generate code */ - + emitCurCodeOffset = ig->igOffs; emitGenIG(ig); #if EMIT_TRACK_STACK_DEPTH @@ -2851,7 +2984,7 @@ void* emitter::emitAddInlineLabel() // void emitter::emitPrintLabel(const insGroup* ig) const { - printf("G_M%03u_IG%02u", m_compiler->compMethodID, ig->igNum); + printf("G_M%03u_IG%02u", m_compiler->compMethodID, ig->GetDisplayId()); } //----------------------------------------------------------------------------- @@ -2869,7 +3002,7 @@ const char* emitter::emitLabelString(const insGroup* ig) const static char buf[4][TEMP_BUFFER_LEN]; const char* retbuf; - sprintf_s(buf[curBuf], TEMP_BUFFER_LEN, "G_M%03u_IG%02u", m_compiler->compMethodID, ig->igNum); + sprintf_s(buf[curBuf], TEMP_BUFFER_LEN, "G_M%03u_IG%02u", m_compiler->compMethodID, ig->GetDisplayId()); retbuf = buf[curBuf]; curBuf = (curBuf + 1) % 4; return retbuf; @@ -2956,7 +3089,7 @@ void emitter::emitSplit(emitLocation* startLoc, { #ifdef DEBUG if (EMITVERBOSE) - printf("emitSplit: can't split at IG%02u; we don't have a candidate to report\n", ig->igNum); + printf("emitSplit: can't split at IG%02u; we don't have a candidate to report\n", ig->GetDisplayId()); #endif return; } @@ -2967,7 +3100,7 @@ void emitter::emitSplit(emitLocation* startLoc, { #ifdef DEBUG if (EMITVERBOSE) - printf("emitSplit: can't split at IG%02u; we already reported it\n", igLastCandidate->igNum); + printf("emitSplit: can't split at IG%02u; we already reported it\n", igLastCandidate->GetDisplayId()); #endif return; } @@ -2980,7 +3113,7 @@ void emitter::emitSplit(emitLocation* startLoc, { #ifdef DEBUG if (EMITVERBOSE) - printf("emitSplit: can't split at IG%02u; zero-sized candidate\n", igLastCandidate->igNum); + printf("emitSplit: can't split at IG%02u; zero-sized candidate\n", igLastCandidate->GetDisplayId()); #endif return; } @@ -2991,7 +3124,7 @@ void emitter::emitSplit(emitLocation* startLoc, if (EMITVERBOSE) { printf("emitSplit: split at IG%02u is size %x, %s than requested maximum size of %x\n", - igLastCandidate->igNum, candidateSize, (candidateSize >= maxSplitSize) ? "larger" : "less", + igLastCandidate->GetDisplayId(), candidateSize, (candidateSize >= maxSplitSize) ? "larger" : "less", maxSplitSize); } #endif @@ -3040,7 +3173,7 @@ void emitter::emitSplit(emitLocation* startLoc, if ((igLastCandidate != nullptr) && (curSize == candidateSize)) { JITDUMP("emitSplit: can't split at last candidate IG%02u because it would create a zero-sized fragment\n", - igLastCandidate->igNum); + igLastCandidate->GetDisplayId()); } else { @@ -4011,7 +4144,7 @@ void emitter::emitDispIG(insGroup* ig, bool displayFunc, bool displayInstruction printf("%s placeholder, next placeholder=", pszType); if (igPh->igPhData->igPhNext) { - printf("IG%02u ", igPh->igPhData->igPhNext->igNum); + printf("IG%02u ", igPh->igPhData->igPhNext->GetDisplayId()); } else { @@ -4113,7 +4246,7 @@ void emitter::emitDispIG(insGroup* ig, bool displayFunc, bool displayInstruction #if FEATURE_LOOP_ALIGN if (ig->igLoopBackEdge != nullptr) { - printf("%sloop=IG%02u", separator, ig->igLoopBackEdge->igNum); + printf("%sloop=IG%02u", separator, ig->igLoopBackEdge->GetDisplayId()); separator = ", "; } #endif // FEATURE_LOOP_ALIGN @@ -4234,7 +4367,7 @@ void emitter::emitDispJumpList() unsigned int jmpCount = 0; for (instrDescJmp* jmp = emitJumpList; jmp != nullptr; jmp = jmp->idjNext) { - printf("IG%02u IN%04x %3s[%u]", jmp->idjIG->igNum, jmp->idDebugOnlyInfo()->idNum, + printf("IG%02u IN%04x %3s[%u]", jmp->idjIG->GetDisplayId(), jmp->idDebugOnlyInfo()->idNum, codeGen->genInsDisplayName(jmp), jmp->idCodeSize()); if (!jmp->idIsBound()) @@ -4255,7 +4388,7 @@ void emitter::emitDispJumpList() } else { - printf(" -> IG%02u", targetGroup->igNum); + printf(" -> IG%02u", targetGroup->GetDisplayId()); } } @@ -4377,7 +4510,7 @@ size_t emitter::emitIssue1Instr(insGroup* ig, instrDesc* id, BYTE** dp) #if FEATURE_LOOP_ALIGN // Should never over-estimate align instruction or any instruction before the last align instruction of a method - assert(id->idIns() != INS_align && emitCurIG->igNum > emitLastAlignedIgNum); + assert(id->idIns() != INS_align && ((emitLastAlignedIG == nullptr) || emitCurIG->IsAfter(emitLastAlignedIG))); #endif #if DEBUG_EMIT @@ -4593,8 +4726,8 @@ void emitter::emitRemoveJumpToNextInst() instrDescJmp* jmp = emitJumpList; instrDescJmp* previousJmp = nullptr; #if DEBUG - UNATIVE_OFFSET previousJumpIgNum = (UNATIVE_OFFSET)-1; - unsigned int previousJumpInsNum = -1; + insGroup* previousJumpIG = nullptr; + unsigned int previousJumpInsNum = -1; #endif // DEBUG while (jmp) @@ -4607,9 +4740,9 @@ void emitter::emitRemoveJumpToNextInst() #if DEBUG assert(jmp->idInsFmt() == IF_LABEL); assert(emitIsUncondJump(jmp)); - assert((jmpGroup->igNum > previousJumpIgNum) || (previousJumpIgNum == (UNATIVE_OFFSET)-1) || - ((jmpGroup->igNum == previousJumpIgNum) && (jmp->idDebugOnlyInfo()->idNum > previousJumpInsNum))); - previousJumpIgNum = jmpGroup->igNum; + assert((previousJumpIG == nullptr) || jmpGroup->IsAfter(previousJumpIG) || + ((jmpGroup == previousJumpIG) && (jmp->idDebugOnlyInfo()->idNum > previousJumpInsNum))); + previousJumpIG = jmpGroup; previousJumpInsNum = jmp->idDebugOnlyInfo()->idNum; #endif // DEBUG @@ -4651,7 +4784,7 @@ void emitter::emitRemoveJumpToNextInst() JITDUMP("IG%02u IN%04x is the last instruction in the group and jumps to the next instruction group " "IG%02u %s, removing.\n", - jmpGroup->igNum, jmp->idDebugOnlyInfo()->idNum, targetGroup->igNum, + jmpGroup->GetDisplayId(), jmp->idDebugOnlyInfo()->idNum, targetGroup->GetDisplayId(), emitLabelString(targetGroup)); #endif // DEBUG @@ -4708,24 +4841,24 @@ void emitter::emitRemoveJumpToNextInst() #if DEBUG if (targetGroup == nullptr) { - JITDUMP("IG%02u IN%04x jump target is not set!, keeping.\n", jmpGroup->igNum, + JITDUMP("IG%02u IN%04x jump target is not set!, keeping.\n", jmpGroup->GetDisplayId(), jmp->idDebugOnlyInfo()->idNum); } else if (jmpGroup->igNext != targetGroup) { - JITDUMP("IG%02u IN%04x does not jump to the next instruction group, keeping.\n", jmpGroup->igNum, - jmp->idDebugOnlyInfo()->idNum); + JITDUMP("IG%02u IN%04x does not jump to the next instruction group, keeping.\n", + jmpGroup->GetDisplayId(), jmp->idDebugOnlyInfo()->idNum); } else if ((jmpGroup->igFlags & IGF_HAS_REMOVABLE_JMP) == 0) { JITDUMP("IG%02u IN%04x containing instruction group is not marked with IGF_HAS_REMOVABLE_JMP, " "keeping.\n", - jmpGroup->igNum, jmp->idDebugOnlyInfo()->idNum); + jmpGroup->GetDisplayId(), jmp->idDebugOnlyInfo()->idNum); } else if (jmpGroup->endsWithAlignInstr()) { - JITDUMP("IG%02u IN%04x containing instruction group has alignment, keeping.\n", jmpGroup->igNum, - jmp->idDebugOnlyInfo()->idNum); + JITDUMP("IG%02u IN%04x containing instruction group has alignment, keeping.\n", + jmpGroup->GetDisplayId(), jmp->idDebugOnlyInfo()->idNum); } #endif // DEBUG } @@ -4740,9 +4873,9 @@ void emitter::emitRemoveJumpToNextInst() { insGroup* adjOffIG = jmpGroup->igNext; insGroup* adjOffUptoIG = nextJmp != nullptr ? nextJmp->idjIG : emitIGlast; - while ((adjOffIG != nullptr) && (adjOffIG->igNum <= adjOffUptoIG->igNum)) + while ((adjOffIG != nullptr) && adjOffIG->IsBeforeOrEqual(adjOffUptoIG)) { - JITDUMP("Adjusted offset of IG%02u from %04X to %04X\n", adjOffIG->igNum, adjOffIG->igOffs, + JITDUMP("Adjusted offset of IG%02u from %04X to %04X\n", adjOffIG->GetDisplayId(), adjOffIG->igOffs, (adjOffIG->igOffs - totalRemovedSize)); adjOffIG->igOffs -= totalRemovedSize; adjOffIG = adjOffIG->igNext; @@ -5013,8 +5146,7 @@ void emitter::emitJumpDistBind() assert(lastLJ == nullptr || lastIG != jmp->idjIG || lastLJ->idjOffs < jmp->idjOffs); lastLJ = (lastIG == jmp->idjIG) ? jmp : nullptr; - assert(lastIG == nullptr || lastIG->igNum <= jmp->idjIG->igNum || jmp->idjIG == prologIG || - emitNxtIGnum > unsigned(0xFFFF)); // igNum might overflow + assert(lastIG == nullptr || lastIG->IsBeforeOrEqual(jmp->idjIG) || jmp->idjIG == prologIG); lastIG = jmp->idjIG; #endif // DEBUG @@ -5043,8 +5175,8 @@ void emitter::emitJumpDistBind() #ifdef DEBUG if (EMITVERBOSE) { - printf("Adjusted offset of " FMT_BB " from %04X to %04X\n", lstIG->igNum, lstIG->igOffs, - lstIG->igOffs - adjIG); + printf("Adjusted offset of " FMT_BB " from %04X to %04X\n", lstIG->GetDisplayId(), + lstIG->igOffs, lstIG->igOffs - adjIG); } #endif // DEBUG lstIG->igOffs -= adjIG; @@ -5219,7 +5351,7 @@ void emitter::emitJumpDistBind() srcEncodingOffs = srcInstrOffs + ssz; // Encoding offset of relative offset for small branch #endif - if (jmpIG->igNum < tgtIG->igNum) + if (jmpIG->IsBefore(tgtIG)) { /* Forward jump */ @@ -5337,7 +5469,7 @@ void emitter::emitJumpDistBind() if (emitIsCmpJump(jmp)) { - if (jmpIG->igNum < tgtIG->igNum) + if (jmpIG->IsBefore(tgtIG)) { /* Forward jump */ @@ -5528,7 +5660,7 @@ void emitter::emitJumpDistBind() #ifdef DEBUG if (EMITVERBOSE) { - printf("Adjusted offset of " FMT_BB " from %04X to %04X\n", lstIG->igNum, lstIG->igOffs, + printf("Adjusted offset of " FMT_BB " from %04X to %04X\n", lstIG->GetDisplayId(), lstIG->igOffs, lstIG->igOffs - adjIG); } #endif // DEBUG @@ -5732,8 +5864,8 @@ void emitter::emitLongLoopAlign(unsigned alignmentBoundary DEBUG_ARG(bool isPlac // void emitter::emitConnectAlignInstrWithCurIG() { - JITDUMP("Mapping 'align' instruction in IG%02u to target IG%02u\n", emitAlignLastGroup->idaIG->igNum, - emitCurIG->igNum); + JITDUMP("Mapping 'align' instruction in IG%02u to target IG%02u\n", emitAlignLastGroup->idaIG->GetDisplayId(), + emitCurIG->GetDisplayId()); // Since we never align overlapping instructions, it is always guaranteed that // the emitAlignLastGroup points to the loop that is in process of getting aligned. @@ -5806,17 +5938,15 @@ bool emitter::emitEndsWithAlignInstr() // isAlignAdjusted - DEBUG only. Determine if adjustments are done to the align instructions or not. // During generating code, it is 'false' (because we haven't adjusted the size yet). // During outputting code, it is 'true'. -// containingIGNum - DEBUG only. IG number of IG that contains the current align instruction we are processing. -// loopHeadPredIGNum - DEBUG only. IG number of IG that precedes the IG that we are aligning with current align +// containingIG - DEBUG only. IG that contains the current align instruction we are processing. +// loopHeadPredIG - DEBUG only. IG that precedes the IG that we are aligning with current align // instruction. // // Returns: size of a loop in bytes. // -unsigned emitter::getLoopSize(insGroup* igLoopHeader, - unsigned maxLoopSize // - DEBUG_ARG(bool isAlignAdjusted) // - DEBUG_ARG(UNATIVE_OFFSET containingIGNum) // - DEBUG_ARG(UNATIVE_OFFSET loopHeadPredIGNum)) +unsigned emitter::getLoopSize(insGroup* igLoopHeader, + unsigned maxLoopSize DEBUGARG(bool isAlignAdjusted) DEBUGARG(insGroup* containingIG) + DEBUGARG(insGroup* loopHeadPredIG)) { unsigned loopSize = 0; @@ -5873,12 +6003,12 @@ unsigned emitter::getLoopSize(insGroup* igLoopHeader, { char buffer[5000]; int written = sprintf_s(buffer, 35, "Mismatch in align instruction.\n"); - written += sprintf_s(buffer + written, 100, "Containing IG: IG%02u\n", containingIGNum); - written += sprintf_s(buffer + written, 100, "loopHeadPredIG: IG%02u\n", loopHeadPredIGNum); - written += sprintf_s(buffer + written, 100, "loopHeadIG: IG%02u\n", igLoopHeader->igNum); - written += sprintf_s(buffer + written, 100, "igInLoop: IG%02u\n", igInLoop->igNum); + written += sprintf_s(buffer + written, 100, "Containing IG: IG%02u\n", containingIG->GetDisplayId()); + written += sprintf_s(buffer + written, 100, "loopHeadPredIG: IG%02u\n", loopHeadPredIG->GetDisplayId()); + written += sprintf_s(buffer + written, 100, "loopHeadIG: IG%02u\n", igLoopHeader->GetDisplayId()); + written += sprintf_s(buffer + written, 100, "igInLoop: IG%02u\n", igInLoop->GetDisplayId()); written += sprintf_s(buffer + written, 100, "igInLoop->igLoopBackEdge: IG%02u\n", - igInLoop->igLoopBackEdge->igNum); + igInLoop->igLoopBackEdge->GetDisplayId()); #if EMIT_BACKWARDS_NAVIGATION if (igInLoop->endsWithAlignInstr()) @@ -5887,7 +6017,7 @@ unsigned emitter::getLoopSize(insGroup* igLoopHeader, instrDescAlign* alignInstr = (instrDescAlign*)igInLoop->igLastIns; assert(alignInstr->idaIG == igInLoop); written += sprintf_s(buffer + written, 100, "igInLoop has align instruction for : IG%02u\n", - alignInstr->idaLoopHeadPredIG->igNext->igNum); + alignInstr->idaLoopHeadPredIG->igNext->GetDisplayId()); } #endif // EMIT_BACKWARDS_NAVIGATION @@ -5896,12 +6026,12 @@ unsigned emitter::getLoopSize(insGroup* igLoopHeader, for (igIter = igLoopHeader; (igIter != nullptr) && (igIter->igLoopBackEdge != igLoopHeader); igIter = igIter->igNext) { - written += sprintf_s(buffer + written, 100, "\tIG%02u\n", igIter->igNum); + written += sprintf_s(buffer + written, 100, "\tIG%02u\n", igIter->GetDisplayId()); } if (igIter == nullptr) { written += sprintf_s(buffer + written, 100, "Did not find IG with back edge to IG%02u\n", - igLoopHeader->igNum); + igLoopHeader->GetDisplayId()); } printf("\n\n%s", buffer); assert(false && !"Mismatch in align instruction"); @@ -5916,7 +6046,7 @@ unsigned emitter::getLoopSize(insGroup* igLoopHeader, // Find the alignInstr for igInLoop IG. for (; alignInstr != nullptr; alignInstr = alignInstr->idaNext) { - if (alignInstr->idaIG->igNum == igInLoop->igNum) + if (alignInstr->idaIG == igInLoop) { foundAlignInstr = true; break; @@ -6008,11 +6138,11 @@ bool emitter::emitSetLoopBackEdge(const BasicBlock* loopTopBlock) return false; } - if (dstIG->igNum > emitCurIG->igNum) + if (dstIG->IsAfter(emitCurIG)) { // Is this possible? - JITDUMP("ALIGN: found forward branch from IG%02u to IG%02u; not marking IG back edge.\n", emitCurIG->igNum, - dstIG->igNum); + JITDUMP("ALIGN: found forward branch from IG%02u to IG%02u; not marking IG back edge.\n", + emitCurIG->GetDisplayId(), dstIG->GetDisplayId()); return false; } @@ -6020,17 +6150,18 @@ bool emitter::emitSetLoopBackEdge(const BasicBlock* loopTopBlock) bool alignCurrentLoop = true; bool alignLastLoop = true; - unsigned currLoopStart = dstIG->igNum; - unsigned currLoopEnd = emitCurIG->igNum; + insGroup* currLoopStart = dstIG; + insGroup* currLoopEnd = emitCurIG; // Only mark back-edge if current loop starts after the last inner loop ended. - if (emitLastLoopEnd < currLoopStart) + if ((emitLastLoopEnd == nullptr) || emitLastLoopEnd->IsBefore(currLoopStart)) { assert(emitCurIG->igLoopBackEdge == nullptr); emitCurIG->igLoopBackEdge = dstIG; backEdgeSet = true; - JITDUMP("** IG%02u jumps back to IG%02u forming a loop.\n", currLoopEnd, currLoopStart); + JITDUMP("** IG%02u jumps back to IG%02u forming a loop.\n", currLoopEnd->GetDisplayId(), + currLoopStart->GetDisplayId()); emitLastLoopStart = currLoopStart; emitLastLoopEnd = currLoopEnd; @@ -6048,13 +6179,13 @@ bool emitter::emitSetLoopBackEdge(const BasicBlock* loopTopBlock) // |-----. // } - else if ((currLoopStart < emitLastLoopStart) && (emitLastLoopEnd < currLoopEnd)) + else if (currLoopStart->IsBefore(emitLastLoopStart) && emitLastLoopEnd->IsBefore(currLoopEnd)) { // if current loop completely encloses last loop, // then current loop should not be aligned. alignCurrentLoop = false; } - else if ((emitLastLoopStart < currLoopStart) && (currLoopEnd < emitLastLoopEnd)) + else if (emitLastLoopStart->IsBefore(currLoopStart) && currLoopEnd->IsBefore(emitLastLoopEnd)) { // if last loop completely encloses current loop, // then last loop should not be aligned. @@ -6088,12 +6219,13 @@ bool emitter::emitSetLoopBackEdge(const BasicBlock* loopTopBlock) markedCurrLoop = true; JITDUMP(";; Skip alignment for current loop IG%02u ~ IG%02u because it encloses an aligned loop " "IG%02u ~ IG%02u.\n", - currLoopStart, currLoopEnd, emitLastLoopStart, emitLastLoopEnd); + currLoopStart->GetDisplayId(), currLoopEnd->GetDisplayId(), emitLastLoopStart->GetDisplayId(), + emitLastLoopEnd->GetDisplayId()); } // Find the IG that has 'align' instruction to align the last loop // and clear the IGF_HAS_ALIGN flag. - if (!alignLastLoop && (loopHeadIG != nullptr) && (loopHeadIG->igNum == emitLastLoopStart)) + if (!alignLastLoop && (loopHeadIG != nullptr) && (loopHeadIG == emitLastLoopStart)) { assert(!markedLastLoop); assert(alignInstr->idaIG->endsWithAlignInstr() || alignInstr->idaIG->hadAlignInstr()); @@ -6104,7 +6236,8 @@ bool emitter::emitSetLoopBackEdge(const BasicBlock* loopTopBlock) markedLastLoop = true; JITDUMP(";; Skip alignment for aligned loop IG%02u ~ IG%02u because it encloses the current loop " "IG%02u ~ IG%02u.\n", - emitLastLoopStart, emitLastLoopEnd, currLoopStart, currLoopEnd); + emitLastLoopStart->GetDisplayId(), emitLastLoopEnd->GetDisplayId(), + currLoopStart->GetDisplayId(), currLoopEnd->GetDisplayId()); } if (markedLastLoop && markedCurrLoop) @@ -6160,8 +6293,8 @@ void emitter::emitLoopAlignAdjustments() insGroup* loopHeadIG = alignInstr->loopHeadIG(); insGroup* containingIG = alignInstr->idaIG; - JITDUMP(" Adjusting 'align' instruction in IG%02u that is targeted for IG%02u \n", containingIG->igNum, - loopHeadIG->igNum); + JITDUMP(" Adjusting 'align' instruction in IG%02u that is targeted for IG%02u \n", + containingIG->GetDisplayId(), loopHeadIG->GetDisplayId()); // Since we only adjust the padding up to the next align instruction which is behind the jump, we make sure // that we take into account all the alignBytes we removed until that point. Hence " - alignBytesRemoved" @@ -6176,9 +6309,8 @@ void emitter::emitLoopAlignAdjustments() unsigned actualPaddingNeeded = containingIG->endsWithAlignInstr() - ? emitCalculatePaddingForLoopAlignment(loopHeadIG, - loopIGOffset DEBUG_ARG(false) DEBUG_ARG(containingIG->igNum) - DEBUG_ARG(loopHeadPredIG->igNum)) + ? emitCalculatePaddingForLoopAlignment(loopHeadIG, loopIGOffset DEBUGARG(false) DEBUGARG(containingIG) + DEBUGARG(loopHeadPredIG)) : 0; assert(estimatedPaddingNeeded >= actualPaddingNeeded); @@ -6258,7 +6390,7 @@ void emitter::emitLoopAlignAdjustments() insGroup* adjOffIG = containingIG->igNext; instrDescAlign* nextAlign = emitAlignInNextIG(alignInstr); insGroup* adjOffUptoIG = nextAlign != nullptr ? nextAlign->idaIG : emitIGlast; - while ((adjOffIG != nullptr) && (adjOffIG->igNum <= adjOffUptoIG->igNum)) + while ((adjOffIG != nullptr) && adjOffIG->IsBeforeOrEqual(adjOffUptoIG)) { JITDUMP("Adjusted offset of %s from %04X to %04X\n", emitLabelString(adjOffIG), adjOffIG->igOffs, (adjOffIG->igOffs - alignBytesRemoved)); @@ -6271,9 +6403,9 @@ void emitter::emitLoopAlignAdjustments() if (actualPaddingNeeded > 0) { // Record the last loop IG that will be aligned. No overestimation - // adjustment will be done after emitLastAlignedIgNum. + // adjustment will be done after emitLastAlignedIG. JITDUMP("Recording last aligned IG: %s\n", emitLabelString(loopHeadPredIG)); - emitLastAlignedIgNum = loopHeadPredIG->igNum; + emitLastAlignedIG = loopHeadPredIG; } } @@ -6292,8 +6424,8 @@ void emitter::emitLoopAlignAdjustments() // isAlignAdjusted - Determine if adjustments are done to the align instructions or not. // During generating code, it is 'false' (because we haven't adjusted the size yet). // During outputting code, it is 'true'. -// containingIGNum - IG number of IG that contains the current align instruction we are processing. -// loopHeadPredIGNum - IG number of IG that preceds the IG that we are aligning with current align instruction. +// containingIG - IG that contains the current align instruction we are processing. +// loopHeadPredIG - IG that preceds the IG that we are aligning with current align instruction. // // Returns: Padding amount. // 0 means no padding is needed, either because loop is already aligned or it @@ -6318,9 +6450,9 @@ void emitter::emitLoopAlignAdjustments() // 3c. return paddingNeeded. // unsigned emitter::emitCalculatePaddingForLoopAlignment(insGroup* loopHeadIG, - size_t offset DEBUG_ARG(bool isAlignAdjusted) - DEBUG_ARG(UNATIVE_OFFSET containingIGNum) - DEBUG_ARG(UNATIVE_OFFSET loopHeadPredIGNum)) + size_t offset DEBUGARG(bool isAlignAdjusted) + DEBUGARG(insGroup* containingIG) + DEBUGARG(insGroup* loopHeadPredIG)) { unsigned alignmentBoundary = m_compiler->opts.compJitAlignLoopBoundary; @@ -6347,8 +6479,8 @@ unsigned emitter::emitCalculatePaddingForLoopAlignment(insGroup* loopHeadIG, maxLoopSize = m_compiler->opts.compJitAlignLoopMaxCodeSize; } - unsigned loopSize = getLoopSize(loopHeadIG, maxLoopSize DEBUG_ARG(isAlignAdjusted) DEBUG_ARG(containingIGNum) - DEBUG_ARG(loopHeadPredIGNum)); + unsigned loopSize = + getLoopSize(loopHeadIG, maxLoopSize DEBUGARG(isAlignAdjusted) DEBUGARG(containingIG) DEBUGARG(loopHeadPredIG)); // No padding if loop is big if (loopSize > maxLoopSize) @@ -7113,12 +7245,6 @@ unsigned emitter::emitEndCodeGen(Compiler* comp, #endif } - /* Are we overflowing? */ - if (ig->igNext && (ig->igNum + 1 != ig->igNext->igNum)) - { - NO_WAY("Too many instruction groups"); - } - instrDesc* id = emitFirstInstrDesc(ig->igData); #ifdef DEBUG @@ -9416,8 +9542,6 @@ UNATIVE_OFFSET emitter::emitCodeOffset(void* blockPtr, unsigned codePos) of = emitGetInsOfsFromCodePos(codePos); - // printf("[IG=%02u;ID=%03u;OF=%04X] <= %08X\n", ig->igNum, emitGetInsNumFromCodePos(codePos), of, codePos); - /* Make sure the offset estimate is accurate */ assert(of == emitFindOffset(ig, emitGetInsNumFromCodePos(codePos))); } @@ -9773,7 +9897,7 @@ void emitter::emitInitIG(insGroup* ig) { /* Assign the next available index to the instruction group */ - ig->igNum = emitNxtIGnum; + ig->InitializeNum(emitNxtIGnum); emitNxtIGnum++; diff --git a/src/coreclr/jit/emit.h b/src/coreclr/jit/emit.h index 28e61029a4c06e..d871a91cd8a516 100644 --- a/src/coreclr/jit/emit.h +++ b/src/coreclr/jit/emit.h @@ -280,7 +280,10 @@ struct insGroup size_t igDataSize; // size of instrDesc data pointed to by 'igData' #endif - UNATIVE_OFFSET igNum; // for ordering (and display) purposes +private: + unsigned igNum; // for ordering (and display) purposes + +public: UNATIVE_OFFSET igOffs; // offset of this group within method unsigned int igFuncIdx; // Which function/funclet does this belong to? (Index into Compiler::compFuncInfos array.) unsigned short igFlags; // see IGF_xxx below @@ -311,6 +314,7 @@ struct insGroup #ifdef TARGET_ARM64 #define IGF_HAS_REMOVED_INSTR 0x1000 // this group has an instruction that was removed. #endif +#define IGF_OUT_OF_ORDER_HEAD 0x2000 // first group (generated in-order) of a region generated out-of-order // Mask of IGF_* flags that should be propagated to new blocks when they are created. // This allows prologs and epilogs to be any number of IGs, but still be @@ -320,6 +324,7 @@ struct insGroup #else // DEBUG #define IGF_PROPAGATE_MASK (IGF_EPILOG | IGF_FUNCLET_PROLOG) #endif // DEBUG +#define IGF_OUT_OF_ORDER_MASK (IGF_EPILOG | IGF_FUNCLET_PROLOG | IGF_FUNCLET_EPILOG) // Try to do better packing based on how large regMaskSmall is (8, 16, or 64 bits). @@ -390,6 +395,11 @@ struct insGroup return (igFlags & IGF_REMOVED_ALIGN) != 0; } + void InitializeNum(unsigned num); + unsigned GetDisplayId() const; + bool IsBefore(const insGroup* ig) const; + bool IsBeforeOrEqual(const insGroup* ig) const; + bool IsAfter(const insGroup* ig) const; }; // end of struct insGroup // For AMD64 the maximum prolog/epilog size supported on the OS is 256 bytes @@ -2902,28 +2912,28 @@ class emitter void emitRemoveJumpToNextInst(); // try to remove unconditional jumps to the next instruction #if FEATURE_LOOP_ALIGN - instrDescAlign* emitCurIGAlignList; // list of align instructions in current IG - unsigned emitLastLoopStart; // Start IG of last inner loop - unsigned emitLastLoopEnd; // End IG of last inner loop - unsigned emitLastAlignedIgNum; // last IG that has align instruction - instrDescAlign* emitAlignList; // list of all align instructions in method - instrDescAlign* emitAlignLast; // last align instruction in method + instrDescAlign* emitCurIGAlignList; // list of align instructions in current IG + insGroup* emitLastLoopStart; // Start IG of last inner loop + insGroup* emitLastLoopEnd; // End IG of last inner loop + insGroup* emitLastAlignedIG; // last IG that has align instruction + instrDescAlign* emitAlignList; // list of all align instructions in method + instrDescAlign* emitAlignLast; // last align instruction in method // Points to the most recent added align instruction. If there are multiple align instructions like in arm64 or // non-adaptive alignment on xarch, this points to the first align instruction of the series of align instructions. instrDescAlign* emitAlignLastGroup; unsigned getLoopSize(insGroup* igLoopHeader, - unsigned maxLoopSize DEBUG_ARG(bool isAlignAdjusted) DEBUG_ARG(UNATIVE_OFFSET containingIGNum) - DEBUG_ARG(UNATIVE_OFFSET loopHeadPredIGNum)); // Get the smallest loop size + unsigned maxLoopSize DEBUGARG(bool isAlignAdjusted) DEBUGARG(insGroup* containingIG) + DEBUGARG(insGroup* loopHeadPredIG)); // Get the smallest loop size void emitLoopAlignment(DEBUG_ARG1(bool isPlacedBehindJmp)); bool emitEndsWithAlignInstr(); // Validate if newLabel is appropriate bool emitSetLoopBackEdge(const BasicBlock* loopTopBlock); void emitLoopAlignAdjustments(); // Predict if loop alignment is needed and make appropriate adjustments unsigned emitCalculatePaddingForLoopAlignment(insGroup* ig, - size_t offset DEBUG_ARG(bool isAlignAdjusted) - DEBUG_ARG(UNATIVE_OFFSET containingIGNum) - DEBUG_ARG(UNATIVE_OFFSET loopHeadPredIGNum)); + size_t offset DEBUGARG(bool isAlignAdjusted) + DEBUGARG(insGroup* containingIG) + DEBUGARG(insGroup* loopHeadPredIG)); void emitLoopAlign(unsigned paddingBytes, bool isFirstAlign DEBUG_ARG(bool isPlacedBehindJmp)); void emitLongLoopAlign(unsigned alignmentBoundary DEBUG_ARG(bool isPlacedBehindJmp)); diff --git a/src/coreclr/jit/emitarm64.cpp b/src/coreclr/jit/emitarm64.cpp index 254a81fe57ec22..250af9a94d9213 100644 --- a/src/coreclr/jit/emitarm64.cpp +++ b/src/coreclr/jit/emitarm64.cpp @@ -14802,7 +14802,7 @@ void emitter::emitDispInsHelp( // targetIG is only set for 1st of the series of align instruction if ((alignInstrId->idaLoopHeadPredIG != nullptr) && (alignInstrId->loopHeadIG() != nullptr)) { - printf(" for IG%02u", alignInstrId->loopHeadIG()->igNum); + printf(" for IG%02u", alignInstrId->loopHeadIG()->GetDisplayId()); } printf("]"); } diff --git a/src/coreclr/jit/emitloongarch64.cpp b/src/coreclr/jit/emitloongarch64.cpp index d7b878bad395d2..a4822839ce7de6 100644 --- a/src/coreclr/jit/emitloongarch64.cpp +++ b/src/coreclr/jit/emitloongarch64.cpp @@ -2884,8 +2884,7 @@ void emitter::emitJumpDistBind() assert(lastSJ == nullptr || lastIG != jmp->idjIG || lastSJ->idjOffs < (jmp->idjOffs + adjSJ)); lastSJ = (lastIG == jmp->idjIG) ? jmp : nullptr; - assert(lastIG == nullptr || lastIG->igNum <= jmp->idjIG->igNum || jmp->idjIG == prologIG || - emitNxtIGnum > unsigned(0xFFFF)); // igNum might overflow + assert(lastIG == nullptr || lastIG->IsBeforeOrEqual(jmp->idjIG) || jmp->idjIG == prologIG); lastIG = jmp->idjIG; #endif // DEBUG @@ -2914,8 +2913,8 @@ void emitter::emitJumpDistBind() #ifdef DEBUG if (EMITVERBOSE) { - printf("Adjusted offset of " FMT_BB " from %04X to %04X\n", lstIG->igNum, lstIG->igOffs, - lstIG->igOffs + adjIG); + printf("Adjusted offset of " FMT_BB " from %04X to %04X\n", lstIG->GetDisplayId(), + lstIG->igOffs, lstIG->igOffs + adjIG); } #endif // DEBUG lstIG->igOffs += adjIG; @@ -2999,7 +2998,7 @@ void emitter::emitJumpDistBind() srcEncodingOffs = srcInstrOffs + ssz; // Encoding offset of relative offset for small branch - if (jmpIG->igNum < tgtIG->igNum) + if (jmpIG->IsBefore(tgtIG)) { /* Forward jump */ @@ -3200,7 +3199,7 @@ void emitter::emitJumpDistBind() #ifdef DEBUG if (EMITVERBOSE) { - printf("Adjusted offset of " FMT_BB " from %04X to %04X\n", lstIG->igNum, lstIG->igOffs, + printf("Adjusted offset of " FMT_BB " from %04X to %04X\n", lstIG->GetDisplayId(), lstIG->igOffs, lstIG->igOffs + adjIG); } #endif // DEBUG diff --git a/src/coreclr/jit/emitxarch.cpp b/src/coreclr/jit/emitxarch.cpp index c4389db758c22a..06eec05ce90b60 100644 --- a/src/coreclr/jit/emitxarch.cpp +++ b/src/coreclr/jit/emitxarch.cpp @@ -14076,7 +14076,7 @@ void emitter::emitDispIns( // targetIG is only set for 1st of the series of align instruction if ((alignInstrId->idaLoopHeadPredIG != nullptr) && (alignInstrId->loopHeadIG() != nullptr)) { - printf(" for IG%02u", alignInstrId->loopHeadIG()->igNum); + printf(" for IG%02u", alignInstrId->loopHeadIG()->GetDisplayId()); } printf("]"); } @@ -20109,9 +20109,8 @@ size_t emitter::emitOutputInstr(insGroup* ig, instrDesc* id, BYTE** dp) #endif #if FEATURE_LOOP_ALIGN - // Only compensate over-estimated instructions if emitCurIG is before - // the last IG that needs alignment. - if (emitCurIG->igNum <= emitLastAlignedIgNum) + // Only compensate over-estimated instructions if emitCurIG is before the last IG that needs alignment. + if ((emitLastAlignedIG != nullptr) && emitCurIG->IsBeforeOrEqual(emitLastAlignedIG)) { int diff = id->idCodeSize() - ((UNATIVE_OFFSET)(dst - *dp)); assert(diff >= 0); diff --git a/src/native/watchdog/watchdog.cpp b/src/native/watchdog/watchdog.cpp index 2ab508a5fb53c8..ef59dbfe87afb2 100644 --- a/src/native/watchdog/watchdog.cpp +++ b/src/native/watchdog/watchdog.cpp @@ -41,7 +41,7 @@ int main(const int argc, const char *argv[]) const long timeout_mins = strtol(argv[1], nullptr, 10); int exit_code = run_timed_process(timeout_mins * 60000L, argc-2, &argv[2]); - printf("App Exit Code: %d\n", exit_code); + printf("Exit Code: %d\n", exit_code); return exit_code; } @@ -58,7 +58,6 @@ int run_timed_process(const long timeout_ms, const int proc_argc, const char *pr STARTUPINFOA startup_info; PROCESS_INFORMATION proc_info; - unsigned long exit_code; ZeroMemory(&startup_info, sizeof(startup_info)); startup_info.cb = sizeof(startup_info); @@ -72,7 +71,11 @@ int run_timed_process(const long timeout_ms, const int proc_argc, const char *pr return error_code; } - WaitForSingleObject(proc_info.hProcess, timeout_ms); + if (WaitForSingleObject(proc_info.hProcess, timeout_ms) == WAIT_TIMEOUT) + { + printf("Child process took too long. Timed out...\n"); + } + DWORD exit_code; GetExitCodeProcess(proc_info.hProcess, &exit_code); CloseHandle(proc_info.hProcess); From 55707bf2eadf45558b78abeabb323ecb3e52938f Mon Sep 17 00:00:00 2001 From: Juan Hoyos <19413848+hoyosjs@users.noreply.github.com> Date: Mon, 4 May 2026 23:12:49 -0700 Subject: [PATCH 102/115] Reorder static fields in LoggingEventSource to prevent reentrant bug in FilterSpec parsing (#127686) Fixes https://github.com/dotnet/runtime/issues/127681 --- .../src/LoggingEventSource.cs | 17 ++++-- .../tests/EventSourceLoggerTest.cs | 54 +++++++++++++++++++ ...xtensions.Logging.EventSource.Tests.csproj | 1 + 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Logging.EventSource/src/LoggingEventSource.cs b/src/libraries/Microsoft.Extensions.Logging.EventSource/src/LoggingEventSource.cs index 177c674f3b34d2..56982fcc479b16 100644 --- a/src/libraries/Microsoft.Extensions.Logging.EventSource/src/LoggingEventSource.cs +++ b/src/libraries/Microsoft.Extensions.Logging.EventSource/src/LoggingEventSource.cs @@ -112,6 +112,17 @@ public static class Keywords public const EventKeywords JsonMessage = (EventKeywords)8; } + // Separators are in a nested class so their initialization is guaranteed to complete + // before first use, regardless of declaration order in the outer class. The Instance + // constructor can trigger OnEventCommand re-entrantly (via EventPipe enable during + // the base EventSource constructor), which calls ParseFilterSpec and needs these arrays. + // See: https://github.com/dotnet/roslyn/issues/77005 + private static class Separators + { + internal static readonly char[] Semicolon = new[] { ';' }; + internal static readonly char[] Colon = new[] { ':' }; + } + /// /// The one and only instance of the LoggingEventSource. /// @@ -125,8 +136,6 @@ public static class Keywords private const string UseAppFilters = "UseAppFilters"; private const string WriteEventCoreSuppressionJustification = "WriteEventCore is safe when eventData object is a primitive type which is in this case."; private const string WriteEventDynamicDependencySuppressionJustification = "DynamicDependency attribute will ensure that the required properties are not trimmed."; - private static readonly char[] s_semicolon = new[] { ';' }; - private static readonly char[] s_colon = new[] { ':' }; // This event source uses IEnumerable as an event parameter type which is only supported by EtwSelfDescribingEventFormat. private LoggingEventSource() : base(EventSourceSettings.EtwSelfDescribingEventFormat) @@ -426,7 +435,7 @@ private static LoggerFilterRule[] ParseFilterSpec(string? filterSpec, LogLevel d var rules = new List(); int ruleStringsStartIndex = 0; - string[] ruleStrings = filterSpec.Split(s_semicolon, StringSplitOptions.RemoveEmptyEntries); + string[] ruleStrings = filterSpec.Split(Separators.Semicolon, StringSplitOptions.RemoveEmptyEntries); if (ruleStrings.Length > 0 && ruleStrings[0].Equals(UseAppFilters, StringComparison.OrdinalIgnoreCase)) { // Avoid adding default rule to disable event source loggers @@ -441,7 +450,7 @@ private static LoggerFilterRule[] ParseFilterSpec(string? filterSpec, LogLevel d { string rule = ruleStrings[i]; LogLevel level = defaultLevel; - string[] parts = rule.Split(s_colon, 2); + string[] parts = rule.Split(Separators.Colon, 2); string loggerName = parts[0]; if (loggerName.Length == 0) { diff --git a/src/libraries/Microsoft.Extensions.Logging.EventSource/tests/EventSourceLoggerTest.cs b/src/libraries/Microsoft.Extensions.Logging.EventSource/tests/EventSourceLoggerTest.cs index d7fd7ac5e3c552..e80953f33b84ee 100644 --- a/src/libraries/Microsoft.Extensions.Logging.EventSource/tests/EventSourceLoggerTest.cs +++ b/src/libraries/Microsoft.Extensions.Logging.EventSource/tests/EventSourceLoggerTest.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using System.Text.Json; +using Microsoft.DotNet.RemoteExecutor; using Microsoft.Extensions.Logging.EventSource; using Xunit; using Newtonsoft.Json; @@ -760,6 +761,59 @@ private static void VerifySingleEvent(string eventJson, string loggerName, strin } } + /// + /// Regression test for https://github.com/dotnet/runtime/issues/127681 + /// Verifies that ParseFilterSpec correctly splits on semicolons when the + /// EventSource is first enabled during its own type initializer. This requires + /// a fresh process because LoggingEventSource.Instance is a singleton. + /// + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void FilterSpec_ParsedCorrectly_WhenEnabledDuringTypeInitializer() + { + RemoteExecutor.Invoke(static () => + { + // Add a listener with a multi-rule FilterSpec BEFORE LoggingEventSource is created + // to test the reentrancy of ParseFilterSpec when LoggingEventSource is enabled + using var listener = new PreEnableListener("Cat1:Warning;Cat2:Error"); + + using ILoggerFactory factory = LoggerFactory.Create(b => b.AddEventSourceLogger()); + ILogger cat1 = factory.CreateLogger("Cat1"); + ILogger cat2 = factory.CreateLogger("Cat2"); + + Assert.True(cat1.IsEnabled(LogLevel.Warning), "Cat1 should be enabled at Warning"); + Assert.False(cat1.IsEnabled(LogLevel.Information), "Cat1 should NOT be enabled at Information"); + Assert.True(cat2.IsEnabled(LogLevel.Error), "Cat2 should be enabled at Error"); + Assert.False(cat2.IsEnabled(LogLevel.Warning), "Cat2 should NOT be enabled at Warning"); + }).Dispose(); + } + + /// + /// EventListener that enables LoggingEventSource with a given FilterSpec + /// as soon as it sees the source being created (via OnEventSourceCreated). + /// Used by the RemoteExecutor regression test to exercise the deferred-command + /// path during type initialization. + /// + private class PreEnableListener : EventListener + { + private readonly string _filterSpec; + + public PreEnableListener(string filterSpec) + { + _filterSpec = filterSpec; + } + + protected override void OnEventSourceCreated(System.Diagnostics.Tracing.EventSource eventSource) + { + if (eventSource.Name == "Microsoft-Extensions-Logging") + { + var args = new Dictionary { ["FilterSpecs"] = _filterSpec }; + EnableEvents(eventSource, EventLevel.Verbose, LoggingEventSource.Keywords.JsonMessage, args); + } + } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) { } + } + private class TestEventListener : EventListener { public class ListenerSettings diff --git a/src/libraries/Microsoft.Extensions.Logging.EventSource/tests/Microsoft.Extensions.Logging.EventSource.Tests.csproj b/src/libraries/Microsoft.Extensions.Logging.EventSource/tests/Microsoft.Extensions.Logging.EventSource.Tests.csproj index e12770f9d99142..a7586b20c77017 100644 --- a/src/libraries/Microsoft.Extensions.Logging.EventSource/tests/Microsoft.Extensions.Logging.EventSource.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.Logging.EventSource/tests/Microsoft.Extensions.Logging.EventSource.Tests.csproj @@ -3,6 +3,7 @@ $(NetCoreAppCurrent);$(NetFrameworkCurrent) true + true true false From 349228c1adaa2a4250db5257f876d218ed6dbc51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Strehovsk=C3=BD?= Date: Tue, 5 May 2026 15:38:18 +0900 Subject: [PATCH 103/115] Preserve execution aborted state in NativeAOT GC info (#127680) Avoid mutating the live MethodInfo from GetCodeOffset debug validation so hardware-fault stack walks keep the executionAborted flag for GC root reporting. --- .../Runtime/windows/CoffNativeCodeManager.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/coreclr/nativeaot/Runtime/windows/CoffNativeCodeManager.cpp b/src/coreclr/nativeaot/Runtime/windows/CoffNativeCodeManager.cpp index b2a85e88d588e5..83ad8b3f8be6d5 100644 --- a/src/coreclr/nativeaot/Runtime/windows/CoffNativeCodeManager.cpp +++ b/src/coreclr/nativeaot/Runtime/windows/CoffNativeCodeManager.cpp @@ -378,7 +378,16 @@ uint32_t CoffNativeCodeManager::GetCodeOffset(MethodInfo* pMethodInfo, PTR_VOID { CoffNativeMethodInfo * pNativeMethodInfo = (CoffNativeMethodInfo *)pMethodInfo; - _ASSERTE(FindMethodInfo(address, pMethodInfo) && (MethodInfo*)pNativeMethodInfo == pMethodInfo); +#ifdef _DEBUG + MethodInfo methodInfo; + bool foundMethodInfo = FindMethodInfo(address, &methodInfo); + _ASSERTE(foundMethodInfo); + if (foundMethodInfo) + { + CoffNativeMethodInfo * pDebugNativeMethodInfo = (CoffNativeMethodInfo *)&methodInfo; + _ASSERTE(pDebugNativeMethodInfo->mainRuntimeFunction == pNativeMethodInfo->mainRuntimeFunction); + } +#endif size_t unwindDataBlobSize; PTR_VOID pUnwindDataBlob = GetUnwindDataBlob(m_moduleBase, pNativeMethodInfo->mainRuntimeFunction, &unwindDataBlobSize); From 7dbea299384aa98d68e344e12fc8a5bd9b324fcc Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 11:01:47 +0100 Subject: [PATCH 104/115] Route async resumption stub fixups through precode fixup handling (#127523) Async resumption stub fixups were added directly to the compiled method node, bypassing the existing precode fixup path that deduplicates fixups and only commits them after a successful compile. The fixups were eventually still deduplicated, but this is the preferred method for adding a fixup. Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jtschuster <36744439+jtschuster@users.noreply.github.com> --- .../TestCases/R2RTestSuites.cs | 47 +++++++++++- .../AsyncMultipleSuspensionPoints.cs | 32 ++++++++ .../TestCasesRunner/R2RResultChecker.cs | 74 +++++++++++++++++-- .../TestCasesRunner/R2RTestRunner.cs | 9 +++ .../JitInterface/CorInfoImpl.ReadyToRun.cs | 2 +- 5 files changed, 156 insertions(+), 8 deletions(-) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncMultipleSuspensionPoints.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 9e42292047dfe9..d40c9f9269f201 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using ILCompiler.ReadyToRun.Tests.TestCasesRunner; using ILCompiler.Reflection.ReadyToRun; +using Internal.ReadyToRunConstants; using Microsoft.DotNet.XUnitExtensions; using Xunit; using Xunit.Abstractions; @@ -270,7 +271,8 @@ public void RuntimeAsyncDevirtualize() static void Validate(ReadyToRunReader reader) { - R2RAssert.HasAsyncVariant(reader, "GetValueAsync"); + R2RAssert.HasAsyncVariant(reader, "OpenImpl.GetValueAsync("); + R2RAssert.HasAsyncVariant(reader, "SealedImpl.GetValueAsync("); } } @@ -308,6 +310,49 @@ static void Validate(ReadyToRunReader reader) } } + /// + /// Validates that ResumptionStubEntryPoint fixups are deduplicated for a method: + /// even with multiple suspension points and forced compilation retries (via + /// --determinism-stress), each compiled method should have exactly one + /// ResumptionStubEntryPoint fixup. + /// + [Fact] + public void RuntimeAsyncResumptionStubFixupDedup() + { + var asm = new CompiledAssembly + { + AssemblyName = nameof(RuntimeAsyncResumptionStubFixupDedup), + SourceResourceNames = + [ + "RuntimeAsync/AsyncMultipleSuspensionPoints.cs", + "RuntimeAsync/RuntimeAsyncMethodGenerationAttribute.cs", + ], + Features = { RuntimeAsyncFeature }, + }; + + new R2RTestRunner(_output).Run(new R2RTestCase( + nameof(RuntimeAsyncResumptionStubFixupDedup), + [ + new(nameof(RuntimeAsyncResumptionStubFixupDedup), [new CrossgenAssembly(asm)]) + { + // Force each method to be compiled multiple times so that + // any non-deduplicated fixup additions become observable. + AdditionalArgs = { "--determinism-stress=2" }, + Validate = Validate, + }, + ])); + + static void Validate(ReadyToRunReader reader) + { + R2RAssert.HasAsyncVariant(reader, ".MultipleAwaits("); + R2RAssert.HasAsyncVariant(reader, ".MultipleAwaitsWithRefs("); + R2RAssert.HasResumptionStubFixup(reader, ".MultipleAwaits("); + R2RAssert.HasResumptionStubFixup(reader, ".MultipleAwaitsWithRefs("); + R2RAssert.HasFixupKindCountOnMethod(reader, ReadyToRunFixupKind.ResumptionStubEntryPoint, ".MultipleAwaits(", 1); + R2RAssert.HasFixupKindCountOnMethod(reader, ReadyToRunFixupKind.ResumptionStubEntryPoint, ".MultipleAwaitsWithRefs(", 1); + } + } + /// /// PR #121679: MutableModule async references + cross-module inlining /// of runtime-async methods with cross-module dependency. diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncMultipleSuspensionPoints.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncMultipleSuspensionPoints.cs new file mode 100644 index 00000000000000..2a189574680745 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/RuntimeAsync/AsyncMultipleSuspensionPoints.cs @@ -0,0 +1,32 @@ +// Test: Async methods with multiple suspension points. +// Used to validate that ResumptionStubEntryPoint fixups are deduplicated +// across compilation retries (only one fixup per compiled method). +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +public static class AsyncMultipleSuspensionPoints +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task MultipleAwaits() + { + int x = 1; + await Task.Yield(); + x++; + await Task.Yield(); + x++; + await Task.Yield(); + return x; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static async Task MultipleAwaitsWithRefs() + { + string a = "a"; + await Task.Yield(); + string b = a + "b"; + await Task.Yield(); + string c = b + "c"; + await Task.Yield(); + return c; + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs index e3e47b450fd9b1..c2e5e651b32cd6 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs @@ -246,7 +246,9 @@ public static void HasCrossModuleInliningInfo(ReadyToRunReader reader) } /// - /// Asserts the R2R image contains an [ASYNC] variant entry whose signature contains the given method name. + /// Asserts the R2R image contains exactly one [ASYNC] variant entry whose signature contains the given method name. + /// Fails if no match is found or if more than one [ASYNC] method signature matches the search token. + /// Use a precise token (e.g. ".MethodName(") to avoid unintended substring matches. /// public static void HasAsyncVariant(ReadyToRunReader reader, string methodName) { @@ -255,10 +257,17 @@ public static void HasAsyncVariant(ReadyToRunReader reader, string methodName) .Select(m => m.SignatureString) .ToList(); - Assert.True( - asyncSigs.Any(s => s.Contains(methodName, StringComparison.OrdinalIgnoreCase)), + var matchingSigs = asyncSigs + .Where(s => s.Contains(methodName, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + Assert.True(matchingSigs.Count > 0, $"Expected [ASYNC] variant for '{methodName}' not found. " + $"Async methods: [{string.Join(", ", asyncSigs)}]"); + + Assert.True(matchingSigs.Count == 1, + $"Expected exactly one [ASYNC] variant matching '{methodName}', " + + $"but found {matchingSigs.Count}: [{string.Join(", ", matchingSigs)}]"); } /// @@ -311,6 +320,52 @@ public static void HasResumptionStubFixup(ReadyToRunReader reader, string method HasFixupKindOnMethod(reader, ReadyToRunFixupKind.ResumptionStubEntryPoint, methodName); } + /// + /// Asserts that exactly one method whose signature contains + /// has at least one fixup of , and that method has exactly + /// fixups of that kind. + /// Fails if no match is found, if more than one method matches the search token, or if + /// the fixup count differs from . + /// Use a precise token (e.g. ".MethodName(") to avoid unintended substring matches. + /// Useful for ensuring fixups are properly deduplicated. + /// + public static void HasFixupKindCountOnMethod(ReadyToRunReader reader, ReadyToRunFixupKind kind, string methodName, int expectedCount) + { + var matchingMethods = new List<(string Signature, int Count)>(); + foreach (var method in GetAllMethods(reader)) + { + if (method.Fixups is null) + continue; + if (!method.SignatureString.Contains(methodName, StringComparison.OrdinalIgnoreCase)) + continue; + + int count = 0; + foreach (var cell in method.Fixups) + { + if (cell.Signature is not null && cell.Signature.FixupKind == kind) + count++; + } + + // Only consider methods that have at least one fixup of this kind so we + // don't false-fail on co-named thunks that legitimately have none. + if (count > 0) + matchingMethods.Add((method.SignatureString, count)); + } + + Assert.True(matchingMethods.Count > 0, + $"No method matching '{methodName}' was found with any '{kind}' fixup."); + + Assert.True(matchingMethods.Count == 1, + $"Expected exactly one method matching '{methodName}' with '{kind}' fixup, " + + $"but found {matchingMethods.Count}: [{string.Join(", ", matchingMethods.Select(m => m.Signature))}]"); + + foreach (var (signature, count) in matchingMethods) + { + Assert.True(count == expectedCount, + $"Expected exactly {expectedCount} '{kind}' fixup(s) on method '{signature}', but found {count}."); + } + } + /// /// Asserts the R2R image contains at least one fixup of the given kind. /// @@ -334,11 +389,14 @@ public static void HasFixupKind(ReadyToRunReader reader, ReadyToRunFixupKind kin } /// - /// Asserts a method whose signature contains + /// Asserts exactly one method whose signature contains /// has at least one fixup of the given kind. + /// Fails if no match is found or if more than one method matches the search token. + /// Use a precise token (e.g. ".MethodName(") to avoid unintended substring matches. /// public static void HasFixupKindOnMethod(ReadyToRunReader reader, ReadyToRunFixupKind kind, string methodName) { + var matchingMethods = new List(); var methodsWithFixup = new List(); foreach (var method in GetAllMethods(reader)) { @@ -359,13 +417,17 @@ public static void HasFixupKindOnMethod(ReadyToRunReader reader, ReadyToRunFixup { methodsWithFixup.Add(method.SignatureString); if (method.SignatureString.Contains(methodName, StringComparison.OrdinalIgnoreCase)) - return; + matchingMethods.Add(method.SignatureString); } } - Assert.Fail( + Assert.True(matchingMethods.Count > 0, $"Expected fixup kind '{kind}' on method matching '{methodName}', but not found.\n" + $"Methods with '{kind}' fixups: [{string.Join(", ", methodsWithFixup)}]"); + + Assert.True(matchingMethods.Count == 1, + $"Expected exactly one method matching '{methodName}' with fixup kind '{kind}', " + + $"but found {matchingMethods.Count}: [{string.Join(", ", matchingMethods)}]"); } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs index e6fe74daf2aaf1..980edfe8f75e72 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RTestRunner.cs @@ -83,6 +83,12 @@ internal sealed class CrossgenCompilation(string name, List as /// public List Options { get; init; } = new(); + /// + /// Additional raw command-line arguments to pass to crossgen2 (e.g. "--determinism-stress=2"). + /// Use this for options that take a value or are not modeled by . + /// + public List AdditionalArgs { get; init; } = new(); + /// /// Optional validator for this compilation's R2R output image. /// @@ -296,6 +302,9 @@ private static string RunCrossgenCompilation( foreach (var option in compilation.Options) args.Add(option.ToArg()); + // Caller-supplied raw args (for options that take values, e.g. --determinism-stress=N) + args.AddRange(compilation.AdditionalArgs); + // Global refs (runtime pack + System.Private.CoreLib) AddRefArgs(args, refPaths); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index 0be908211636f7..a5ea5e2c67573d 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -493,7 +493,7 @@ private void AddPrecodeFixup(ISymbolNode node) private void AddResumptionStubFixup(MethodWithGCInfo compiledStubNode) { - _methodCodeNode.Fixups.Add(_compilation.SymbolNodeFactory.ResumptionStubEntryPoint(compiledStubNode)); + AddPrecodeFixup(_compilation.SymbolNodeFactory.ResumptionStubEntryPoint(compiledStubNode)); } private CORINFO_METHOD_STRUCT_* getAsyncResumptionStub(ref void* entryPoint) From 7ab06a3e7067a7203ac85f2720d003cac7991440 Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Tue, 5 May 2026 12:04:29 +0200 Subject: [PATCH 105/115] ProcessTests: redirect stdio and kill entire process tree for shell-script-based tests. (#127566) The LongProcessNamesAreSupported and ProcessNameMatchesScriptName tests spawn a shell script that runs sleep as a grandchild. Killing only the shell left the sleep process alive, holding stdout/stderr pipes open for the parent that runs the tests. --------- Co-authored-by: Adam Sitnik --- .../tests/ProcessTests.Unix.cs | 11 +++++++++-- .../System.Diagnostics.Process/tests/ProcessTests.cs | 10 ++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs index 1e7399eba08ae0..cea313179abac9 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs @@ -15,6 +15,7 @@ using Xunit; using Microsoft.DotNet.RemoteExecutor; using Microsoft.DotNet.XUnitExtensions; +using Microsoft.Win32.SafeHandles; namespace System.Diagnostics.Tests { @@ -173,7 +174,13 @@ public void ProcessNameMatchesScriptName() File.WriteAllText(filename, $"#!/bin/sh\nsleep 600\n"); // sleep 10 min. File.SetUnixFileMode(filename, ExecutablePermissions); - using (var process = Process.Start(new ProcessStartInfo { FileName = filename })) + using SafeFileHandle nullHandle = File.OpenNullHandle(); + ProcessStartInfo psi = new(filename) + { + StandardOutputHandle = nullHandle, + StandardErrorHandle= nullHandle + }; + using (var process = Process.Start(psi)) { try { @@ -186,7 +193,7 @@ public void ProcessNameMatchesScriptName() } finally { - process.Kill(); + process.Kill(entireProcessTree: true); process.WaitForExit(); } } diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs index 61dad866b71209..afa66d916c2f5a 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs @@ -2499,7 +2499,13 @@ public void LongProcessNamesAreSupported() string sleepCommandPathFileName = Path.Combine(TestDirectory, LongProcessName); File.Copy(sleepPath, sleepCommandPathFileName); - using (Process px = Process.Start(sleepCommandPathFileName, "600")) + using SafeFileHandle nullHandle = File.OpenNullHandle(); + ProcessStartInfo psi = new(sleepCommandPathFileName, "600") + { + StandardOutputHandle = nullHandle, + StandardErrorHandle= nullHandle + }; + using (Process px = Process.Start(psi)) { // Reading of long process names is flaky during process startup and shutdown. // Wait a bit to skip over the period where the ProcessName is not reliable. @@ -2512,7 +2518,7 @@ public void LongProcessNamesAreSupported() } finally { - px.Kill(); + px.Kill(entireProcessTree: true); px.WaitForExit(); } } From 578b4d2118efa2d471eb0bf8f75f625533bc5758 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 10:12:37 +0000 Subject: [PATCH 106/115] [mobile] Skip failing Android localhost subdomain DNS tests (#127742) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes failing `System.Net.NameResolution` tests on Android by adding `ActiveIssue` attributes to skip tests that expect localhost subdomain resolution to return loopback addresses. ## Details On Android, DNS resolution for `.localhost` subdomains (e.g., `foo.localhost`, `test.localhost`) returns link-local IPv6 addresses (`fe80::/10`) instead of loopback addresses, causing test failures. ### Tests Fixed - `DnsGetHostEntry_LocalhostSubdomain_ReturnsLoopback` - `DnsGetHostAddresses_LocalhostSubdomain_ReturnsLoopback` These tests are now skipped on Android with reference to existing issue #124751, which already tracks the same underlying problem for the `RespectsAddressFamily` variants of these tests. ### Build Information - **Build**: [1406427](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1406427) - **Date**: 2026-05-03 - **Job**: android-arm Release AllSubsets_Mono - **Helix Job**: 6255e58e-bf4f-4c25-ba48-27c957d4ea3e - **Work Item**: System.Net.NameResolution.Functional.Tests ### Console Log Excerpt (sanitized) ``` [20:56:01] info: Instrumentation finished normally with exit code 1 [FAIL] System.Net.NameResolution.Tests.GetHostAddressesTest.DnsGetHostAddresses_LocalhostSubdomain_ReturnsLoopback(hostName: "foo.localhost") Assert.All() Failure: 2 out of 3 items in the collection did not pass. Error: Expected loopback address but got: fe80::cb7c:d6fd:eb1f:6f44%10 Error: Expected loopback address but got: fe80::b4f2:efff:fea1:dbef%3 [FAIL] System.Net.NameResolution.Tests.GetHostAddressesTest.DnsGetHostAddresses_LocalhostSubdomain_ReturnsLoopback(hostName: "test.localhost") Assert.All() Failure: 2 out of 3 items in the collection did not pass. Error: Expected loopback address but got: fe80::cb7c:d6fd:eb1f:6f44%10 Error: Expected loopback address but got: fe80::b4f2:efff:fea1:dbef%3 ``` The tests receive link-local IPv6 addresses instead of the expected loopback addresses (127.0.0.1 or ::1). ### Root Cause Android's DNS resolver behavior for RFC 6761 localhost subdomains differs from desktop platforms. When the OS resolver fallback tries to resolve plain "localhost" with or without an address family filter, Android may return the device's network interface addresses instead of loopback addresses. ### Related Issue Closes: #124751 > [!NOTE] > This content was generated by GitHub Copilot and may contain AI-generated content. > [!NOTE] >
> 🔒 Integrity filter blocked 2 items > > The following items were blocked because they don't meet the GitHub integrity level. > > - [#19443](https://github.com/dotnet/runtime/issues/19443) `search_issues`: has lower integrity than agent requires. The agent cannot read data with integrity below "approved". > - [#126805](https://github.com/dotnet/runtime/pull/126805) `search_pull_requests`: has lower integrity than agent requires. The agent cannot read data with integrity below "approved". > > To allow these resources, lower `min-integrity` in your GitHub frontmatter: > > ```yaml > tools: > github: > min-integrity: approved # merged | approved | unapproved | none > ``` > >
> Generated by [Mobile Platform Failure Scanner](https://github.com/dotnet/runtime/actions/runs/25316071096/agentic_workflow) · ● 3.6M · [◷](https://github.com/search?q=repo%3Adotnet%2Fruntime+%22gh-aw-workflow-id%3A+mobile-scan%22&type=pullrequests) --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: Milos Kotlar Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../tests/FunctionalTests/GetHostAddressesTest.cs | 1 + .../tests/FunctionalTests/GetHostEntryTest.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostAddressesTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostAddressesTest.cs index 3273ebc49cffc7..ed65832403c3f4 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostAddressesTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostAddressesTest.cs @@ -231,6 +231,7 @@ public async Task DnsGetHostAddresses_InvalidDomain_ThrowsHostNotFound(string ho [InlineData("test.localhost")] [InlineData("FOO.LOCALHOST")] [InlineData("Test.LocalHost")] + [ActiveIssue("https://github.com/dotnet/runtime/issues/126456", TestPlatforms.Android)] public async Task DnsGetHostAddresses_LocalhostSubdomain_ReturnsLoopback(string hostName) { // The subdomain goes to OS resolver first. If it fails (likely on most systems), diff --git a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs index 6403b7dcb9195e..616c2c6f4bd9a4 100644 --- a/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs +++ b/src/libraries/System.Net.NameResolution/tests/FunctionalTests/GetHostEntryTest.cs @@ -353,6 +353,7 @@ public async Task DnsGetHostEntry_InvalidDomain_ThrowsHostNotFound(string hostNa [InlineData("test.localhost")] [InlineData("FOO.LOCALHOST")] [InlineData("Test.LocalHost")] + [ActiveIssue("https://github.com/dotnet/runtime/issues/126456", TestPlatforms.Android)] public async Task DnsGetHostEntry_LocalhostSubdomain_ReturnsLoopback(string hostName) { // The subdomain goes to OS resolver first. If it fails (likely on most systems), From 004c358f4376950ead5ef31b95bf4f91a725b42a Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Tue, 5 May 2026 14:34:03 +0200 Subject: [PATCH 107/115] [browser][CoreCLR] Implement `invokeLibraryInitializers` for JS initializer hooks (#127551) --- .../LibraryInitializerTests.cs | 13 ++++++ .../wwwroot/WasmBasicTestApp.lib.module.js | 4 ++ .../WasmBasicTestApp/App/wwwroot/main.js | 7 +++- .../libs/Common/JavaScript/loader/assets.ts | 10 ++--- .../JavaScript/loader/lib-initializers.ts | 40 +++++++++++++++++-- 5 files changed, 65 insertions(+), 9 deletions(-) diff --git a/src/mono/wasm/Wasm.Build.Tests/LibraryInitializerTests.cs b/src/mono/wasm/Wasm.Build.Tests/LibraryInitializerTests.cs index 2ef6e921a754a2..259f1a0a16c187 100644 --- a/src/mono/wasm/Wasm.Build.Tests/LibraryInitializerTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/LibraryInitializerTests.cs @@ -64,4 +64,17 @@ public async Task AbortStartupOnError() result.ConsoleOutput.Any(m => expectedRegex.IsMatch(m)), $"The library initializer test didn't emit expected error message.\nConsole output:\n{string.Join("\n", result.ConsoleOutput)}"); } + + [Fact, TestCategory("bundler-friendly")] + public async Task InvokeLibraryInitializersApi() + { + Configuration config = Configuration.Debug; + ProjectInfo info = CopyTestAsset(config, false, TestAsset.WasmBasicTestApp, "LibraryInitializerTests_InvokeLibraryInitializersApi"); + PublishProject(info, config); + RunResult result = await RunForPublishWithWebServer(new BrowserRunOptions(config, TestScenario: "InvokeLibraryInitializersTest")); + Assert.Collection( + result.TestOutput, + m => Assert.Equal("customHookCalled=true", m) + ); + } } diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/WasmBasicTestApp.lib.module.js b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/WasmBasicTestApp.lib.module.js index f0a78e2efb3185..9d9fcdb72a6b99 100644 --- a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/WasmBasicTestApp.lib.module.js +++ b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/WasmBasicTestApp.lib.module.js @@ -23,4 +23,8 @@ export async function onRuntimeReady({ getAssemblyExports, getConfig }) { exports.LibraryInitializerTest.Run(); } +} + +export function customHook() { + globalThis.__customHookCalled = true; } \ No newline at end of file diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js index b7349232ba97f6..57f299cc3310bc 100644 --- a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js +++ b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js @@ -218,7 +218,7 @@ switch (testCase) { break; } -const { setModuleImports, Module, getAssemblyExports, getConfig, INTERNAL } = await dotnet.create(); +const { setModuleImports, Module, getAssemblyExports, getConfig, INTERNAL, invokeLibraryInitializers } = await dotnet.create(); const config = getConfig(); const exports = await getAssemblyExports(config.mainAssemblyName); const assemblyExtension = Object.keys(config.resources.coreAssembly)[0].endsWith('.wasm') ? ".wasm" : ".dll"; @@ -263,6 +263,11 @@ try { case "LibraryInitializerTest": exit(0); break; + case "InvokeLibraryInitializersTest": + await invokeLibraryInitializers("customHook", []); + testOutput(`customHookCalled=${globalThis.__customHookCalled === true}`); + exit(0); + break; case "ZipArchiveInteropTest": exports.ZipArchiveInteropTest.Run(); exit(0); diff --git a/src/native/libs/Common/JavaScript/loader/assets.ts b/src/native/libs/Common/JavaScript/loader/assets.ts index 5fa921c55be5da..35e57c750359b0 100644 --- a/src/native/libs/Common/JavaScript/loader/assets.ts +++ b/src/native/libs/Common/JavaScript/loader/assets.ts @@ -33,7 +33,10 @@ export async function loadDotnetModule(asset: JsAsset): Promise export async function loadJSModule(asset: JsAsset): Promise { const assetInternal = asset as AssetEntryInternal; - let mod: JsModuleExports = asset.moduleExports; + let mod: JsModuleExports = await asset.moduleExports; + if (mod) { + asset.moduleExports = mod; + } totalAssetsToDownload++; if (!mod) { if (assetInternal.name && !asset.resolvedUrl) { @@ -50,6 +53,7 @@ export async function loadJSModule(asset: JsAsset): Promise { if (!asset.resolvedUrl) throw new Error("Invalid config, resources is not set"); mod = await import(/* webpackIgnore: true */ asset.resolvedUrl); + asset.moduleExports = mod; } onDownloadedAsset(assetInternal); return mod; @@ -61,8 +65,6 @@ export async function callLibraryInitializerOnRuntimeConfigLoaded(asset: JsAsset try { if (typeof module.onRuntimeConfigLoaded === "function") { await module.onRuntimeConfigLoaded(loaderConfig); - } else if (typeof module.onRuntimeReady !== "function") { - dotnetLogger.warn(`Module '${name}' does not export 'onRuntimeConfigLoaded' function. Make sure the module initializer is correctly defined and exported.`); } return module; } catch (err) { @@ -77,8 +79,6 @@ export async function callLibraryInitializerOnRuntimeReady([asset, modulePromise try { if (typeof module.onRuntimeReady === "function") { await module.onRuntimeReady(dotnetApi); - } else if (typeof module.onRuntimeConfigLoaded !== "function") { - dotnetLogger.warn(`Module '${name}' does not export 'onRuntimeReady' function. Make sure the module initializer is correctly defined and exported.`); } } catch (err) { const message = err instanceof Error ? err.message : String(err); diff --git a/src/native/libs/Common/JavaScript/loader/lib-initializers.ts b/src/native/libs/Common/JavaScript/loader/lib-initializers.ts index 3541d2ec95ef20..48e21f3b494e70 100644 --- a/src/native/libs/Common/JavaScript/loader/lib-initializers.ts +++ b/src/native/libs/Common/JavaScript/loader/lib-initializers.ts @@ -1,8 +1,42 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { loaderConfig } from "./config"; +import { dotnetLogger } from "./cross-module"; +import { exit } from "./exit"; + export async function invokeLibraryInitializers(functionName: string, args: any[]): Promise { - // functionName: "onRuntimeReady", "onRuntimeConfigLoaded" - throw new Error("Not implemented"); + const allModules = [ + ...loaderConfig.resources?.modulesAfterConfigLoaded ?? [], + ...loaderConfig.resources?.modulesAfterRuntimeReady ?? [], + ]; + + const promises: Promise[] = []; + for (const asset of allModules) { + promises.push(invokeWithErrorHandling(asset, functionName, args)); + } + await Promise.all(promises); +} + +async function invokeWithErrorHandling( + asset: { moduleExports?: any; name?: string; resolvedUrl?: string }, + functionName: string, args: any[] +): Promise { + try { + const mod = await asset.moduleExports; + if (mod) { + asset.moduleExports = mod; + } + if (mod && typeof mod[functionName] === "function") { + await mod[functionName](...args); + } + } catch (err) { + const name = asset.name || asset.resolvedUrl || "unknown"; + const message = err instanceof Error ? err.message : String(err); + dotnetLogger.warn( + `Failed to invoke '${functionName}' on library initializer '${name}': ${message}` + ); + exit(1, err); + throw err; + } } From 7018450b196e52b63064391b6c0df303d6604ae9 Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Tue, 5 May 2026 17:37:03 +0200 Subject: [PATCH 108/115] [wasm][coreclr] Cache calli cookies (#127016) Avoid repeated expensive calls to get calli cookie by caching it Checked in HelloWorld ``` CalliCookie cache: 18 hits, 17 misses, 35 total (51.4% hit rate) ``` --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jan Kotas Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/coreclr/vm/interpexec.cpp | 31 ++-- src/coreclr/vm/interpexec.h | 4 +- src/coreclr/vm/jitinterface.cpp | 43 +++-- src/coreclr/vm/method.cpp | 12 +- src/coreclr/vm/method.hpp | 14 +- src/coreclr/vm/precode_portable.hpp | 4 +- .../vm/wasm/callhelpers-interp-to-managed.cpp | 156 +++++++++--------- src/coreclr/vm/wasm/callhelpers-pinvoke.cpp | 4 +- src/coreclr/vm/wasm/helpers.cpp | 32 ++-- .../coreclr/InterpToNativeGenerator.cs | 7 +- 10 files changed, 172 insertions(+), 135 deletions(-) diff --git a/src/coreclr/vm/interpexec.cpp b/src/coreclr/vm/interpexec.cpp index e3bd37659a2d57..2196c2b73acb4d 100644 --- a/src/coreclr/vm/interpexec.cpp +++ b/src/coreclr/vm/interpexec.cpp @@ -206,9 +206,9 @@ static size_t CreateDispatchTokenForMethod(MethodDesc* pMD) void InvokeManagedMethod(ManagedMethodParam *pParam); void InvokeUnmanagedMethod(MethodDesc *targetMethod, int8_t *pArgs, int8_t *pRet, PCODE callTarget); void InvokeCalliStub(CalliStubParam* pParam); -void InvokeUnmanagedCalli(PCODE ftn, void *cookie, int8_t *pArgs, int8_t *pRet); +void InvokeUnmanagedCalli(PCODE ftn, InterpreterCalliCookie cookie, int8_t *pArgs, int8_t *pRet); void InvokeDelegateInvokeMethod(DelegateInvokeMethodParam* pParam); -void* GetCookieForCalliSig(MetaSig metaSig, MethodDesc *pContextMD); +InterpreterCalliCookie GetCookieForCalliSig(MetaSig metaSig, MethodDesc *pContextMD); extern "C" PCODE CID_VirtualOpenDelegateDispatch(TransitionBlock * pTransitionBlock); // Filter to ignore SEH exceptions representing C++ exceptions. @@ -287,7 +287,7 @@ void InvokeUnmanagedMethodWithTransition(UnmanagedMethodWithTransitionParam *pPa } NOINLINE -void InvokeUnmanagedCalliWithTransition(PCODE ftn, void *cookie, int8_t *stack, InterpMethodContextFrame *pFrame, int8_t *pArgs, int8_t *pRet) +void InvokeUnmanagedCalliWithTransition(PCODE ftn, InterpreterCalliCookie cookie, int8_t *stack, InterpMethodContextFrame *pFrame, int8_t *pArgs, int8_t *pRet) { CONTRACTL { @@ -349,7 +349,7 @@ static CallStubHeader *UpdateCallStubForMethod(MethodDesc *pMD, PCODE target) header->SetTarget(target); } - if (pMD->SetCallStub(header)) + if (pMD->SetCalliCookie(header)) { amTracker.SuppressRelease(); } @@ -357,7 +357,7 @@ static CallStubHeader *UpdateCallStubForMethod(MethodDesc *pMD, PCODE target) { // We have lost the race for generating the header, use the one that was generated by another thread // and let the amTracker release the memory of the one we generated. - header = pMD->GetCallStub(); + header = pMD->GetCalliCookie(); } return header; @@ -408,7 +408,7 @@ void InvokeManagedMethod(ManagedMethodParam* pParam) PCODE target = pParam->target; Object** pContinuationRet = pParam->pContinuationRet; - CallStubHeader *pHeader = pParam->pMD->GetCallStub(); + CallStubHeader *pHeader = pParam->pMD->GetCalliCookie(); if (pHeader == NULL) { pHeader = UpdateCallStubForMethod(pMD, target == (PCODE)NULL ? pMD->GetMultiCallableAddrOfCode(CORINFO_ACCESS_ANY) : target); @@ -467,7 +467,7 @@ void InvokeDelegateInvokeMethod(DelegateInvokeMethodParam* pParam) PCODE target = pParam->target; Object** pContinuationRet = pParam->pContinuationRet; - CallStubHeader *stubHeaderTemplate = pMDDelegateInvoke->GetCallStub(); + CallStubHeader *stubHeaderTemplate = pMDDelegateInvoke->GetCalliCookie(); if (stubHeaderTemplate == NULL) { stubHeaderTemplate = UpdateCallStubForMethod(pMDDelegateInvoke, (PCODE)pMDDelegateInvoke->GetMultiCallableAddrOfCode(CORINFO_ACCESS_ANY)); @@ -490,7 +490,7 @@ void InvokeDelegateInvokeMethod(DelegateInvokeMethodParam* pParam) pHeader->Invoke(pHeader->Routines, pArgs, pRet, pHeader->TotalStackSize, pContinuationRet); } -void InvokeUnmanagedCalli(PCODE ftn, void *cookie, int8_t *pArgs, int8_t *pRet) +void InvokeUnmanagedCalli(PCODE ftn, InterpreterCalliCookie cookie, int8_t *pArgs, int8_t *pRet) { CONTRACTL { @@ -549,7 +549,7 @@ void InvokeCalliStub(CalliStubParam* pParam) pHeader->Invoke(pHeader->Routines, pArgs, pRet, pHeader->TotalStackSize, pContinuationRet); } -void* GetCookieForCalliSig(MetaSig metaSig, MethodDesc *pContextMD) +InterpreterCalliCookie GetCookieForCalliSig(MetaSig metaSig, MethodDesc *pContextMD) { STANDARD_VM_CONTRACT; @@ -3162,7 +3162,7 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr int32_t calliCookie = ip[4]; int32_t flags = ip[5]; - void* cookie = pMethod->pDataItems[calliCookie]; + InterpreterCalliCookie cookie = (InterpreterCalliCookie)pMethod->pDataItems[calliCookie]; ip += 6; // Save current execution state for when we return from called method @@ -3206,8 +3206,15 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr if (PortableEntryPoint::PrefersInterpreterEntryPoint(calliFunctionPointer) || !PortableEntryPoint::HasNativeEntryPoint(calliFunctionPointer)) goto CALL_INTERP_METHOD; - MetaSig sig(targetMethod); - cookie = GetCookieForCalliSig(sig, NULL); + cookie = targetMethod->GetCalliCookie(); + if (cookie == NULL) + { + MetaSig sig(targetMethod); + cookie = GetCookieForCalliSig(sig, NULL); + _ASSERTE(cookie != NULL); + targetMethod->SetCalliCookie(cookie); + cookie = targetMethod->GetCalliCookie(); + } #endif // FEATURE_PORTABLE_ENTRYPOINTS frameNeedsTailcallUpdate = false; CalliStubParam param = { calliFunctionPointer, cookie, callArgsAddress, returnValueAddress, pInterpreterFrame->GetContinuationPtr() }; diff --git a/src/coreclr/vm/interpexec.h b/src/coreclr/vm/interpexec.h index 9f234145069595..1192ddadf0cdbe 100644 --- a/src/coreclr/vm/interpexec.h +++ b/src/coreclr/vm/interpexec.h @@ -132,14 +132,16 @@ struct ManagedMethodParam void InvokeManagedMethod(ManagedMethodParam *pParam); +#ifdef FEATURE_INTERPRETER struct CalliStubParam { PCODE ftn; - void* cookie; + InterpreterCalliCookie cookie; int8_t *pArgs; int8_t *pRet; Object** pContinuationRet; }; +#endif // FEATURE_INTERPRETER struct DelegateInvokeMethodParam { diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index d06b57ed47c5b8..cb30fa87c3aaec 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -11491,25 +11491,13 @@ LPVOID CEEInfo::GetCookieForInterpreterCalliSig(CORINFO_SIG_INFO* szMetaSig) #ifdef FEATURE_INTERPRETER // Forward declare the function for mapping MetaSig to a cookie. -void* GetCookieForCalliSig(MetaSig metaSig, MethodDesc *pContextMD); +InterpreterCalliCookie GetCookieForCalliSig(MetaSig metaSig, MethodDesc *pContextMD); LPVOID CInterpreterJitInfo::GetCookieForInterpreterCalliSig(CORINFO_SIG_INFO* szMetaSig) { - void* result = NULL; + InterpreterCalliCookie result = NULL; JIT_TO_EE_TRANSITION(); - Instantiation classInst = Instantiation((TypeHandle*) szMetaSig->sigInst.classInst, szMetaSig->sigInst.classInstCount); - Instantiation methodInst = Instantiation((TypeHandle*) szMetaSig->sigInst.methInst, szMetaSig->sigInst.methInstCount); - SigTypeContext typeContext = SigTypeContext(classInst, methodInst); - Module* mod = GetModule(szMetaSig->scope); - - MetaSig sig(szMetaSig->pSig, szMetaSig->cbSig, mod, &typeContext); - - if (szMetaSig->isAsyncCall()) - sig.SetIsAsyncCall(); - - _ASSERTE(szMetaSig->isAsyncCall() == sig.IsAsyncCall()); - // When compiling a calli inside an IL stub for a P/Invoke, pass the target // P/Invoke MethodDesc so ComputeCallStub can detect the Swift calling convention. MethodDesc* pContextMD = nullptr; @@ -11519,12 +11507,35 @@ LPVOID CInterpreterJitInfo::GetCookieForInterpreterCalliSig(CORINFO_SIG_INFO* sz if (pTargetMD != nullptr) { pContextMD = pTargetMD; + result = pTargetMD->GetCalliCookie(); + } + } + + if (result == NULL) + { + Instantiation classInst = Instantiation((TypeHandle*) szMetaSig->sigInst.classInst, szMetaSig->sigInst.classInstCount); + Instantiation methodInst = Instantiation((TypeHandle*) szMetaSig->sigInst.methInst, szMetaSig->sigInst.methInstCount); + SigTypeContext typeContext = SigTypeContext(classInst, methodInst); + Module* mod = GetModule(szMetaSig->scope); + + MetaSig sig(szMetaSig->pSig, szMetaSig->cbSig, mod, &typeContext); + + if (szMetaSig->isAsyncCall()) + sig.SetIsAsyncCall(); + + _ASSERTE(szMetaSig->isAsyncCall() == sig.IsAsyncCall()); + + result = GetCookieForCalliSig(sig, pContextMD); + + if (pContextMD != nullptr) + { + pContextMD->SetCalliCookie(result); + result = pContextMD->GetCalliCookie(); } } - result = GetCookieForCalliSig(sig, pContextMD); EE_TO_JIT_TRANSITION(); - return result; + return (void*)result; } void CInterpreterJitInfo::allocMem(AllocMemArgs *pArgs) diff --git a/src/coreclr/vm/method.cpp b/src/coreclr/vm/method.cpp index 23c355e8534bdc..ca4bf5c4a87e22 100644 --- a/src/coreclr/vm/method.cpp +++ b/src/coreclr/vm/method.cpp @@ -300,26 +300,26 @@ PatchpointInfo* MethodDesc::GetMethodDescAltJitPatchpointInfo() #endif // FEATURE_CODE_VERSIONING #ifdef FEATURE_INTERPRETER -// Set the call stub for the interpreter to JIT/AOT calls -// Returns true if the current call set the stub, false if it was already set -bool MethodDesc::SetCallStub(CallStubHeader *pHeader) +// Cache the calli cookie on the MethodDesc +// Returns true if the current call set the cookie, false if it was already set +bool MethodDesc::SetCalliCookie(InterpreterCalliCookie cookie) { STANDARD_VM_CONTRACT; IfFailThrow(EnsureCodeDataExists(NULL)); _ASSERTE(m_codeData != NULL); - return InterlockedCompareExchangeT(&m_codeData->CallStub, pHeader, NULL) == NULL; + return InterlockedCompareExchangeT((void**)&m_codeData->CalliCookie, (void*)cookie, (void*)NULL) == NULL; } -CallStubHeader *MethodDesc::GetCallStub() +InterpreterCalliCookie MethodDesc::GetCalliCookie() { LIMITED_METHOD_CONTRACT; PTR_MethodDescCodeData codeData = VolatileLoadWithoutBarrier(&m_codeData); if (codeData == NULL) return NULL; - return VolatileLoadWithoutBarrier(&codeData->CallStub); + return (InterpreterCalliCookie)VolatileLoadWithoutBarrier(&codeData->CalliCookie); } #endif // FEATURE_INTERPRETER diff --git a/src/coreclr/vm/method.hpp b/src/coreclr/vm/method.hpp index 43368727b1fa47..38bc47b364c01e 100644 --- a/src/coreclr/vm/method.hpp +++ b/src/coreclr/vm/method.hpp @@ -249,6 +249,14 @@ enum MethodDescFlags }; // Used for storing additional items related to native code +#ifdef FEATURE_INTERPRETER +#ifdef FEATURE_PORTABLE_ENTRYPOINTS +typedef void(*InterpreterCalliCookie)(PCODE, int8_t*, int8_t*); +#else +typedef CallStubHeader* InterpreterCalliCookie; +#endif // FEATURE_PORTABLE_ENTRYPOINTS +#endif // FEATURE_INTERPRETER + struct MethodDescCodeData final { #ifdef FEATURE_CODE_VERSIONING @@ -257,7 +265,7 @@ struct MethodDescCodeData final #endif // FEATURE_CODE_VERSIONING PCODE TemporaryEntryPoint; #ifdef FEATURE_INTERPRETER - CallStubHeader *CallStub; + InterpreterCalliCookie CalliCookie; #endif // FEATURE_INTERPRETER #if defined(_DEBUG) && defined(ALLOW_SXS_JIT) PatchpointInfo *AltJitPatchpointInfo; @@ -1976,8 +1984,8 @@ class MethodDesc #endif //!DACCESS_COMPILE #if defined(FEATURE_INTERPRETER) && !defined(DACCESS_COMPILE) - bool SetCallStub(CallStubHeader *pHeader); - CallStubHeader *GetCallStub(); + bool SetCalliCookie(InterpreterCalliCookie cookie); + InterpreterCalliCookie GetCalliCookie(); #endif // FEATURE_INTERPRETER && !DACCESS_COMPILE #ifdef FEATURE_CODE_VERSIONING diff --git a/src/coreclr/vm/precode_portable.hpp b/src/coreclr/vm/precode_portable.hpp index 6603fd907e3a2a..a1761950e95cd0 100644 --- a/src/coreclr/vm/precode_portable.hpp +++ b/src/coreclr/vm/precode_portable.hpp @@ -116,12 +116,14 @@ class PortableEntryPoint final friend struct ::cdac_data; }; + template<> struct cdac_data { static constexpr size_t MethodDesc = offsetof(PortableEntryPoint, _pMD); -}; + static_assert(offsetof(PortableEntryPoint, _pActualCode) == 0, "CLR ABI requires _pActualCode to be at offset 0 of PortableEntryPoint"); +}; extern InterleavedLoaderHeapConfig s_stubPrecodeHeapConfig; diff --git a/src/coreclr/vm/wasm/callhelpers-interp-to-managed.cpp b/src/coreclr/vm/wasm/callhelpers-interp-to-managed.cpp index c0265475c3447d..7a87bbde0e76b8 100644 --- a/src/coreclr/vm/wasm/callhelpers-interp-to-managed.cpp +++ b/src/coreclr/vm/wasm/callhelpers-interp-to-managed.cpp @@ -20,32 +20,32 @@ namespace { - NOINLINE static void CallFunc_F64_F64_F64_RetF64_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_F64_F64_F64_RetF64_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - double (*fptr)(int*, double, double, double, PCODE) = (double (*)(int*, double, double, double, PCODE))pcode; - *((double*)pRet) = (*fptr)(&framePointer, ARG_F64(0), ARG_F64(1), ARG_F64(2), pPortableEntryPointContext); + double (*fptr)(int*, double, double, double, PCODE) = *(double (**)(int*, double, double, double, PCODE))(pPortableEntryPoint); + *((double*)pRet) = (*fptr)(&framePointer, ARG_F64(0), ARG_F64(1), ARG_F64(2), pPortableEntryPoint); } - NOINLINE static void CallFunc_F64_F64_RetF64_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_F64_F64_RetF64_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - double (*fptr)(int*, double, double, PCODE) = (double (*)(int*, double, double, PCODE))pcode; - *((double*)pRet) = (*fptr)(&framePointer, ARG_F64(0), ARG_F64(1), pPortableEntryPointContext); + double (*fptr)(int*, double, double, PCODE) = *(double (**)(int*, double, double, PCODE))(pPortableEntryPoint); + *((double*)pRet) = (*fptr)(&framePointer, ARG_F64(0), ARG_F64(1), pPortableEntryPoint); } - NOINLINE static void CallFunc_F64_I32_RetF64_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_F64_I32_RetF64_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - double (*fptr)(int*, double, int32_t, PCODE) = (double (*)(int*, double, int32_t, PCODE))pcode; - *((double*)pRet) = (*fptr)(&framePointer, ARG_F64(0), ARG_I32(1), pPortableEntryPointContext); + double (*fptr)(int*, double, int32_t, PCODE) = *(double (**)(int*, double, int32_t, PCODE))(pPortableEntryPoint); + *((double*)pRet) = (*fptr)(&framePointer, ARG_F64(0), ARG_I32(1), pPortableEntryPoint); } - NOINLINE static void CallFunc_F64_RetF64_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_F64_RetF64_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - double (*fptr)(int*, double, PCODE) = (double (*)(int*, double, PCODE))pcode; - *((double*)pRet) = (*fptr)(&framePointer, ARG_F64(0), pPortableEntryPointContext); + double (*fptr)(int*, double, PCODE) = *(double (**)(int*, double, PCODE))(pPortableEntryPoint); + *((double*)pRet) = (*fptr)(&framePointer, ARG_F64(0), pPortableEntryPoint); } static void CallFunc_I32_RetF64(PCODE pcode, int8_t* pArgs, int8_t* pRet) @@ -54,32 +54,32 @@ namespace *((double*)pRet) = (*fptr)(ARG_I32(0)); } - NOINLINE static void CallFunc_F32_F32_F32_RetF32_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_F32_F32_F32_RetF32_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - float (*fptr)(int*, float, float, float, PCODE) = (float (*)(int*, float, float, float, PCODE))pcode; - *((float*)pRet) = (*fptr)(&framePointer, ARG_F32(0), ARG_F32(1), ARG_F32(2), pPortableEntryPointContext); + float (*fptr)(int*, float, float, float, PCODE) = *(float (**)(int*, float, float, float, PCODE))(pPortableEntryPoint); + *((float*)pRet) = (*fptr)(&framePointer, ARG_F32(0), ARG_F32(1), ARG_F32(2), pPortableEntryPoint); } - NOINLINE static void CallFunc_F32_F32_RetF32_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_F32_F32_RetF32_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - float (*fptr)(int*, float, float, PCODE) = (float (*)(int*, float, float, PCODE))pcode; - *((float*)pRet) = (*fptr)(&framePointer, ARG_F32(0), ARG_F32(1), pPortableEntryPointContext); + float (*fptr)(int*, float, float, PCODE) = *(float (**)(int*, float, float, PCODE))(pPortableEntryPoint); + *((float*)pRet) = (*fptr)(&framePointer, ARG_F32(0), ARG_F32(1), pPortableEntryPoint); } - NOINLINE static void CallFunc_F32_I32_RetF32_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_F32_I32_RetF32_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - float (*fptr)(int*, float, int32_t, PCODE) = (float (*)(int*, float, int32_t, PCODE))pcode; - *((float*)pRet) = (*fptr)(&framePointer, ARG_F32(0), ARG_I32(1), pPortableEntryPointContext); + float (*fptr)(int*, float, int32_t, PCODE) = *(float (**)(int*, float, int32_t, PCODE))(pPortableEntryPoint); + *((float*)pRet) = (*fptr)(&framePointer, ARG_F32(0), ARG_I32(1), pPortableEntryPoint); } - NOINLINE static void CallFunc_F32_RetF32_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_F32_RetF32_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - float (*fptr)(int*, float, PCODE) = (float (*)(int*, float, PCODE))pcode; - *((float*)pRet) = (*fptr)(&framePointer, ARG_F32(0), pPortableEntryPointContext); + float (*fptr)(int*, float, PCODE) = *(float (**)(int*, float, PCODE))(pPortableEntryPoint); + *((float*)pRet) = (*fptr)(&framePointer, ARG_F32(0), pPortableEntryPoint); } static void CallFunc_Void_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) @@ -136,25 +136,25 @@ namespace *((int32_t*)pRet) = (*fptr)(ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4), ARG_I32(5), ARG_I32(6), ARG_I32(7), ARG_I32(8), ARG_I32(9), ARG_I32(10), ARG_I32(11), ARG_I32(12), ARG_I32(13)); } - NOINLINE static void CallFunc_I32_I32_I32_I32_I32_I32_RetI32_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_I32_I32_I32_I32_I32_I32_RetI32_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - int32_t (*fptr)(int*, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, PCODE) = (int32_t (*)(int*, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, PCODE))pcode; - *((int32_t*)pRet) = (*fptr)(&framePointer, ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4), ARG_I32(5), pPortableEntryPointContext); + int32_t (*fptr)(int*, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, PCODE) = *(int32_t (**)(int*, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, PCODE))(pPortableEntryPoint); + *((int32_t*)pRet) = (*fptr)(&framePointer, ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4), ARG_I32(5), pPortableEntryPoint); } - NOINLINE static void CallFunc_I32_I32_I32_I32_I32_RetI32_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_I32_I32_I32_I32_I32_RetI32_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - int32_t (*fptr)(int*, int32_t, int32_t, int32_t, int32_t, int32_t, PCODE) = (int32_t (*)(int*, int32_t, int32_t, int32_t, int32_t, int32_t, PCODE))pcode; - *((int32_t*)pRet) = (*fptr)(&framePointer, ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4), pPortableEntryPointContext); + int32_t (*fptr)(int*, int32_t, int32_t, int32_t, int32_t, int32_t, PCODE) = *(int32_t (**)(int*, int32_t, int32_t, int32_t, int32_t, int32_t, PCODE))(pPortableEntryPoint); + *((int32_t*)pRet) = (*fptr)(&framePointer, ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), ARG_I32(4), pPortableEntryPoint); } - NOINLINE static void CallFunc_I32_I32_I32_I32_RetI32_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_I32_I32_I32_I32_RetI32_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - int32_t (*fptr)(int*, int32_t, int32_t, int32_t, int32_t, PCODE) = (int32_t (*)(int*, int32_t, int32_t, int32_t, int32_t, PCODE))pcode; - *((int32_t*)pRet) = (*fptr)(&framePointer, ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), pPortableEntryPointContext); + int32_t (*fptr)(int*, int32_t, int32_t, int32_t, int32_t, PCODE) = *(int32_t (**)(int*, int32_t, int32_t, int32_t, int32_t, PCODE))(pPortableEntryPoint); + *((int32_t*)pRet) = (*fptr)(&framePointer, ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_I32(3), pPortableEntryPoint); } static void CallFunc_I32_I32_I32_I64_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) @@ -169,11 +169,11 @@ namespace *((int32_t*)pRet) = (*fptr)(ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_IND(3), ARG_I32(4)); } - NOINLINE static void CallFunc_I32_I32_I32_RetI32_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_I32_I32_I32_RetI32_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - int32_t (*fptr)(int*, int32_t, int32_t, int32_t, PCODE) = (int32_t (*)(int*, int32_t, int32_t, int32_t, PCODE))pcode; - *((int32_t*)pRet) = (*fptr)(&framePointer, ARG_I32(0), ARG_I32(1), ARG_I32(2), pPortableEntryPointContext); + int32_t (*fptr)(int*, int32_t, int32_t, int32_t, PCODE) = *(int32_t (**)(int*, int32_t, int32_t, int32_t, PCODE))(pPortableEntryPoint); + *((int32_t*)pRet) = (*fptr)(&framePointer, ARG_I32(0), ARG_I32(1), ARG_I32(2), pPortableEntryPoint); } static void CallFunc_I32_I32_I64_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) @@ -194,11 +194,11 @@ namespace *((int32_t*)pRet) = (*fptr)(ARG_I32(0), ARG_I32(1), ARG_IND(2), ARG_I32(3), ARG_I32(4), ARG_IND(5)); } - NOINLINE static void CallFunc_I32_I32_RetI32_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_I32_I32_RetI32_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - int32_t (*fptr)(int*, int32_t, int32_t, PCODE) = (int32_t (*)(int*, int32_t, int32_t, PCODE))pcode; - *((int32_t*)pRet) = (*fptr)(&framePointer, ARG_I32(0), ARG_I32(1), pPortableEntryPointContext); + int32_t (*fptr)(int*, int32_t, int32_t, PCODE) = *(int32_t (**)(int*, int32_t, int32_t, PCODE))(pPortableEntryPoint); + *((int32_t*)pRet) = (*fptr)(&framePointer, ARG_I32(0), ARG_I32(1), pPortableEntryPoint); } static void CallFunc_I32_I64_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) @@ -243,11 +243,11 @@ namespace *((int32_t*)pRet) = (*fptr)(ARG_I32(0), ARG_IND(1), ARG_I32(2), ARG_I32(3), ARG_I32(4)); } - NOINLINE static void CallFunc_I32_RetI32_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_I32_RetI32_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - int32_t (*fptr)(int*, int32_t, PCODE) = (int32_t (*)(int*, int32_t, PCODE))pcode; - *((int32_t*)pRet) = (*fptr)(&framePointer, ARG_I32(0), pPortableEntryPointContext); + int32_t (*fptr)(int*, int32_t, PCODE) = *(int32_t (**)(int*, int32_t, PCODE))(pPortableEntryPoint); + *((int32_t*)pRet) = (*fptr)(&framePointer, ARG_I32(0), pPortableEntryPoint); } static void CallFunc_I64_I32_I64_I32_RetI32(PCODE pcode, int8_t* pArgs, int8_t* pRet) @@ -328,11 +328,11 @@ namespace *((int32_t*)pRet) = (*fptr)(ARG_IND(0), ARG_IND(1), ARG_IND(2)); } - NOINLINE static void CallFunc_Void_RetI32_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_Void_RetI32_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - int32_t (*fptr)(int*, PCODE) = (int32_t (*)(int*, PCODE))pcode; - *((int32_t*)pRet) = (*fptr)(&framePointer, pPortableEntryPointContext); + int32_t (*fptr)(int*, PCODE) = *(int32_t (**)(int*, PCODE))(pPortableEntryPoint); + *((int32_t*)pRet) = (*fptr)(&framePointer, pPortableEntryPoint); } static void CallFunc_Void_RetI64(PCODE pcode, int8_t* pArgs, int8_t* pRet) @@ -365,39 +365,39 @@ namespace *((int64_t*)pRet) = (*fptr)(ARG_I32(0), ARG_I64(1), ARG_I32(2)); } - NOINLINE static void CallFunc_I32_I64_I64_RetI64_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_I32_I64_I64_RetI64_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - int64_t (*fptr)(int*, int32_t, int64_t, int64_t, PCODE) = (int64_t (*)(int*, int32_t, int64_t, int64_t, PCODE))pcode; - *((int64_t*)pRet) = (*fptr)(&framePointer, ARG_I32(0), ARG_I64(1), ARG_I64(2), pPortableEntryPointContext); + int64_t (*fptr)(int*, int32_t, int64_t, int64_t, PCODE) = *(int64_t (**)(int*, int32_t, int64_t, int64_t, PCODE))(pPortableEntryPoint); + *((int64_t*)pRet) = (*fptr)(&framePointer, ARG_I32(0), ARG_I64(1), ARG_I64(2), pPortableEntryPoint); } - NOINLINE static void CallFunc_I32_I64_RetI64_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_I32_I64_RetI64_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - int64_t (*fptr)(int*, int32_t, int64_t, PCODE) = (int64_t (*)(int*, int32_t, int64_t, PCODE))pcode; - *((int64_t*)pRet) = (*fptr)(&framePointer, ARG_I32(0), ARG_I64(1), pPortableEntryPointContext); + int64_t (*fptr)(int*, int32_t, int64_t, PCODE) = *(int64_t (**)(int*, int32_t, int64_t, PCODE))(pPortableEntryPoint); + *((int64_t*)pRet) = (*fptr)(&framePointer, ARG_I32(0), ARG_I64(1), pPortableEntryPoint); } - NOINLINE static void CallFunc_I32_RetI64_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_I32_RetI64_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - int64_t (*fptr)(int*, int32_t, PCODE) = (int64_t (*)(int*, int32_t, PCODE))pcode; - *((int64_t*)pRet) = (*fptr)(&framePointer, ARG_I32(0), pPortableEntryPointContext); + int64_t (*fptr)(int*, int32_t, PCODE) = *(int64_t (**)(int*, int32_t, PCODE))(pPortableEntryPoint); + *((int64_t*)pRet) = (*fptr)(&framePointer, ARG_I32(0), pPortableEntryPoint); } - NOINLINE static void CallFunc_I64_I64_RetI64_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_I64_I64_RetI64_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - int64_t (*fptr)(int*, int64_t, int64_t, PCODE) = (int64_t (*)(int*, int64_t, int64_t, PCODE))pcode; - *((int64_t*)pRet) = (*fptr)(&framePointer, ARG_I64(0), ARG_I64(1), pPortableEntryPointContext); + int64_t (*fptr)(int*, int64_t, int64_t, PCODE) = *(int64_t (**)(int*, int64_t, int64_t, PCODE))(pPortableEntryPoint); + *((int64_t*)pRet) = (*fptr)(&framePointer, ARG_I64(0), ARG_I64(1), pPortableEntryPoint); } - NOINLINE static void CallFunc_Void_RetI64_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_Void_RetI64_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - int64_t (*fptr)(int*, PCODE) = (int64_t (*)(int*, PCODE))pcode; - *((int64_t*)pRet) = (*fptr)(&framePointer, pPortableEntryPointContext); + int64_t (*fptr)(int*, PCODE) = *(int64_t (**)(int*, PCODE))(pPortableEntryPoint); + *((int64_t*)pRet) = (*fptr)(&framePointer, pPortableEntryPoint); } static void CallFunc_Void_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) @@ -406,18 +406,18 @@ namespace (*fptr)(); } - NOINLINE static void CallFunc_F64_I32_I32_RetVoid_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_F64_I32_I32_RetVoid_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - void (*fptr)(int*, double, int32_t, int32_t, PCODE) = (void (*)(int*, double, int32_t, int32_t, PCODE))pcode; - (*fptr)(&framePointer, ARG_F64(0), ARG_I32(1), ARG_I32(2), pPortableEntryPointContext); + void (*fptr)(int*, double, int32_t, int32_t, PCODE) = *(void (**)(int*, double, int32_t, int32_t, PCODE))(pPortableEntryPoint); + (*fptr)(&framePointer, ARG_F64(0), ARG_I32(1), ARG_I32(2), pPortableEntryPoint); } - NOINLINE static void CallFunc_F32_I32_I32_RetVoid_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_F32_I32_I32_RetVoid_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - void (*fptr)(int*, float, int32_t, int32_t, PCODE) = (void (*)(int*, float, int32_t, int32_t, PCODE))pcode; - (*fptr)(&framePointer, ARG_F32(0), ARG_I32(1), ARG_I32(2), pPortableEntryPointContext); + void (*fptr)(int*, float, int32_t, int32_t, PCODE) = *(void (**)(int*, float, int32_t, int32_t, PCODE))(pPortableEntryPoint); + (*fptr)(&framePointer, ARG_F32(0), ARG_I32(1), ARG_I32(2), pPortableEntryPoint); } static void CallFunc_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) @@ -468,11 +468,11 @@ namespace (*fptr)(ARG_I32(0), ARG_I32(1), ARG_I32(2), ARG_IND(3), ARG_IND(4), ARG_I32(5)); } - NOINLINE static void CallFunc_I32_I32_I32_RetVoid_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_I32_I32_I32_RetVoid_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - void (*fptr)(int*, int32_t, int32_t, int32_t, PCODE) = (void (*)(int*, int32_t, int32_t, int32_t, PCODE))pcode; - (*fptr)(&framePointer, ARG_I32(0), ARG_I32(1), ARG_I32(2), pPortableEntryPointContext); + void (*fptr)(int*, int32_t, int32_t, int32_t, PCODE) = *(void (**)(int*, int32_t, int32_t, int32_t, PCODE))(pPortableEntryPoint); + (*fptr)(&framePointer, ARG_I32(0), ARG_I32(1), ARG_I32(2), pPortableEntryPoint); } static void CallFunc_I32_I32_IND_IND_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) @@ -487,11 +487,11 @@ namespace (*fptr)(ARG_I32(0), ARG_I32(1), ARG_IND(2), ARG_IND(3), ARG_I32(4), ARG_I32(5)); } - NOINLINE static void CallFunc_I32_I32_RetVoid_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_I32_I32_RetVoid_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - void (*fptr)(int*, int32_t, int32_t, PCODE) = (void (*)(int*, int32_t, int32_t, PCODE))pcode; - (*fptr)(&framePointer, ARG_I32(0), ARG_I32(1), pPortableEntryPointContext); + void (*fptr)(int*, int32_t, int32_t, PCODE) = *(void (**)(int*, int32_t, int32_t, PCODE))(pPortableEntryPoint); + (*fptr)(&framePointer, ARG_I32(0), ARG_I32(1), pPortableEntryPoint); } static void CallFunc_I32_IND_I32_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) @@ -500,11 +500,11 @@ namespace (*fptr)(ARG_I32(0), ARG_IND(1), ARG_I32(2)); } - NOINLINE static void CallFunc_I32_RetVoid_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_I32_RetVoid_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - void (*fptr)(int*, int32_t, PCODE) = (void (*)(int*, int32_t, PCODE))pcode; - (*fptr)(&framePointer, ARG_I32(0), pPortableEntryPointContext); + void (*fptr)(int*, int32_t, PCODE) = *(void (**)(int*, int32_t, PCODE))(pPortableEntryPoint); + (*fptr)(&framePointer, ARG_I32(0), pPortableEntryPoint); } static void CallFunc_I64_RetVoid(PCODE pcode, int8_t* pArgs, int8_t* pRet) @@ -573,11 +573,11 @@ namespace (*fptr)(ARG_IND(0), ARG_IND(1), ARG_I32(2), ARG_I32(3), ARG_I32(4), ARG_I32(5), ARG_I32(6)); } - NOINLINE static void CallFunc_Void_RetVoid_PE(PCODE pcode, int8_t* pArgs, int8_t* pRet, PCODE pPortableEntryPointContext) + NOINLINE static void CallFunc_Void_RetVoid_PE(PCODE pPortableEntryPoint, int8_t* pArgs, int8_t* pRet) { alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK; - void (*fptr)(int*, PCODE) = (void (*)(int*, PCODE))pcode; - (*fptr)(&framePointer, pPortableEntryPointContext); + void (*fptr)(int*, PCODE) = *(void (**)(int*, PCODE))(pPortableEntryPoint); + (*fptr)(&framePointer, pPortableEntryPoint); } } diff --git a/src/coreclr/vm/wasm/callhelpers-pinvoke.cpp b/src/coreclr/vm/wasm/callhelpers-pinvoke.cpp index 2be5428681fe54..1c820b6d7b18ca 100644 --- a/src/coreclr/vm/wasm/callhelpers-pinvoke.cpp +++ b/src/coreclr/vm/wasm/callhelpers-pinvoke.cpp @@ -11,6 +11,7 @@ #include extern "C" { + uint32_t CompressionNative_CompressBound (uint32_t); uint32_t CompressionNative_Crc32 (uint32_t, void *, int32_t); int32_t CompressionNative_Deflate (void *, int32_t); int32_t CompressionNative_DeflateEnd (void *); @@ -192,6 +193,7 @@ static const Entry s_libSystem_Globalization_Native [] = { }; static const Entry s_libSystem_IO_Compression_Native [] = { + DllImportEntry(CompressionNative_CompressBound) // System.IO.Compression DllImportEntry(CompressionNative_Crc32) // System.IO.Compression DllImportEntry(CompressionNative_Deflate) // System.IO.Compression, System.Net.WebSockets DllImportEntry(CompressionNative_DeflateEnd) // System.IO.Compression, System.Net.WebSockets @@ -320,7 +322,7 @@ typedef struct PInvokeTable { static PInvokeTable s_PInvokeTables[] = { {"libSystem.Globalization.Native", s_libSystem_Globalization_Native, 33}, - {"libSystem.IO.Compression.Native", s_libSystem_IO_Compression_Native, 8}, + {"libSystem.IO.Compression.Native", s_libSystem_IO_Compression_Native, 9}, {"libSystem.Native", s_libSystem_Native, 94}, {"libSystem.Native.Browser", s_libSystem_Native_Browser, 1}, {"libSystem.Runtime.InteropServices.JavaScript.Native", s_libSystem_Runtime_InteropServices_JavaScript_Native, 6} diff --git a/src/coreclr/vm/wasm/helpers.cpp b/src/coreclr/vm/wasm/helpers.cpp index bcc938905914a4..01ccd1aa50da87 100644 --- a/src/coreclr/vm/wasm/helpers.cpp +++ b/src/coreclr/vm/wasm/helpers.cpp @@ -736,15 +736,14 @@ void InvokeCalliStub(CalliStubParam* pParam) _ASSERTE(pParam->ftn != (PCODE)NULL); _ASSERTE(pParam->cookie != NULL); - PCODE actualFtn = (PCODE)PortableEntryPoint::GetActualCode(pParam->ftn); - ((void(*)(PCODE, int8_t*, int8_t*, PCODE))pParam->cookie)(actualFtn, pParam->pArgs, pParam->pRet, pParam->ftn); + (pParam->cookie)(pParam->ftn, pParam->pArgs, pParam->pRet); } -void InvokeUnmanagedCalli(PCODE ftn, void *cookie, int8_t *pArgs, int8_t *pRet) +void InvokeUnmanagedCalli(PCODE ftn, InterpreterCalliCookie cookie, int8_t *pArgs, int8_t *pRet) { _ASSERTE(ftn != (PCODE)NULL); _ASSERTE(cookie != NULL); - ((void(*)(PCODE, int8_t*, int8_t*))cookie)(ftn, pArgs, pRet); + (cookie)(ftn, pArgs, pRet); } void InvokeDelegateInvokeMethod(DelegateInvokeMethodParam* pParam) @@ -902,13 +901,13 @@ namespace static StringToWasmSigThunkHash* thunkCache = nullptr; static StringToWasmSigThunkHash* portableEntrypointThunkCache = nullptr; - void* LookupThunk(const char* key) + InterpreterCalliCookie LookupThunk(const char* key) { StringToWasmSigThunkHash* table = thunkCache; _ASSERTE(table != nullptr && "Wasm thunk cache not initialized. Call InitializeWasmThunkCaches() at EEStartup."); void* thunk; bool success = table->Lookup(key, &thunk); - return success ? thunk : nullptr; + return success ? (InterpreterCalliCookie)thunk : nullptr; } void* LookupPortableEntryPointThunk(const char* key) @@ -921,7 +920,7 @@ namespace } // This is a simple signature computation routine for signatures currently supported in the wasm environment. - void* ComputeCalliSigThunk(MetaSig& sig) + InterpreterCalliCookie ComputeCalliSigThunk(MetaSig& sig) { STANDARD_VM_CONTRACT; _ASSERTE(sizeof(int32_t) == sizeof(void*)); @@ -944,7 +943,7 @@ namespace if (!GetSignatureKey(sig, keyBuffer, keyBufferLen)) return NULL; - void* thunk = LookupThunk(keyBuffer); + InterpreterCalliCookie thunk = LookupThunk(keyBuffer); #ifdef _DEBUG if (thunk == NULL) printf("WASM calli missing for key: %s\n", keyBuffer); @@ -1118,11 +1117,11 @@ void InitializeWasmThunkCaches() } } -void* GetCookieForCalliSig(MetaSig metaSig, MethodDesc *pContextMD) +InterpreterCalliCookie GetCookieForCalliSig(MetaSig metaSig, MethodDesc *pContextMD) { STANDARD_VM_CONTRACT; - void* thunk = ComputeCalliSigThunk(metaSig); + InterpreterCalliCookie thunk = ComputeCalliSigThunk(metaSig); if (thunk == NULL) { PORTABILITY_ASSERT("GetCookieForCalliSig: unknown thunk signature"); @@ -1190,10 +1189,15 @@ void* GetUnmanagedCallersOnlyThunk(MethodDesc* pMD) void InvokeManagedMethod(ManagedMethodParam *pParam) { - MetaSig sig(pParam->pMD); - void* cookie = GetCookieForCalliSig(sig, pParam->pMD); - - _ASSERTE(cookie != NULL); + InterpreterCalliCookie cookie = pParam->pMD->GetCalliCookie(); + if (cookie == NULL) + { + MetaSig sig(pParam->pMD); + cookie = GetCookieForCalliSig(sig, pParam->pMD); + _ASSERTE(cookie != NULL); + pParam->pMD->SetCalliCookie(cookie); + cookie = pParam->pMD->GetCalliCookie(); + } CalliStubParam param = { pParam->target == NULL ? pParam->pMD->GetMultiCallableAddrOfCode(CORINFO_ACCESS_ANY) : pParam->target, cookie, pParam->pArgs, pParam->pRet, pParam->pContinuationRet }; InvokeCalliStub(¶m); diff --git a/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs b/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs index 711b88198e1062..a358f74c4c57a8 100644 --- a/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs +++ b/src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs @@ -105,15 +105,16 @@ private static void Emit(StreamWriter w, IEnumerable cookies) var portableEntryPointComma = signature.Length > 1 ? ", " : ""; var portableEntrypointDeclaration = isPortableEntryPointCall ? portableEntryPointComma + "PCODE" : ""; - var portableEntrypointParam = isPortableEntryPointCall ? portableEntryPointComma + "pPortableEntryPointContext" : ""; + var portableEntrypointParam = isPortableEntryPointCall ? portableEntryPointComma + "pPortableEntryPoint" : ""; var portableEntrypointStackDeclaration = isPortableEntryPointCall ? "int*, " : ""; var portableEntrypointStackParam = isPortableEntryPointCall ? "&framePointer, " : ""; + var portableEntrypointPointerRD = isPortableEntryPointCall ? "*" : ""; w.Write( $$""" - {{(isPortableEntryPointCall ? "NOINLINE " : "")}}static void {{CallFuncName(args, SignatureMapper.CharToNameType(signature[0]), isPortableEntryPointCall)}}(PCODE pcode, int8_t* pArgs, int8_t* pRet{{(isPortableEntryPointCall ? ", PCODE pPortableEntryPointContext" : "")}}) + {{(isPortableEntryPointCall ? "NOINLINE " : "")}}static void {{CallFuncName(args, SignatureMapper.CharToNameType(signature[0]), isPortableEntryPointCall)}}(PCODE {{(isPortableEntryPointCall ? "pPortableEntryPoint" : "pcode")}}, int8_t* pArgs, int8_t* pRet) {{{(isPortableEntryPointCall ? "\n alignas(16) int framePointer = TERMINATE_R2R_STACK_WALK;" : "")}} - {{result.nativeType}} (*fptr)({{portableEntrypointStackDeclaration}}{{args.Join(", ", (p, i) => SignatureMapper.CharToNativeType(p))}}{{portableEntrypointDeclaration}}) = ({{result.nativeType}} (*)({{portableEntrypointStackDeclaration}}{{args.Join(", ", (p, i) => SignatureMapper.CharToNativeType(p))}}{{portableEntrypointDeclaration}}))pcode; + {{result.nativeType}} (*fptr)({{portableEntrypointStackDeclaration}}{{args.Join(", ", (p, i) => SignatureMapper.CharToNativeType(p))}}{{portableEntrypointDeclaration}}) = {{portableEntrypointPointerRD}}({{result.nativeType}} ({{portableEntrypointPointerRD}}*)({{portableEntrypointStackDeclaration}}{{args.Join(", ", (p, i) => SignatureMapper.CharToNativeType(p))}}{{portableEntrypointDeclaration}})){{(isPortableEntryPointCall ? "(pPortableEntryPoint)" : "pcode")}}; {{portabilityAssert}}{{(result.isVoid ? "" : "*" + "((" + result.nativeType + "*)pRet) = ")}}(*fptr)({{portableEntrypointStackParam}}{{args.Join(", ", (p, i) => $"{SignatureMapper.CharToArgType(p)}({i})")}}{{portableEntrypointParam}}); } From c5a57b04b0280bea0b01b5ac50d664773a75c978 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang <16830051+mdh1418@users.noreply.github.com> Date: Tue, 5 May 2026 11:38:09 -0400 Subject: [PATCH 109/115] [Android] In-Proc Crash Reporter (#126916) Android CoreCLR does not currently have the same out-of-proc `createdump` experience available on other platforms. That makes native crashes, aborts, and mixed managed/native failures significantly harder to diagnose in real applications. This PR adds an **opt-in in-proc crash reporter for Android CoreCLR** that emits a **JSON-formatted crash report** modeled after `createdump`'s `CrashReportWriter`. The report is structurally compatible with createdump's output (same envelope, same per-thread / per-frame fields, every value emitted as a quoted JSON string including booleans and numerics) so the same downstream tooling can consume both reports. There are **two intentional schema deviations** from createdump for Android in-proc reports: - **`signal` instead of `ExceptionType`.** createdump emits a Windows-shaped `ExceptionType` field, which is meaningless on Unix where the crash is identified by the POSIX signal number. The in-proc reporter emits the signal number directly (per earlier reviewer feedback). Tooling that branches on createdump's `ExceptionType` should add a `signal`-aware path for Android reports. - **No root `pid` field.** createdump exposes the dumped process's PID because it runs out-of-process; the in-proc reporter runs inside the crashing process, so the PID is implicit and is already encoded in the report filename via the `%p` template substitution. When enabled, the runtime writes a `*.crashreport.json` file directly from the crash path to the location derived from `DOTNET_DbgMiniDumpName`. The long-term intent is for this reporting path to be **async-signal-safe**. This PR starts that work by making the lower-risk / low-hanging pieces fit that model where practical, while leaving the more complex runtime-state publication and hardening work to follow-up PRs. ## Enabling the crash reporter The crash reporter is **opt-in**, and requires both env vars to be set. ### Enable JSON crash reporting ```bash DOTNET_EnableCrashReport=1 DOTNET_DbgMiniDumpName=/data/data//files/dotnet_crash_%p ``` When configured this way, the runtime will write the report to: ```text /data/data//files/dotnet_crash_.crashreport.json ``` `DOTNET_DbgMiniDumpName` is required and is expected to be a writable absolute path. Template substitution is supported via `%p` (PID), `%d` (PID alias), `%e` (process / executable name), `%h` (host name, captured at startup), and `%t` (UNIX timestamp), matching createdump's `FormatDumpName`. If `DOTNET_DbgMiniDumpName` is unset or the file cannot be opened, no report is written. The reporter does **not** synthesize a fallback path: open-coded `TMPDIR` / `Path.GetTempPath` resolution from a fatal-signal context is security-sensitive and intentionally left to the host (this matches createdump's existing contract for the same setting). ## Current design note This PR is intentionally the **first step**, not the complete hardening story. The payload shape and reporting path are designed with **async-signal-safety** as the target, but today the implementation is a mix of: - crash-time logic that is already close to signal-safe - best-effort runtime inspection for richer managed diagnostics Follow-up PRs will continue hardening the remaining pieces so the implementation better matches the intended strict crash-path constraints. ## Example crash report shapes https://gist.github.com/mdh1418/31b36ef77d3c057ff061b92e28b527d7 ## Follow-up work - **Cross-platform support** The reporter library and the PAL callback ABI are already platform-neutral; what remains is per-platform `ucontext_t` register extraction, validation on each target, and host-side opt-in wiring. - **Emit the report to logcat / stderr in addition to the file.** Useful when no writable file location is available or when capturing logs from CI / device farms. - **Crash-report file lifecycle on Android.** The app's private files/ directory has no OS-level rotation (unlike desktop where tmpwatch / systemd / a CI step typically prunes), so a crash-looping app can accumulate *.crashreport.json files indefinitely when DbgMiniDumpName uses a %p template. Need a host-side rotation policy (cap on file count, age-based cleanup at startup, or single-slot overwrite) appropriate for sandboxed apps. - **Cross-runtime temp-directory helper.** Mono / EventPipe also have ad-hoc `g_get_tmp_dir` / `ep_rt_temp_path_get` helpers. A shared async-signal-safe helper (matching the security model of the managed `Path.GetTempPath`) would let the reporter optionally fall back to a tempdir. - **Misleading-throwable suppression.** The reporter currently skips `getExceptionCallback` only for `SIGSEGV`/`SIGBUS`. A native fault while a managed throwable is pinned on the thread (e.g., during an in-flight `EX_THROW`) can still surface as `SIGABRT`/`SIGILL`/`SIGFPE` and report a stale exception. A complete fix needs a TLS "currently dispatching" flag in the VM. - **Stress log integration.** Dump the in-process stress log into the JSON report (and/or logcat) when enabled. - **Async-signal-safety hardening of the remaining best-effort paths.** Replace the remaining VM helpers in the crash path with strictly signal-safe equivalents (frame metadata lookup, IL offset resolution, etc.). --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/clrdefinitions.cmake | 4 + src/coreclr/clrfeatures.cmake | 8 + src/coreclr/debug/CMakeLists.txt | 3 + src/coreclr/debug/crashreport/CMakeLists.txt | 10 + .../debug/crashreport/inproccrashreporter.cpp | 1046 +++++++++++++++++ .../debug/crashreport/inproccrashreporter.h | 105 ++ .../crashreport/signalsafejsonwriter.cpp | 399 +++++++ .../debug/crashreport/signalsafejsonwriter.h | 78 ++ .../dlls/mscoree/coreclr/CMakeLists.txt | 4 + src/coreclr/pal/inc/pal.h | 19 + src/coreclr/pal/src/thread/process.cpp | 55 +- src/coreclr/vm/CMakeLists.txt | 9 + src/coreclr/vm/ceemain.cpp | 8 + src/coreclr/vm/crashreportstackwalker.cpp | 446 +++++++ src/coreclr/vm/crashreportstackwalker.h | 13 + 15 files changed, 2194 insertions(+), 13 deletions(-) create mode 100644 src/coreclr/debug/crashreport/CMakeLists.txt create mode 100644 src/coreclr/debug/crashreport/inproccrashreporter.cpp create mode 100644 src/coreclr/debug/crashreport/inproccrashreporter.h create mode 100644 src/coreclr/debug/crashreport/signalsafejsonwriter.cpp create mode 100644 src/coreclr/debug/crashreport/signalsafejsonwriter.h create mode 100644 src/coreclr/vm/crashreportstackwalker.cpp create mode 100644 src/coreclr/vm/crashreportstackwalker.h diff --git a/src/coreclr/clrdefinitions.cmake b/src/coreclr/clrdefinitions.cmake index 543ef485f0d525..0fd1a1f3d3e182 100644 --- a/src/coreclr/clrdefinitions.cmake +++ b/src/coreclr/clrdefinitions.cmake @@ -153,6 +153,10 @@ if(FEATURE_WEBCIL) add_compile_definitions(FEATURE_WEBCIL) endif() +if(FEATURE_INPROC_CRASHREPORT) + add_compile_definitions(FEATURE_INPROC_CRASHREPORT) +endif() + add_compile_definitions($<${FEATURE_JAVAMARSHAL}:FEATURE_JAVAMARSHAL>) add_definitions(-DFEATURE_READYTORUN) diff --git a/src/coreclr/clrfeatures.cmake b/src/coreclr/clrfeatures.cmake index c03aace596ffb3..829efbc022c5b9 100644 --- a/src/coreclr/clrfeatures.cmake +++ b/src/coreclr/clrfeatures.cmake @@ -88,6 +88,14 @@ if(NOT DEFINED FEATURE_SINGLE_FILE_DIAGNOSTICS) set(FEATURE_SINGLE_FILE_DIAGNOSTICS 1) endif(NOT DEFINED FEATURE_SINGLE_FILE_DIAGNOSTICS) +if(NOT DEFINED FEATURE_INPROC_CRASHREPORT) + if(CLR_CMAKE_TARGET_ANDROID) + set(FEATURE_INPROC_CRASHREPORT 1) + else() + set(FEATURE_INPROC_CRASHREPORT 0) + endif() +endif(NOT DEFINED FEATURE_INPROC_CRASHREPORT) + if ((CLR_CMAKE_TARGET_WIN32 OR CLR_CMAKE_TARGET_UNIX) AND NOT CLR_CMAKE_TARGET_ARCH_WASM) set(FEATURE_COMWRAPPERS 1) endif() diff --git a/src/coreclr/debug/CMakeLists.txt b/src/coreclr/debug/CMakeLists.txt index 5a0a420346882f..4771c8bfe34022 100644 --- a/src/coreclr/debug/CMakeLists.txt +++ b/src/coreclr/debug/CMakeLists.txt @@ -7,6 +7,9 @@ include_directories(${RUNTIME_DIR}) add_subdirectory(daccess) add_subdirectory(ee) add_subdirectory(di) +if(FEATURE_INPROC_CRASHREPORT AND NOT CLR_CROSS_COMPONENTS_BUILD) + add_subdirectory(crashreport) +endif() if(CLR_CMAKE_HOST_WIN32) add_subdirectory(createdump) endif(CLR_CMAKE_HOST_WIN32) diff --git a/src/coreclr/debug/crashreport/CMakeLists.txt b/src/coreclr/debug/crashreport/CMakeLists.txt new file mode 100644 index 00000000000000..f88699a4c6a464 --- /dev/null +++ b/src/coreclr/debug/crashreport/CMakeLists.txt @@ -0,0 +1,10 @@ +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CRASHREPORT_SOURCES + signalsafejsonwriter.cpp + inproccrashreporter.cpp +) + +add_library(inproccrashreport OBJECT ${CRASHREPORT_SOURCES}) + +target_sources(coreclrpal PRIVATE $) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp new file mode 100644 index 00000000000000..d8c5257e8aff28 --- /dev/null +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -0,0 +1,1046 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// In-proc crash report generator. +// +// Streams a createdump-shaped JSON skeleton to a crashreport.json file. + +#include "inproccrashreporter.h" +#include "signalsafejsonwriter.h" + +#include "pal.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +// Include the .NET version string instead of linking because it is "static". +#if __has_include("_version.c") +#include "_version.c" +#else +static char sccsid[] = "@(#)Version N/A"; +#endif + +class ThreadEnumerationContext +{ +public: + ThreadEnumerationContext( + SignalSafeJsonWriter* writer, + void* signalContext) + : m_writer(writer), + m_signalContext(signalContext), + m_threadCount(0), + m_sawCrashThread(false) + { + } + + ThreadEnumerationContext(const ThreadEnumerationContext&) = delete; + ThreadEnumerationContext& operator=(const ThreadEnumerationContext&) = delete; + + size_t ThreadCount() const { return m_threadCount; } + bool SawCrashThread() const { return m_sawCrashThread; } + SignalSafeJsonWriter* Writer() const { return m_writer; } + + void EnumerateThreads(InProcCrashReportEnumerateThreadsCallback callback, uint64_t crashingTid); + + static void ThreadCallback( + uint64_t osThreadId, + bool isCrashThread, + const char* exceptionType, + uint32_t exceptionHResult, + void* ctx); + + static void FrameCallback( + uint64_t ip, + uint64_t stackPointer, + const char* methodName, + const char* className, + const char* moduleName, + uint32_t nativeOffset, + uint32_t token, + uint32_t ilOffset, + uint32_t moduleTimestamp, + uint32_t moduleSize, + const char* moduleGuid, + void* ctx); + +private: + void OnThread( + uint64_t osThreadId, + bool isCrashThread, + const char* exceptionType, + uint32_t exceptionHResult); + + void OnFrame( + uint64_t ip, + uint64_t stackPointer, + const char* methodName, + const char* className, + const char* moduleName, + uint32_t nativeOffset, + uint32_t token, + uint32_t ilOffset, + uint32_t moduleTimestamp, + uint32_t moduleSize, + const char* moduleGuid); + + SignalSafeJsonWriter* m_writer; + void* m_signalContext; + size_t m_threadCount; + bool m_sawCrashThread; +}; + +class CrashReportOutputContext +{ +public: + explicit CrashReportOutputContext(int fd) + : m_fd(fd), + m_writeFailed(false) + { + } + + CrashReportOutputContext(const CrashReportOutputContext&) = delete; + CrashReportOutputContext& operator=(const CrashReportOutputContext&) = delete; + + int Fd() const { return m_fd; } + bool WriteFailed() const { return m_writeFailed; } + + static bool ChunkCallback(const char* buffer, size_t len, void* ctx); + +private: + bool HandleChunk(const char* buffer, size_t len); + + int m_fd; + bool m_writeFailed; +}; + +class CrashReportHelpers +{ +public: + static void GetVersionString( + char* buffer, + size_t bufferSize); + + static bool AppendString( + char* buffer, + size_t bufferSize, + size_t* pos, + const char* value); + + static void WriteRegistersToJson( + SignalSafeJsonWriter* writer, + void* context); + + static uint64_t GetInstructionPointer( + void* context); + + static uint64_t GetStackPointer( + void* context); + + static uint64_t GetFramePointer( + void* context); + + static void WriteCrashSiteFrameToJson( + SignalSafeJsonWriter* writer, + void* context); + + static void BuildMethodName( + char* buffer, + size_t bufferSize, + const char* className, + const char* methodName); + + static const char* GetFilename( + const char* path); + + static void CopyString( + char* buffer, + size_t bufferSize, + const char* value); + + static void JsonFrameCallback( + uint64_t ip, + uint64_t stackPointer, + const char* methodName, + const char* className, + const char* moduleName, + uint32_t nativeOffset, + uint32_t token, + uint32_t ilOffset, + uint32_t moduleTimestamp, + uint32_t moduleSize, + const char* moduleGuid, + void* ctx); + + static bool WriteToFile( + int fd, + const char* buffer, + size_t len); + + static bool BuildReportPath( + char* buffer, + size_t bufferSize, + const char* dumpPath, + const char* processName, + const char* hostName); + + static size_t ExpandDumpTemplate( + char* buffer, + size_t bufferSize, + const char* pattern, + const char* processName, + const char* hostName); +}; + +void +InProcCrashReporter::CreateReport( + int signal, + siginfo_t* siginfo, + void* context) +{ + static LONG s_generating = 0; + if (InterlockedCompareExchange(&s_generating, 1, 0) != 0) + { + return; + } + + char reportPath[CRASHREPORT_PATH_BUFFER_SIZE]; + reportPath[0] = '\0'; + + if (m_reportPath[0] == '\0' || !CrashReportHelpers::BuildReportPath(reportPath, sizeof(reportPath), m_reportPath, m_processName, m_hostName)) + { + return; + } + + int fd = open(reportPath, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd == -1) + { + return; + } + + (void)siginfo; + + CrashReportOutputContext outputContext(fd); + + m_jsonWriter.Init(&CrashReportOutputContext::ChunkCallback, &outputContext); + + m_jsonWriter.OpenObject(); + m_jsonWriter.OpenObject("payload"); + m_jsonWriter.WriteString("protocol_version", "1.0.0"); + + m_jsonWriter.OpenObject("configuration"); +#if defined(__x86_64__) + m_jsonWriter.WriteString("architecture", "amd64"); +#elif defined(__aarch64__) + m_jsonWriter.WriteString("architecture", "arm64"); +#elif defined(__arm__) + m_jsonWriter.WriteString("architecture", "arm"); +#endif + char version[sizeof(sccsid)]; + CrashReportHelpers::GetVersionString(version, sizeof(version)); + m_jsonWriter.WriteString("version", version); + m_jsonWriter.CloseObject(); // configuration + + if (m_processName[0] != '\0') + { + m_jsonWriter.WriteString("process_name", m_processName); + } + + m_jsonWriter.WriteDecimalAsString("pid", static_cast(GetCurrentProcessId())); + + m_jsonWriter.OpenArray("threads"); + if (m_enumerateThreadsCallback != nullptr) + { + ThreadEnumerationContext threadContext(&m_jsonWriter, context); + uint64_t crashingTid = static_cast(minipal_get_current_thread_id()); + + threadContext.EnumerateThreads(m_enumerateThreadsCallback, crashingTid); + + if (threadContext.ThreadCount() == 0 || !threadContext.SawCrashThread()) + { + EmitSynthesizedCrashThread(context, /*walkStack*/ false); + } + } + else + { + EmitSynthesizedCrashThread(context, /*walkStack*/ true); + } + m_jsonWriter.CloseArray(); // threads + + m_jsonWriter.CloseObject(); // payload + + m_jsonWriter.OpenObject("parameters"); + m_jsonWriter.WriteSignedDecimalAsString("signal", static_cast(signal)); + m_jsonWriter.CloseObject(); // parameters + + m_jsonWriter.CloseObject(); // root + + if (fd != -1) + { + bool writeSucceeded = m_jsonWriter.Finish() && + !outputContext.WriteFailed() && + CrashReportHelpers::WriteToFile(fd, "\n", 1); + + if (close(fd) != 0 || !writeSucceeded) + { + unlink(reportPath); + } + } +} + +InProcCrashReporter& +InProcCrashReporter::GetInstance() +{ + static InProcCrashReporter s_instance; + return s_instance; +} + +void +InProcCrashReporter::Initialize( + const InProcCrashReporterSettings& settings) +{ + m_isManagedThreadCallback = settings.isManagedThreadCallback; + m_walkStackCallback = settings.walkStackCallback; + m_enumerateThreadsCallback = settings.enumerateThreadsCallback; + CrashReportHelpers::CopyString(m_reportPath, sizeof(m_reportPath), settings.reportPath); + + m_processName[0] = '\0'; +#if defined(__ANDROID__) + // On Android every app forks from the Zygote, so /proc/self/exe always + // resolves to /system/bin/app_process64. /proc/self/cmdline holds the + // package name (set by ActivityThread via PR_SET_NAME / setproctitle), + // which is what crash diagnostics actually want. + int cmdlineFd = open("/proc/self/cmdline", O_RDONLY | O_CLOEXEC); + if (cmdlineFd >= 0) + { + char buf[CRASHREPORT_STRING_BUFFER_SIZE]; + ssize_t n = read(cmdlineFd, buf, sizeof(buf) - 1); + close(cmdlineFd); + if (n > 0) + { + buf[n] = '\0'; + CrashReportHelpers::CopyString(m_processName, sizeof(m_processName), CrashReportHelpers::GetFilename(buf)); + } + } +#endif + if (m_processName[0] == '\0') + { + if (char* exePath = minipal_getexepath()) + { + CrashReportHelpers::CopyString(m_processName, sizeof(m_processName), CrashReportHelpers::GetFilename(exePath)); + free(exePath); + } + } + + // Cache hostname here because gethostname is not on the POSIX + // async-signal-safe list; the dump-template expander needs it for %h + // expansion at crash time. + m_hostName[0] = '\0'; + if (gethostname(m_hostName, sizeof(m_hostName) - 1) == 0) + { + m_hostName[sizeof(m_hostName) - 1] = '\0'; + } + else + { + m_hostName[0] = '\0'; + } +} + +void +InProcCrashReportSignalDispatcher(int signal, void* siginfo, void* context) +{ + InProcCrashReporter& reporter = InProcCrashReporter::GetInstance(); + reporter.CreateReport(signal, static_cast(siginfo), context); +} + +void +InProcCrashReportInitialize(const InProcCrashReporterSettings& settings) +{ + InProcCrashReporter::GetInstance().Initialize(settings); + + // Register last so PAL only observes the dispatcher after the reporter + // singleton is fully populated (mirrors the publication ordering used by + // PAL_SetLogManagedCallstackForSignalCallback). + PAL_SetInProcCrashReportCallback(&InProcCrashReportSignalDispatcher); +} + +bool +CrashReportHelpers::WriteToFile( + int fd, + const char* buffer, + size_t len) +{ + if (fd < 0 || buffer == nullptr) + { + return false; + } + + size_t totalWritten = 0; + while (totalWritten < len) + { + ssize_t written = write(fd, buffer + totalWritten, len - totalWritten); + if (written > 0) + { + totalWritten += static_cast(written); + continue; + } + + if (written == -1 && errno == EINTR) + { + continue; + } + + return false; + } + + return true; +} + +bool +CrashReportOutputContext::HandleChunk( + const char* buffer, + size_t len) +{ + if (m_fd == -1) + { + return false; + } + + if (!CrashReportHelpers::WriteToFile(m_fd, buffer, len)) + { + m_writeFailed = true; + return false; + } + + return true; +} + +bool +CrashReportOutputContext::ChunkCallback( + const char* buffer, + size_t len, + void* ctx) +{ + CrashReportOutputContext* outputContext = reinterpret_cast(ctx); + if (outputContext == nullptr) + { + return false; + } + + return outputContext->HandleChunk(buffer, len); +} + +// Expand the coredump template patterns supported by createdump's +// FormatDumpName for DOTNET_DbgMiniDumpName: %% %p %d (PID), %e (process +// name, cached at Initialize), %h (hostname, cached at Initialize), and %t +// (current epoch seconds via time(2), POSIX async-signal-safe). Unknown +// specifiers are rejected (return 0) to match createdump and to avoid +// silently producing diverging file names from the same template. +size_t +CrashReportHelpers::ExpandDumpTemplate( + char* buffer, + size_t bufferSize, + const char* pattern, + const char* processName, + const char* hostName) +{ + if (buffer == nullptr || bufferSize == 0 || pattern == nullptr) + { + return 0; + } + + size_t pos = 0; + unsigned pid = static_cast(GetCurrentProcessId()); + + while (*pattern != '\0' && pos + 1 < bufferSize) + { + if (*pattern != '%') + { + buffer[pos++] = *pattern++; + continue; + } + + pattern++; + char specifier = *pattern; + + const char* substitution = nullptr; + char numberBuf[CRASHREPORT_NUMBER_BUFFER_SIZE]; + + switch (specifier) + { + case '%': + if (pos + 1 < bufferSize) + { + buffer[pos++] = '%'; + } + pattern++; + continue; + + case 'p': + case 'd': + if (SignalSafeJsonWriter::FormatUnsignedDecimal(numberBuf, sizeof(numberBuf), pid) == 0) + { + return 0; + } + substitution = numberBuf; + break; + + case 'e': + substitution = (processName != nullptr && processName[0] != '\0') ? processName : nullptr; + break; + + case 'h': + substitution = (hostName != nullptr && hostName[0] != '\0') ? hostName : nullptr; + break; + + case 't': + if (SignalSafeJsonWriter::FormatUnsignedDecimal( + numberBuf, sizeof(numberBuf), static_cast(time(nullptr))) == 0) + { + return 0; + } + substitution = numberBuf; + break; + + default: + // Unknown / unsupported specifier; fail rather than emit a + // path with a literal '%X' that would diverge from the file + // name createdump would produce for the same template. + return 0; + } + + if (substitution == nullptr) + { + // Required substitution unavailable (e.g. hostname capture failed + // at Initialize). Fail rather than emit a path missing this + // component, which could collide with the dump file on disk. + return 0; + } + + size_t subLen = strlen(substitution); + if (pos + subLen >= bufferSize) + { + return 0; + } + memcpy(buffer + pos, substitution, subLen); + pos += subLen; + + if (*pattern != '\0') + { + pattern++; + } + } + + buffer[pos] = '\0'; + if (*pattern != '\0') + { + // The output buffer filled before the full template was consumed. + // Fail rather than returning a truncated path that could collide or + // unexpectedly change the report location. + return 0; + } + return pos; +} + +bool +CrashReportHelpers::BuildReportPath( + char* buffer, + size_t bufferSize, + const char* dumpPath, + const char* processName, + const char* hostName) +{ + if (buffer == nullptr || bufferSize == 0 || dumpPath == nullptr || dumpPath[0] == '\0') + { + return false; + } + + char expanded[CRASHREPORT_PATH_BUFFER_SIZE]; + size_t expandedLen = ExpandDumpTemplate(expanded, sizeof(expanded), dumpPath, processName, hostName); + if (expandedLen == 0) + { + return false; + } + + size_t pos = 0; + if (!AppendString(buffer, bufferSize, &pos, expanded)) + { + return false; + } + if (!AppendString(buffer, bufferSize, &pos, ".crashreport.json")) + { + return false; + } + return true; +} + +void +CrashReportHelpers::GetVersionString( + char* buffer, + size_t bufferSize) +{ + if (buffer == nullptr || bufferSize == 0) + { + return; + } + + if (bufferSize == 1) + { + buffer[0] = '\0'; + return; + } + + buffer[0] = '\0'; + + const char* version = sccsid; + const char versionPrefix[] = "@(#)Version "; + if (strncmp(version, versionPrefix, sizeof(versionPrefix) - 1) != 0) + { + return; + } + + version += sizeof(versionPrefix) - 1; + + size_t toCopy = strnlen(version, bufferSize - 1); + if (toCopy != 0) + { + memcpy(buffer, version, toCopy); + } + + buffer[toCopy] = '\0'; +} + +// Appends |value| to |buffer| at *|pos|, advancing *|pos|, while leaving +// room for a trailing null terminator. Always null-terminates when +// bufferSize > 0. Returns true iff the full value was appended. +// Async-signal-safe. +bool +CrashReportHelpers::AppendString( + char* buffer, + size_t bufferSize, + size_t* pos, + const char* value) +{ + if (buffer == nullptr || pos == nullptr || value == nullptr || bufferSize == 0) + { + return false; + } + + size_t p = *pos; + while (*value != '\0' && p + 1 < bufferSize) + { + buffer[p++] = *value++; + } + buffer[p] = '\0'; + *pos = p; + return *value == '\0'; +} + +void +CrashReportHelpers::WriteRegistersToJson( + SignalSafeJsonWriter* writer, + void* context) +{ + uint64_t ipValue = GetInstructionPointer(context); + uint64_t spValue = GetStackPointer(context); + uint64_t bpValue = GetFramePointer(context); + + writer->OpenObject("ctx"); + writer->WriteHexAsString("IP", ipValue); + writer->WriteHexAsString("SP", spValue); + writer->WriteHexAsString("BP", bpValue); + writer->CloseObject(); // ctx +} + +uint64_t +CrashReportHelpers::GetInstructionPointer( + void* context) +{ + if (context == nullptr) + { + return 0; + } + + ucontext_t* ucontext = reinterpret_cast(context); +#if defined(__x86_64__) + return static_cast(ucontext->uc_mcontext.gregs[REG_RIP]); +#elif defined(__aarch64__) + return static_cast(ucontext->uc_mcontext.pc); +#elif defined(__arm__) + return static_cast(ucontext->uc_mcontext.arm_pc); +#else + return 0; +#endif +} + +uint64_t +CrashReportHelpers::GetStackPointer( + void* context) +{ + if (context == nullptr) + { + return 0; + } + + ucontext_t* ucontext = reinterpret_cast(context); +#if defined(__x86_64__) + return static_cast(ucontext->uc_mcontext.gregs[REG_RSP]); +#elif defined(__aarch64__) + return static_cast(ucontext->uc_mcontext.sp); +#elif defined(__arm__) + return static_cast(ucontext->uc_mcontext.arm_sp); +#else + return 0; +#endif +} + +uint64_t +CrashReportHelpers::GetFramePointer( + void* context) +{ + if (context == nullptr) + { + return 0; + } + + ucontext_t* ucontext = reinterpret_cast(context); +#if defined(__x86_64__) + return static_cast(ucontext->uc_mcontext.gregs[REG_RBP]); +#elif defined(__aarch64__) + return static_cast(ucontext->uc_mcontext.regs[29]); +#elif defined(__arm__) + return static_cast(ucontext->uc_mcontext.arm_fp); +#else + return 0; +#endif +} + +void +CrashReportHelpers::WriteCrashSiteFrameToJson( + SignalSafeJsonWriter* writer, + void* context) +{ + uint64_t ipValue = GetInstructionPointer(context); + uint64_t spValue = GetStackPointer(context); + + writer->OpenObject(); + // Crash-site frame: IP/SP captured directly from the signal's saved + // ucontext_t. It is the instruction the OS interrupted (faulting user + // code, libc abort(), the JIT, etc.) - not a frame inside this reporter. + // Marked native because classifying an arbitrary IP as managed would + // require a JIT lookup we deliberately avoid in the signal handler; + // subsequent frames produced by the managed stack walker carry their + // own is_managed classification. + writer->WriteString("is_managed", "false"); + writer->WriteHexAsString("stack_pointer", spValue); + writer->WriteHexAsString("native_address", ipValue); + writer->CloseObject(); // frame +} + +void +CrashReportHelpers::BuildMethodName( + char* buffer, + size_t bufferSize, + const char* className, + const char* methodName) +{ + if (buffer == nullptr || bufferSize == 0) + { + return; + } + + if (className != nullptr && methodName != nullptr) + { + size_t pos = 0; + AppendString(buffer, bufferSize, &pos, className); + AppendString(buffer, bufferSize, &pos, "."); + AppendString(buffer, bufferSize, &pos, methodName); + } + else if (className != nullptr) + { + CopyString(buffer, bufferSize, className); + } + else if (methodName != nullptr) + { + CopyString(buffer, bufferSize, methodName); + } + else + { + buffer[0] = '\0'; + } +} + +// Returns the basename of a path (the substring after the last directory +// separator). The crash reporter is currently Unix-only via +// FEATURE_INPROC_CRASHREPORT gating, but a future Windows port would need +// a different separator; expose a platform-conditional constant so callers +// don't have to change. +#if defined(_WIN32) +static constexpr char CRASHREPORT_DIRECTORY_SEPARATOR = '\\'; +#else +static constexpr char CRASHREPORT_DIRECTORY_SEPARATOR = '/'; +#endif + +const char* +CrashReportHelpers::GetFilename( + const char* path) +{ + if (path == nullptr) + { + return nullptr; + } + + const char* fileName = strrchr(path, CRASHREPORT_DIRECTORY_SEPARATOR); + if (fileName != nullptr) + { + ++fileName; + if (*fileName != '\0') + { + return fileName; + } + } + + return path; +} + +void +CrashReportHelpers::CopyString( + char* buffer, + size_t bufferSize, + const char* value) +{ + if (buffer == nullptr || bufferSize == 0) + { + return; + } + + if (value == nullptr) + { + buffer[0] = '\0'; + return; + } + + size_t toCopy = strnlen(value, bufferSize - 1); + if (toCopy != 0) + { + memcpy(buffer, value, toCopy); + } + + buffer[toCopy] = '\0'; +} + +void +CrashReportHelpers::JsonFrameCallback( + uint64_t ip, + uint64_t stackPointer, + const char* methodName, + const char* className, + const char* moduleName, + uint32_t nativeOffset, + uint32_t token, + uint32_t ilOffset, + uint32_t moduleTimestamp, + uint32_t moduleSize, + const char* moduleGuid, + void* ctx) +{ + SignalSafeJsonWriter* writer = reinterpret_cast(ctx); + if (writer == nullptr) + { + return; + } + + writer->OpenObject(); + writer->WriteHexAsString("stack_pointer", stackPointer); + writer->WriteHexAsString("native_address", ip); + writer->WriteHexAsString("native_offset", nativeOffset); + + if (methodName != nullptr) + { + char fullName[CRASHREPORT_STRING_BUFFER_SIZE]; + BuildMethodName(fullName, sizeof(fullName), className, methodName); + writer->WriteString("method_name", fullName); + writer->WriteString("is_managed", "true"); + writer->WriteHexAsString("token", token); + writer->WriteHexAsString("il_offset", ilOffset); + if (moduleName != nullptr) + { + writer->WriteString("filename", moduleName); + } + if (moduleTimestamp != 0) + { + writer->WriteHexAsString("timestamp", moduleTimestamp); + } + if (moduleSize != 0) + { + writer->WriteHexAsString("sizeofimage", moduleSize); + } + if (moduleGuid != nullptr && moduleGuid[0] != '\0') + { + writer->WriteString("guid", moduleGuid); + } + } + else + { + writer->WriteString("is_managed", "false"); + if (moduleName != nullptr) + { + writer->WriteString("native_module", moduleName); + } + } + + writer->CloseObject(); // frame +} + +void +ThreadEnumerationContext::OnFrame( + uint64_t ip, + uint64_t stackPointer, + const char* methodName, + const char* className, + const char* moduleName, + uint32_t nativeOffset, + uint32_t token, + uint32_t ilOffset, + uint32_t moduleTimestamp, + uint32_t moduleSize, + const char* moduleGuid) +{ + CrashReportHelpers::JsonFrameCallback(ip, stackPointer, methodName, className, moduleName, nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid, m_writer); +} + +void +ThreadEnumerationContext::FrameCallback( + uint64_t ip, + uint64_t stackPointer, + const char* methodName, + const char* className, + const char* moduleName, + uint32_t nativeOffset, + uint32_t token, + uint32_t ilOffset, + uint32_t moduleTimestamp, + uint32_t moduleSize, + const char* moduleGuid, + void* ctx) +{ + if (ctx == nullptr) + { + return; + } + reinterpret_cast(ctx)->OnFrame(ip, stackPointer, methodName, className, moduleName, nativeOffset, token, ilOffset, moduleTimestamp, moduleSize, moduleGuid); +} + +void +ThreadEnumerationContext::OnThread( + uint64_t osThreadId, + bool isCrashThread, + const char* exceptionType, + uint32_t exceptionHResult) +{ + if (m_threadCount > 0) + { + m_writer->CloseArray(); // stack_frames + m_writer->CloseObject(); // thread + + (void)m_writer->Flush(); + } + + if (isCrashThread) + { + m_sawCrashThread = true; + } + m_threadCount++; + + m_writer->OpenObject(); + m_writer->WriteString("is_managed", "true"); + m_writer->WriteString("crashed", isCrashThread ? "true" : "false"); + m_writer->WriteHexAsString("native_thread_id", osThreadId); + + if (exceptionType != nullptr && exceptionType[0] != '\0') + { + m_writer->WriteString("managed_exception_type", exceptionType); + m_writer->WriteHexAsString("managed_exception_hresult", exceptionHResult); + } + + if (isCrashThread) + { + CrashReportHelpers::WriteRegistersToJson(m_writer, m_signalContext); + } + + m_writer->OpenArray("stack_frames"); + if (isCrashThread) + { + CrashReportHelpers::WriteCrashSiteFrameToJson(m_writer, m_signalContext); + } +} + +void +ThreadEnumerationContext::ThreadCallback( + uint64_t osThreadId, + bool isCrashThread, + const char* exceptionType, + uint32_t exceptionHResult, + void* ctx) +{ + if (ctx == nullptr) + { + return; + } + reinterpret_cast(ctx)->OnThread(osThreadId, isCrashThread, exceptionType, exceptionHResult); +} + +void +ThreadEnumerationContext::EnumerateThreads( + InProcCrashReportEnumerateThreadsCallback callback, + uint64_t crashingTid) +{ + if (callback == nullptr) + { + return; + } + + callback(crashingTid, &ThreadCallback, &FrameCallback, this); + + if (m_threadCount == 0) + { + return; + } + + // Close the last thread's stack_frames + thread objects opened by OnThread. + m_writer->CloseArray(); // stack_frames + m_writer->CloseObject(); // thread + + // Flush the final thread so it reaches the crash report file even if any + // later work (e.g. synthesizing a crash thread fallback) hangs or faults. + (void)m_writer->Flush(); +} + +void +InProcCrashReporter::EmitSynthesizedCrashThread( + void* context, + bool walkStack) +{ + uint64_t crashingTid = static_cast(minipal_get_current_thread_id()); + + m_jsonWriter.OpenObject(); + m_jsonWriter.WriteString("is_managed", + m_isManagedThreadCallback != nullptr && m_isManagedThreadCallback() ? "true" : "false"); + m_jsonWriter.WriteString("crashed", "true"); + m_jsonWriter.WriteHexAsString("native_thread_id", crashingTid); + + CrashReportHelpers::WriteRegistersToJson(&m_jsonWriter, context); + m_jsonWriter.OpenArray("stack_frames"); + CrashReportHelpers::WriteCrashSiteFrameToJson(&m_jsonWriter, context); + if (walkStack && m_walkStackCallback != nullptr) + { + m_walkStackCallback(&CrashReportHelpers::JsonFrameCallback, &m_jsonWriter); + } + m_jsonWriter.CloseArray(); // stack_frames + m_jsonWriter.CloseObject(); // thread +} diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.h b/src/coreclr/debug/crashreport/inproccrashreporter.h new file mode 100644 index 00000000000000..01fa1e706c87da --- /dev/null +++ b/src/coreclr/debug/crashreport/inproccrashreporter.h @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// In-proc crash report generation. +// +// Emits a minimal createdump-shaped JSON payload to a *.crashreport.json file +// on disk. + +#pragma once + +#include +#include + +#include "signalsafejsonwriter.h" + +// Scratch-buffer sizes used throughout the in-proc crash reporter: +// - 1024 (matching createdump's MAX_LONGPATH) for paths (report paths and +// expanded dump templates), so DOTNET_DbgMiniDumpName values that work +// with createdump also work here. +// - 256 for identifiers (process name, type/class/exception names). +// - 32 for a single hex-or-decimal integer formatted as a C string +// (addresses, thread IDs, hresults). +static constexpr size_t CRASHREPORT_PATH_BUFFER_SIZE = 1024; +static constexpr size_t CRASHREPORT_STRING_BUFFER_SIZE = 256; +static constexpr size_t CRASHREPORT_NUMBER_BUFFER_SIZE = 32; + +using InProcCrashReportIsManagedThreadCallback = bool (*)(); + +using InProcCrashReportFrameCallback = void (*)( + uint64_t ip, + uint64_t stackPointer, + const char* methodName, + const char* className, + const char* moduleName, + uint32_t nativeOffset, + uint32_t token, + uint32_t ilOffset, + uint32_t moduleTimestamp, + uint32_t moduleSize, + const char* moduleGuid, + void* ctx); + +using InProcCrashReportWalkStackCallback = void (*)( + InProcCrashReportFrameCallback frameCallback, + void* ctx); + +using InProcCrashReportThreadCallback = void (*)( + uint64_t osThreadId, + bool isCrashThread, + const char* exceptionType, + uint32_t exceptionHResult, + void* ctx); + +using InProcCrashReportEnumerateThreadsCallback = void (*)( + uint64_t crashingTid, + InProcCrashReportThreadCallback threadCallback, + InProcCrashReportFrameCallback frameCallback, + void* ctx); + +struct InProcCrashReporterSettings +{ + const char* reportPath; + InProcCrashReportIsManagedThreadCallback isManagedThreadCallback; + InProcCrashReportWalkStackCallback walkStackCallback; + InProcCrashReportEnumerateThreadsCallback enumerateThreadsCallback; +}; + +class InProcCrashReporter +{ +public: + static InProcCrashReporter& GetInstance(); + + // Capture configuration and the crash-report template path. Must be called + // before the PAL enables signal-handler dispatch to CreateReport. + void Initialize(const InProcCrashReporterSettings& settings); + + void CreateReport( + int signal, + siginfo_t* siginfo, + void* context); + +private: + InProcCrashReporter() = default; + InProcCrashReporter(const InProcCrashReporter&) = delete; + InProcCrashReporter& operator=(const InProcCrashReporter&) = delete; + + void EmitSynthesizedCrashThread( + void* context, + bool walkStack); + + SignalSafeJsonWriter m_jsonWriter; + InProcCrashReportIsManagedThreadCallback m_isManagedThreadCallback = nullptr; + InProcCrashReportWalkStackCallback m_walkStackCallback = nullptr; + InProcCrashReportEnumerateThreadsCallback m_enumerateThreadsCallback = nullptr; + char m_reportPath[CRASHREPORT_PATH_BUFFER_SIZE] = {}; + char m_processName[CRASHREPORT_STRING_BUFFER_SIZE] = {}; + char m_hostName[CRASHREPORT_STRING_BUFFER_SIZE] = {}; +}; + +// Free-function entry point used by the runtime to wire the in-proc crash +// reporter into the PAL signal-handler path. Captures `settings` into the +// singleton and registers a signal-safe dispatcher with PAL via +// PAL_SetInProcCrashReportCallback. PAL has no direct dependency on the +// reporter; the only coupling is through this registered callback. +void InProcCrashReportInitialize(const InProcCrashReporterSettings& settings); diff --git a/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp b/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp new file mode 100644 index 00000000000000..2cd858ab544564 --- /dev/null +++ b/src/coreclr/debug/crashreport/signalsafejsonwriter.cpp @@ -0,0 +1,399 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "signalsafejsonwriter.h" + +#include +#include + +static +char +ToHexChar(unsigned value) +{ + return (value < 10) ? static_cast('0' + value) : static_cast('a' + (value - 10)); +} + +void +SignalSafeJsonWriter::Init( + SignalSafeJsonOutputCallback outputCallback, + void* outputContext) +{ + m_pos = 0; + m_commaNeeded = false; + m_writeFailed = false; + m_outputCallback = outputCallback; + m_outputContext = outputContext; +} + +bool +SignalSafeJsonWriter::OpenObject( + const char* key) +{ + WriteSeparator(); + if (key != nullptr) + { + WriteEscapedString(key); + AppendStr(": "); + } + AppendChar('{'); + m_commaNeeded = false; + return !m_writeFailed; +} + +bool +SignalSafeJsonWriter::OpenObject() +{ + return OpenObject(nullptr); +} + +bool +SignalSafeJsonWriter::CloseObject() +{ + AppendChar('}'); + m_commaNeeded = true; + return !m_writeFailed; +} + +bool +SignalSafeJsonWriter::OpenArray( + const char* key) +{ + WriteSeparator(); + if (key != nullptr) + { + WriteEscapedString(key); + AppendStr(": "); + } + AppendChar('['); + m_commaNeeded = false; + return !m_writeFailed; +} + +bool +SignalSafeJsonWriter::OpenArray() +{ + return OpenArray(nullptr); +} + +bool +SignalSafeJsonWriter::CloseArray() +{ + AppendChar(']'); + m_commaNeeded = true; + return !m_writeFailed; +} + +bool +SignalSafeJsonWriter::WriteString( + const char* key, + const char* value) +{ + WriteSeparator(); + WriteEscapedString(key); + AppendStr(": "); + WriteEscapedString(value); + return !m_writeFailed; +} + +bool +SignalSafeJsonWriter::Finish() +{ + return Flush(); +} + +bool +SignalSafeJsonWriter::Flush() +{ + if (m_writeFailed) + { + return false; + } + + if (m_pos == 0) + { + return true; + } + + if (m_outputCallback != nullptr && !m_outputCallback(m_buffer, m_pos, m_outputContext)) + { + m_writeFailed = true; + return false; + } + + m_pos = 0; + return true; +} + +bool +SignalSafeJsonWriter::Append( + const char* str, + size_t len) +{ + if (m_writeFailed) + { + return false; + } + + if (str == nullptr) + { + // Invalid input mid-document would corrupt the JSON. Latch the + // failure so subsequent writes become no-ops, matching the + // behavior when the output callback reports an I/O failure. + m_writeFailed = true; + return false; + } + + if (len == 0) + { + return true; + } + + size_t offset = 0; + size_t remaining = SIGNAL_SAFE_JSON_BUFFER_SIZE - m_pos; + while (offset < len) + { + if (remaining == 0) + { + if (!Flush()) + { + return false; + } + remaining = SIGNAL_SAFE_JSON_BUFFER_SIZE; + } + + size_t chunk = len - offset; + if (chunk > remaining) + { + chunk = remaining; + } + + memcpy(m_buffer + m_pos, str + offset, chunk); + m_pos += chunk; + offset += chunk; + remaining -= chunk; + } + + return true; +} + +bool +SignalSafeJsonWriter::AppendChar(char c) +{ + if (m_writeFailed) + { + return false; + } + + if (m_pos == SIGNAL_SAFE_JSON_BUFFER_SIZE && !Flush()) + { + return false; + } + + m_buffer[m_pos++] = c; + return true; +} + +bool +SignalSafeJsonWriter::AppendStr( + const char* str) +{ + if (str == nullptr) + { + m_writeFailed = true; + return false; + } + + return Append(str, strlen(str)); +} + +void +SignalSafeJsonWriter::WriteSeparator() +{ + if (m_commaNeeded) + AppendChar(','); + + m_commaNeeded = true; +} + +// Escape a string value for JSON. Handles \, ", and control characters. +void +SignalSafeJsonWriter::WriteEscapedString( + const char* str) +{ + AppendChar('"'); + if (str != nullptr) + { + for (size_t i = 0; str[i]; i++) + { + char c = str[i]; + if (c == '"') + AppendStr("\\\""); + else if (c == '\\') + AppendStr("\\\\"); + else if (c == '\n') + AppendStr("\\n"); + else if (c == '\r') + AppendStr("\\r"); + else if (c == '\t') + AppendStr("\\t"); + else if (static_cast(c) < 0x20) + { + char esc[6]; + esc[0] = '\\'; + esc[1] = 'u'; + esc[2] = '0'; + esc[3] = '0'; + esc[4] = ToHexChar((static_cast(c) >> 4) & 0xF); + esc[5] = ToHexChar(static_cast(c) & 0xF); + Append(esc, sizeof(esc)); + } + else + { + AppendChar(c); + } + } + } + + AppendChar('"'); +} + +// Bounded, async-signal-safe integer-to-string formatters. They write into the +// caller-supplied buffer and never allocate or call into stdio/locale code. +// If the buffer is too small to hold the maximum-width output (per the +// MAX_*_BUFFER_SIZE constants on SignalSafeJsonWriter), they leave only a null +// terminator and return early. + +void +SignalSafeJsonWriter::FormatHexValue( + char* buffer, + size_t bufferSize, + uint64_t value) +{ + if (buffer == nullptr || bufferSize == 0) + { + return; + } + + char reverse[MAX_HEX_DIGITS_UINT64]; + size_t reverseLength = 0; + do + { + unsigned digit = static_cast(value & 0xf); + reverse[reverseLength++] = static_cast(digit < 10 ? ('0' + digit) : ('a' + digit - 10)); + value >>= 4; + } while (value != 0 && reverseLength < sizeof(reverse)); + + if (bufferSize < HEX_PREFIX_LEN + reverseLength + NULL_TERMINATOR_LEN) + { + buffer[0] = '\0'; + return; + } + + buffer[0] = '0'; + buffer[1] = 'x'; + + size_t index = HEX_PREFIX_LEN; + while (reverseLength > 0) + { + buffer[index++] = reverse[--reverseLength]; + } + buffer[index] = '\0'; +} + +size_t +SignalSafeJsonWriter::FormatUnsignedDecimal( + char* buffer, + size_t bufferSize, + uint64_t value) +{ + if (buffer == nullptr || bufferSize == 0) + { + return 0; + } + + char reverse[MAX_DECIMAL_DIGITS_UINT64]; + size_t reverseLength = 0; + do + { + reverse[reverseLength++] = static_cast('0' + (value % 10)); + value /= 10; + } while (value != 0 && reverseLength < sizeof(reverse)); + + if (bufferSize < reverseLength + NULL_TERMINATOR_LEN) + { + buffer[0] = '\0'; + return 0; + } + + size_t pos = 0; + while (reverseLength > 0) + { + buffer[pos++] = reverse[--reverseLength]; + } + buffer[pos] = '\0'; + return pos; +} + +size_t +SignalSafeJsonWriter::FormatSignedDecimal( + char* buffer, + size_t bufferSize, + int64_t value) +{ + if (buffer == nullptr || bufferSize == 0) + { + return 0; + } + + if (value >= 0) + { + return FormatUnsignedDecimal(buffer, bufferSize, static_cast(value)); + } + + if (bufferSize < SIGN_LEN + NULL_TERMINATOR_LEN) + { + buffer[0] = '\0'; + return 0; + } + + buffer[0] = '-'; + // Cast to unsigned first to handle INT64_MIN without signed overflow. + uint64_t absValue = static_cast(-(value + 1)) + 1; + size_t written = FormatUnsignedDecimal(buffer + 1, bufferSize - 1, absValue); + if (written == 0) + { + buffer[0] = '\0'; + return 0; + } + return written + 1; +} + +bool +SignalSafeJsonWriter::WriteHexAsString( + const char* key, + uint64_t value) +{ + char scratch[MAX_HEX_FORMAT_BUFFER_SIZE]; + FormatHexValue(scratch, sizeof(scratch), value); + return WriteString(key, scratch); +} + +bool +SignalSafeJsonWriter::WriteDecimalAsString( + const char* key, + uint64_t value) +{ + char scratch[MAX_UNSIGNED_DECIMAL_BUFFER_SIZE]; + (void)FormatUnsignedDecimal(scratch, sizeof(scratch), value); + return WriteString(key, scratch); +} + +bool +SignalSafeJsonWriter::WriteSignedDecimalAsString( + const char* key, + int64_t value) +{ + char scratch[MAX_SIGNED_DECIMAL_BUFFER_SIZE]; + (void)FormatSignedDecimal(scratch, sizeof(scratch), value); + return WriteString(key, scratch); +} diff --git a/src/coreclr/debug/crashreport/signalsafejsonwriter.h b/src/coreclr/debug/crashreport/signalsafejsonwriter.h new file mode 100644 index 00000000000000..54eac5dbf6d30d --- /dev/null +++ b/src/coreclr/debug/crashreport/signalsafejsonwriter.h @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Bounded, signal-safe JSON writer. +// Streams content through a small fixed-size buffer using bounded low-level +// string and memory operations so file output does not require materializing +// the whole document at once. All public members are async-signal-safe: no +// heap allocation, no stdio, no locale or variadic formatting. + +#pragma once + +#include +#include + +using SignalSafeJsonOutputCallback = bool (*)(const char* buffer, size_t len, void* ctx); + +static constexpr size_t SIGNAL_SAFE_JSON_BUFFER_SIZE = 4 * 1024; + +class SignalSafeJsonWriter +{ +public: + // Maximum digit counts and required buffer sizes for the static format helpers below. + static constexpr size_t MAX_HEX_DIGITS_UINT64 = 16; + static constexpr size_t MAX_DECIMAL_DIGITS_UINT64 = 20; + static constexpr size_t HEX_PREFIX_LEN = 2; // "0x" + static constexpr size_t SIGN_LEN = 1; // '-' for signed decimals + static constexpr size_t NULL_TERMINATOR_LEN = 1; + static constexpr size_t MAX_HEX_FORMAT_BUFFER_SIZE = HEX_PREFIX_LEN + MAX_HEX_DIGITS_UINT64 + NULL_TERMINATOR_LEN; + static constexpr size_t MAX_UNSIGNED_DECIMAL_BUFFER_SIZE = MAX_DECIMAL_DIGITS_UINT64 + NULL_TERMINATOR_LEN; + static constexpr size_t MAX_SIGNED_DECIMAL_BUFFER_SIZE = SIGN_LEN + MAX_DECIMAL_DIGITS_UINT64 + NULL_TERMINATOR_LEN; + + SignalSafeJsonWriter() + : m_pos(0), + m_commaNeeded(false), + m_writeFailed(false), + m_outputCallback(nullptr), + m_outputContext(nullptr) + { + } + + SignalSafeJsonWriter(const SignalSafeJsonWriter&) = delete; + SignalSafeJsonWriter& operator=(const SignalSafeJsonWriter&) = delete; + + void Init(SignalSafeJsonOutputCallback outputCallback, void* outputContext); + bool OpenObject(const char* key); + bool OpenObject(); + bool CloseObject(); + bool OpenArray(const char* key); + bool OpenArray(); + bool CloseArray(); + bool WriteString(const char* key, const char* value); + bool WriteHexAsString(const char* key, uint64_t value); + bool WriteDecimalAsString(const char* key, uint64_t value); + bool WriteSignedDecimalAsString(const char* key, int64_t value); + bool Finish(); + bool Flush(); + + // Async-signal-safe integer-to-string formatters used by the Write* members + // above and by the few non-writer call sites that need the raw text (e.g. + // dump-name pattern expansion). All are bounded and never allocate. + static void FormatHexValue(char* buffer, size_t bufferSize, uint64_t value); + static size_t FormatUnsignedDecimal(char* buffer, size_t bufferSize, uint64_t value); + static size_t FormatSignedDecimal(char* buffer, size_t bufferSize, int64_t value); + +private: + bool Append(const char* str, size_t len); + bool AppendChar(char c); + bool AppendStr(const char* str); + void WriteSeparator(); + void WriteEscapedString(const char* str); + + char m_buffer[SIGNAL_SAFE_JSON_BUFFER_SIZE]; + size_t m_pos; + bool m_commaNeeded; + bool m_writeFailed; + SignalSafeJsonOutputCallback m_outputCallback; + void* m_outputContext; +}; diff --git a/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt b/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt index c0cf0a1ff4176b..a14eca3c136b9f 100644 --- a/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt +++ b/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt @@ -183,6 +183,10 @@ endif() target_link_libraries(coreclr_static PUBLIC ${CORECLR_LIBRARIES} ${CORECLR_STATIC_CLRJIT_STATIC} ${CORECLR_STATIC_CLRINTERPRETER_STATIC} cee_wks_core ${CEE_WKS_STATIC} ${FOUNDATION}) target_compile_definitions(coreclr_static PUBLIC CORECLR_EMBEDDED) +if(FEATURE_INPROC_CRASHREPORT AND NOT CLR_CROSS_COMPONENTS_BUILD) + target_sources(coreclr_static PRIVATE $) +endif() + if (CLR_CMAKE_HOST_ANDROID) target_link_libraries(coreclr PUBLIC log) endif() diff --git a/src/coreclr/pal/inc/pal.h b/src/coreclr/pal/inc/pal.h index 3f74cf3ec21b28..87df075da108d5 100644 --- a/src/coreclr/pal/inc/pal.h +++ b/src/coreclr/pal/inc/pal.h @@ -264,6 +264,25 @@ PALAPI PAL_SetLogManagedCallstackForSignalCallback( IN PLOGMANAGEDCALLSTACKFORSIGNAL_CALLBACK callback); +/// +/// Callback invoked from the fatal-signal path to write an in-proc crash +/// report. The callback runs inside the signal handler and must therefore +/// be async-signal-safe. siginfo is opaque (siginfo_t*) and context is the +/// raw ucontext_t pointer received by the PAL signal handler. +/// +/// Registration is opt-in: if no callback is installed the PAL falls back +/// to its default crash-dump path (createdump where available). The PAL +/// itself has no source-level dependency on the in-proc reporter library; +/// it only knows about this callback ABI. +/// +typedef VOID (*PINPROCCRASHREPORT_CALLBACK)(int signal, void* siginfo, void* context); + +PALIMPORT +VOID +PALAPI +PAL_SetInProcCrashReportCallback( + IN PINPROCCRASHREPORT_CALLBACK callback); + PALIMPORT VOID PALAPI diff --git a/src/coreclr/pal/src/thread/process.cpp b/src/coreclr/pal/src/thread/process.cpp index 4586b7c8114691..a77427191c17db 100644 --- a/src/coreclr/pal/src/thread/process.cpp +++ b/src/coreclr/pal/src/thread/process.cpp @@ -60,6 +60,7 @@ SET_DEFAULT_DEBUG_CHANNEL(PROCESS); // some headers have code with asserts, so d #include #include #include +#include #ifdef __linux__ #include @@ -190,6 +191,16 @@ Volatile g_logManagedCallstackForSignalC #define MAX_ARGV_ENTRIES 32 const char* g_argvCreateDump[MAX_ARGV_ENTRIES] = { nullptr }; +// Read from the fatal-signal path (PROCCreateCrashDumpIfEnabled) and written +// once during startup via PAL_SetInProcCrashReportCallback; use Volatile<> +// to match the publication ordering of g_logManagedCallstackForSignalCallback. +// PAL has no direct dependency on the in-proc crash reporter library; the +// reporter registers itself by installing this signal-safe callback. When +// no callback is registered, the fatal-signal path falls back to the +// out-of-proc createdump utility (where g_argvCreateDump has been populated +// via PAL_InitializeCoreCLR). +static Volatile g_inProcCrashReportCallback = nullptr; + // // Key used for associating CPalThread's with the underlying pthread // (through pthread_setspecific) @@ -648,6 +659,26 @@ PAL_SetLogManagedCallstackForSignalCallback( g_logManagedCallstackForSignalCallback = callback; } +/*++ +Function: + PAL_SetInProcCrashReportCallback + +Abstract: + Sets a callback that is invoked from the fatal-signal path to let the host + emit an in-proc crash report (used by Android CoreCLR in place of + out-of-proc createdump). + + NOTE: Currently only one callback can be set at a time. +--*/ +VOID +PALAPI +PAL_SetInProcCrashReportCallback( + IN PINPROCCRASHREPORT_CALLBACK callback) +{ + _ASSERTE(g_inProcCrashReportCallback == nullptr); + g_inProcCrashReportCallback = callback; +} + // Build the semaphore names using the PID and a value that can be used for distinguishing // between processes with the same PID (which ran at different times). This is to avoid // cases where a prior process with the same PID exited abnormally without having a chance @@ -2065,25 +2096,24 @@ PROCLogManagedCallstackForSignal(int signal) (no return value) --*/ -#ifdef HOST_ANDROID -#include VOID PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, bool serialize) { // Preserve context pointer to prevent optimization DoNotOptimize(&context); - // TODO: Dump stress log into logcat and/or file when enabled? - minipal_log_write_fatal("Aborting process.\n"); -} -#else -VOID -PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, bool serialize) -{ - // Preserve context pointer to prevent optimization - DoNotOptimize(&context); + // If a host registered an in-proc crash report callback, prefer it: the + // host emits its report from this signal frame and the process aborts. + PINPROCCRASHREPORT_CALLBACK callback = g_inProcCrashReportCallback; + if (callback != nullptr) + { + callback(signal, siginfo, context); + minipal_log_write_fatal("Aborting process.\n"); + return; + } - // If enabled, launch the create minidump utility and wait until it completes + // Otherwise fall back to launching the out-of-proc createdump utility + // and wait until it completes. if (g_argvCreateDump[0] != nullptr) { const char* argv[MAX_ARGV_ENTRIES]; @@ -2153,7 +2183,6 @@ PROCCreateCrashDumpIfEnabled(int signal, siginfo_t* siginfo, void* context, bool free(signalAddressArg); } } -#endif /*++ Function: diff --git a/src/coreclr/vm/CMakeLists.txt b/src/coreclr/vm/CMakeLists.txt index dff4181bdcbeab..39feb7f1ac6676 100644 --- a/src/coreclr/vm/CMakeLists.txt +++ b/src/coreclr/vm/CMakeLists.txt @@ -4,6 +4,9 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) include_directories(BEFORE ${CMAKE_CURRENT_SOURCE_DIR}) include_directories(${ARCH_SOURCES_DIR}) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../interop/inc) +if(FEATURE_INPROC_CRASHREPORT) + include_directories(${CLR_DIR}/debug/crashreport) +endif(FEATURE_INPROC_CRASHREPORT) include_directories(${CLR_SRC_NATIVE_DIR}) include_directories(${RUNTIME_DIR}) @@ -552,6 +555,12 @@ if(FEATURE_OBJCMARSHAL) ) endif(FEATURE_OBJCMARSHAL) +if(FEATURE_INPROC_CRASHREPORT AND NOT CLR_CROSS_COMPONENTS_BUILD) + list(APPEND VM_SOURCES_WKS + crashreportstackwalker.cpp + ) +endif(FEATURE_INPROC_CRASHREPORT AND NOT CLR_CROSS_COMPONENTS_BUILD) + list(APPEND VM_SOURCES_WKS interoplibinterface_java.cpp ) diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index 30c5e748d594de..d7325f5e5796e3 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -209,6 +209,10 @@ #include "gdbjit.h" #endif // FEATURE_GDBJIT +#ifdef FEATURE_INPROC_CRASHREPORT +#include "crashreportstackwalker.h" +#endif // FEATURE_INPROC_CRASHREPORT + #include "genanalysis.h" #ifdef HAVE_GCCOVER @@ -709,6 +713,10 @@ void EEStartupHelper() PAL_SetLogManagedCallstackForSignalCallback(EEPolicy::LogManagedCallstackForSignal); #endif // HOST_ANDROID +#ifdef FEATURE_INPROC_CRASHREPORT + CrashReportConfigure(); +#endif // FEATURE_INPROC_CRASHREPORT + #ifdef STRESS_LOG if (CLRConfig::GetConfigValue(CLRConfig::UNSUPPORTED_StressLog, g_pConfig->StressLog()) != 0) { unsigned facilities = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_LogFacility, LF_ALL); diff --git a/src/coreclr/vm/crashreportstackwalker.cpp b/src/coreclr/vm/crashreportstackwalker.cpp new file mode 100644 index 00000000000000..1670ec970d91ff --- /dev/null +++ b/src/coreclr/vm/crashreportstackwalker.cpp @@ -0,0 +1,446 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// VM-side implementation of the in-proc crash report thread callbacks. + +#include "common.h" +#include "codeman.h" +#include "dbginterface.h" +#include "method.hpp" +#include "peassembly.h" +#include +#include + +#ifdef FEATURE_INPROC_CRASHREPORT + +#include "inproccrashreporter.h" +#include "threadsuspend.h" +#include "gcenv.h" + +struct WalkContext +{ + InProcCrashReportFrameCallback callback; + void* userCtx; +}; + +static void BuildTypeName(LPUTF8 buffer, size_t bufferSize, LPCUTF8 namespaceName, LPCUTF8 className); + +static +StackWalkAction +FrameCallbackAdapter( + CrawlFrame* pCF, + VOID* pData) +{ + CONTRACTL + { + NOTHROW; + GC_NOTRIGGER; + CANNOT_TAKE_LOCK; + MODE_ANY; + } + CONTRACTL_END; + + WalkContext* ctx = static_cast(pData); + if (ctx == nullptr) + { + return SWA_CONTINUE; + } + + MethodDesc* pMD = pCF->GetFunction(); + if (pMD == nullptr) + { + return SWA_CONTINUE; + } + + LPCUTF8 methodName = pMD->GetName(); + mdMethodDef token = pMD->GetMemberDef(); + + LPCUTF8 className = nullptr; + LPCUTF8 namespaceName = nullptr; + MethodTable* pMT = pMD->GetMethodTable(); + if (pMT != nullptr) + { + mdTypeDef cl = pMT->GetCl(); + IMDInternalImport* pImport = pMD->GetMDImport(); + if (pImport != nullptr && cl != mdTypeDefNil) + { + pImport->GetNameOfTypeDef(cl, &className, &namespaceName); + } + } + + char classNameBuf[CRASHREPORT_STRING_BUFFER_SIZE]; + BuildTypeName(classNameBuf, sizeof(classNameBuf), namespaceName, className); + + LPCUTF8 moduleName = nullptr; + Module* pModule = pMD->GetModule(); + if (pModule != nullptr) + { + Assembly* pAssembly = pModule->GetAssembly(); + if (pAssembly != nullptr) + { + moduleName = pAssembly->GetSimpleName(); + } + } + + uint32_t nativeOffset = pCF->HasFaulted() ? 0 : pCF->GetRelOffset(); + uint32_t ilOffset = 0; + PCODE ip = (PCODE)0; + TADDR stackPointer = (TADDR)0; + PREGDISPLAY pRD = pCF->GetRegisterSet(); + if (pRD != nullptr) + { + ip = GetControlPC(pRD); + stackPointer = GetRegdisplaySP(pRD); + } + + if (ip == (PCODE)0 && stackPointer == (TADDR)0) + { + return SWA_CONTINUE; + } + + if (g_pDebugInterface != nullptr && pMD != nullptr) + { + DWORD resolvedILOffset = 0; + BOOL haveILOffset = FALSE; + EX_TRY + { + haveILOffset = g_pDebugInterface->GetILOffsetFromNative( + pMD, + reinterpret_cast(ip), + nativeOffset, + &resolvedILOffset); + } + EX_CATCH + { + // Best-effort: if IL-offset resolution throws, leave ilOffset = 0 + // and continue with the native frame metadata we already have. + } + EX_END_CATCH + if (haveILOffset) + { + ilOffset = resolvedILOffset; + } + } + + uint32_t moduleTimestamp = 0; + uint32_t moduleSize = 0; + char moduleGuid[MINIPAL_GUID_BUFFER_LEN]; + moduleGuid[0] = '\0'; + + if (pModule != nullptr) + { + PEAssembly* pPEAssembly = pModule->GetPEAssembly(); + if (pPEAssembly != nullptr && pPEAssembly->HasLoadedPEImage()) + { + moduleTimestamp = pPEAssembly->GetLoadedLayout()->GetTimeDateStamp(); + moduleSize = static_cast(pPEAssembly->GetLoadedLayout()->GetSize()); + } + + IMDInternalImport* pImport = pModule->GetMDImport(); + if (pImport != nullptr) + { + GUID mvid; + if (SUCCEEDED(pImport->GetScopeProps(nullptr, &mvid))) + { + minipal_guid_as_string(mvid, moduleGuid, MINIPAL_GUID_BUFFER_LEN); + } + } + } + + className = classNameBuf[0] == '\0' ? nullptr : classNameBuf; + ctx->callback(static_cast(ip), static_cast(stackPointer), methodName, className, moduleName, nativeOffset, static_cast(token), ilOffset, moduleTimestamp, moduleSize, moduleGuid, ctx->userCtx); + return SWA_CONTINUE; +} + +static +void +CrashReportWalkThread( + Thread* pThread, + InProcCrashReportFrameCallback frameCallback, + void* ctx) +{ + if (pThread == nullptr || frameCallback == nullptr) + { + return; + } + + WalkContext walkContext = { frameCallback, ctx }; + pThread->StackWalkFrames(FrameCallbackAdapter, &walkContext, + QUICKUNWIND | FUNCTIONSONLY | ALLOW_ASYNC_STACK_WALK); +} + +static +void +CrashReportWalkStack( + InProcCrashReportFrameCallback frameCallback, + void* ctx) +{ + CrashReportWalkThread(GetThreadAsyncSafe(), frameCallback, ctx); +} + +static +bool +CrashReportIsCurrentThreadManaged() +{ + return GetThreadAsyncSafe() != nullptr; +} + +// Copy a type's namespace-qualified name (namespace + '.' + class) into +// |buffer|, truncating if needed. Always null-terminates when bufferSize > 0. +static +void +BuildTypeName( + LPUTF8 buffer, + size_t bufferSize, + LPCUTF8 namespaceName, + LPCUTF8 className) +{ + if (bufferSize == 0) + { + return; + } + + size_t index = 0; + if (namespaceName != nullptr) + { + while (*namespaceName != '\0' && index + 1 < bufferSize) + { + buffer[index++] = *namespaceName++; + } + } + + if (className != nullptr) + { + if (index > 0 && index + 1 < bufferSize) + { + buffer[index++] = '.'; + } + + while (*className != '\0' && index + 1 < bufferSize) + { + buffer[index++] = *className++; + } + } + + buffer[index] = '\0'; +} + +static +bool +CrashReportGetExceptionForThread( + Thread* pThread, + char* exceptionTypeBuf, + size_t exceptionTypeBufSize, + uint32_t* hresult) +{ + CONTRACTL + { + NOTHROW; + GC_NOTRIGGER; + CANNOT_TAKE_LOCK; + MODE_ANY; + } + CONTRACTL_END; + + if (exceptionTypeBufSize > 0) + { + exceptionTypeBuf[0] = '\0'; + } + + if (hresult != nullptr) + { + *hresult = 0; + } + + // Only inspect the managed throwable when the thread is already in cooperative mode. + if (!pThread->PreemptiveGCDisabled()) + { + return false; + } + + bool result = false; + + // GCX_COOP transitions into cooperative mode via DisablePreemptiveGC. + // We early-return above when the thread isn't already cooperative, so + // GCX_COOP here is a no-op marker and never actually transitions modes. + GCX_COOP(); + + OBJECTREF throwable = pThread->GetThrowable(); + GCPROTECT_BEGIN(throwable); + + if (throwable != nullptr) + { + MethodTable* pMT = throwable->GetMethodTable(); + if (pMT != nullptr) + { + mdTypeDef cl = pMT->GetCl(); + Module* pModule = pMT->GetModule(); + if (pModule != nullptr) + { + IMDInternalImport* pImport = pModule->GetMDImport(); + if (pImport != nullptr && cl != mdTypeDefNil) + { + LPCUTF8 className = nullptr; + LPCUTF8 namespaceName = nullptr; + pImport->GetNameOfTypeDef(cl, &className, &namespaceName); + + BuildTypeName(exceptionTypeBuf, exceptionTypeBufSize, namespaceName, className); + } + } + } + + if (hresult != nullptr) + { + *hresult = static_cast(((EXCEPTIONREF)throwable)->GetHResult()); + } + + result = true; + } + + GCPROTECT_END(); + + return result; +} + +// Suspend non-crashing managed threads via SuspendEE so their stacks +// can be walked from runtime-known safe points. SuspendEE acquires the +// thread store lock and waits for every other managed thread to reach a +// safe point (and for any in-progress GC to complete), so skip it when +// a known pre-condition would prevent forward progress: +// +// * g_fFatalErrorOccurredOnGCThread: GC thread faulted mid-GC, so GC +// will never finish and SuspendEE's GC wait would hang. +// * GCHeapUtilities::IsGCInProgress(): a GC is already running; if it +// is wedged (common in runtime-internal crashes) SuspendEE hangs. +// * IsGCSpecialThread(): we are a GC thread ourselves; the GC wait +// would wait on us. +// * ThreadStore::HoldingThreadStore(pCrashThread): SuspendEE's +// LockThreadStore asserts the holder is unknown, so it would +// assert-fail in checked builds (undefined in release). +// +// The crash reporter is best-effort; on hang the Android watchdog +// kills the process and we keep whatever crash report JSON was flushed +// beforehand. + +static +bool +CrashReportSuspendThreads(Thread* pCrashThread) +{ + if (g_fFatalErrorOccurredOnGCThread + || GCHeapUtilities::IsGCInProgress() + || IsGCSpecialThread() + || ThreadStore::HoldingThreadStore(pCrashThread)) + { + return false; + } + + ThreadSuspend::SuspendEE(ThreadSuspend::SUSPEND_OTHER); + return true; +} + +static +void +CrashReportResumeThreads() +{ + ThreadSuspend::RestartEE(FALSE /* bFinishedGC */, TRUE /* SuspendSucceeded */); +} + +static +void +CrashReportEnumerateThreads( + uint64_t crashingTid, + InProcCrashReportThreadCallback threadCallback, + InProcCrashReportFrameCallback frameCallback, + void* ctx) +{ + Thread* pCrashThread = GetThreadAsyncSafe(); + + // Capture the crashing thread's exception state BEFORE suspending the EE + // so the throwable inspection runs in the thread's natural EE-live context, + // outside the suspended window which exists for safe-point operations on + // other threads. + char crashExceptionType[CRASHREPORT_STRING_BUFFER_SIZE]; + crashExceptionType[0] = '\0'; + uint32_t crashHresult = 0; + bool crashHasException = false; + bool isCrashingThread = pCrashThread != nullptr + && static_cast(pCrashThread->GetOSThreadId()) == crashingTid; + if (isCrashingThread) + { + crashHasException = CrashReportGetExceptionForThread( + pCrashThread, crashExceptionType, sizeof(crashExceptionType), &crashHresult); + } + + bool runtimeSuspended = CrashReportSuspendThreads(pCrashThread); + + // Emit the crashing thread first so the report keeps the most important + // thread even if later enumeration is incomplete. + if (isCrashingThread) + { + uint64_t crashOsId = static_cast(pCrashThread->GetOSThreadId()); + threadCallback(crashOsId, true, crashHasException ? crashExceptionType : "", crashHresult, ctx); + + CrashReportWalkThread(pCrashThread, frameCallback, ctx); + } + + // Walk the remaining managed threads only when the runtime was + // successfully suspended; otherwise the walker is not guaranteed + // to be at a safe point for them. + if (runtimeSuspended) + { + Thread* pThread = nullptr; + while ((pThread = ThreadStore::GetThreadList(pThread)) != nullptr) + { + if (pThread == pCrashThread) + continue; + + uint64_t osThreadId = static_cast(pThread->GetOSThreadId()); + if (osThreadId == 0 || osThreadId == crashingTid) + continue; + + threadCallback(osThreadId, false, "", 0, ctx); + CrashReportWalkThread(pThread, frameCallback, ctx); + } + + CrashReportResumeThreads(); + } +} + +void +CrashReportConfigure() +{ + // Read crash report configuration here rather than in PROCAbortInitialize + // because on Android the DOTNET_* environment variables are set via JNI + // after PAL_Initialize has already run. + CLRConfigNoCache enabledReportCfg = CLRConfigNoCache::Get("EnableCrashReport", /*noprefix*/ false, &getenv); + DWORD reportEnabled = 0; + bool enableCrashReport = enabledReportCfg.IsSet() && enabledReportCfg.TryAsInteger(10, reportEnabled) && reportEnabled == 1; + + CLRConfigNoCache enabledReportOnlyCfg = CLRConfigNoCache::Get("EnableCrashReportOnly", /*noprefix*/ false, &getenv); + DWORD reportOnlyEnabled = 0; + bool enableCrashReportOnly = enabledReportOnlyCfg.IsSet() && enabledReportOnlyCfg.TryAsInteger(10, reportOnlyEnabled) && reportOnlyEnabled == 1; + + if (!enableCrashReport && !enableCrashReportOnly) + { + return; + } + + CLRConfigNoCache dmpNameCfg = CLRConfigNoCache::Get("DbgMiniDumpName", /*noprefix*/ false, &getenv); + const char* dumpName = dmpNameCfg.IsSet() ? dmpNameCfg.AsString() : nullptr; + if (dumpName == nullptr || dumpName[0] == '\0') + { + return; + } + + InProcCrashReporterSettings settings = {}; + settings.reportPath = dumpName; + settings.isManagedThreadCallback = CrashReportIsCurrentThreadManaged; + settings.walkStackCallback = CrashReportWalkStack; + settings.enumerateThreadsCallback = CrashReportEnumerateThreads; + + // Initialize the reporter and register the PAL signal-path callback last + // so PAL only observes the reporter after all VM callbacks are wired in. + InProcCrashReportInitialize(settings); +} + +#endif // FEATURE_INPROC_CRASHREPORT diff --git a/src/coreclr/vm/crashreportstackwalker.h b/src/coreclr/vm/crashreportstackwalker.h new file mode 100644 index 00000000000000..7afa32eeb71f2f --- /dev/null +++ b/src/coreclr/vm/crashreportstackwalker.h @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#ifndef CRASHREPORTSTACKWALKER_H +#define CRASHREPORTSTACKWALKER_H + +#ifdef FEATURE_INPROC_CRASHREPORT + +void CrashReportConfigure(); + +#endif // FEATURE_INPROC_CRASHREPORT + +#endif // CRASHREPORTSTACKWALKER_H From 7cc34e6b5ddd4bdc1165e115cf4e4cbc8e3a8859 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 21 Apr 2026 09:23:53 -0400 Subject: [PATCH 110/115] Enable in-proc crash reporter on iOS, tvOS, and MacCatalyst Broaden the Android-only opt-in for the in-proc crash reporter to cover the other mobile Apple platforms: iOS, tvOS, and MacCatalyst. None of these ship with createdump, so the in-proc reporter is the only crash-report path available and the Android integration pattern applies directly. FEATURE_INPROC_CRASHREPORT in clrfeatures.cmake is extended from CLR_CMAKE_TARGET_ANDROID to also cover CLR_CMAKE_TARGET_IOS, CLR_CMAKE_TARGET_TVOS, and CLR_CMAKE_TARGET_MACCATALYST; no other build plumbing is needed because the existing PAL callback registration and DOTNET_EnableCrashReport / DOTNET_EnableCrashReportOnly knobs are already platform-agnostic. inproccrashreporter.cpp gains __APPLE__ branches for the instruction, stack, and frame pointer accessors. Darwin's ucontext_t::uc_mcontext is a pointer to __darwin_mcontext64, so the Apple branches dereference through ->__ss. On arm64 the arm_thread_state64_get_pc/sp/fp(__ss) accessor macros are used so the registers are stripped of pointer authentication codes on arm64e (identity on arm64). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/clrfeatures.cmake | 2 +- .../debug/crashreport/inproccrashreporter.cpp | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/coreclr/clrfeatures.cmake b/src/coreclr/clrfeatures.cmake index 829efbc022c5b9..0a15452b398e85 100644 --- a/src/coreclr/clrfeatures.cmake +++ b/src/coreclr/clrfeatures.cmake @@ -89,7 +89,7 @@ if(NOT DEFINED FEATURE_SINGLE_FILE_DIAGNOSTICS) endif(NOT DEFINED FEATURE_SINGLE_FILE_DIAGNOSTICS) if(NOT DEFINED FEATURE_INPROC_CRASHREPORT) - if(CLR_CMAKE_TARGET_ANDROID) + if(CLR_CMAKE_TARGET_ANDROID OR CLR_CMAKE_TARGET_IOS OR CLR_CMAKE_TARGET_TVOS OR CLR_CMAKE_TARGET_MACCATALYST) set(FEATURE_INPROC_CRASHREPORT 1) else() set(FEATURE_INPROC_CRASHREPORT 0) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index d8c5257e8aff28..d3793bd1475fe4 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -667,7 +667,11 @@ CrashReportHelpers::GetInstructionPointer( } ucontext_t* ucontext = reinterpret_cast(context); -#if defined(__x86_64__) +#if defined(__APPLE__) && defined(__x86_64__) + return static_cast(ucontext->uc_mcontext->__ss.__rip); +#elif defined(__APPLE__) && defined(__aarch64__) + return static_cast(arm_thread_state64_get_pc(ucontext->uc_mcontext->__ss)); +#elif defined(__x86_64__) return static_cast(ucontext->uc_mcontext.gregs[REG_RIP]); #elif defined(__aarch64__) return static_cast(ucontext->uc_mcontext.pc); @@ -688,7 +692,11 @@ CrashReportHelpers::GetStackPointer( } ucontext_t* ucontext = reinterpret_cast(context); -#if defined(__x86_64__) +#if defined(__APPLE__) && defined(__x86_64__) + return static_cast(ucontext->uc_mcontext->__ss.__rsp); +#elif defined(__APPLE__) && defined(__aarch64__) + return static_cast(arm_thread_state64_get_sp(ucontext->uc_mcontext->__ss)); +#elif defined(__x86_64__) return static_cast(ucontext->uc_mcontext.gregs[REG_RSP]); #elif defined(__aarch64__) return static_cast(ucontext->uc_mcontext.sp); @@ -709,7 +717,11 @@ CrashReportHelpers::GetFramePointer( } ucontext_t* ucontext = reinterpret_cast(context); -#if defined(__x86_64__) +#if defined(__APPLE__) && defined(__x86_64__) + return static_cast(ucontext->uc_mcontext->__ss.__rbp); +#elif defined(__APPLE__) && defined(__aarch64__) + return static_cast(arm_thread_state64_get_fp(ucontext->uc_mcontext->__ss)); +#elif defined(__x86_64__) return static_cast(ucontext->uc_mcontext.gregs[REG_RBP]); #elif defined(__aarch64__) return static_cast(ucontext->uc_mcontext.regs[29]); From b226c7d334ca89de7fbb5b5eec9dfd0a9685bc42 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Mon, 20 Apr 2026 20:47:16 -0400 Subject: [PATCH 111/115] Populate OSVersion and SystemModel via sysctl on Apple Replace the placeholder empty strings for OSVersion and SystemModel in the in-proc crash report with values read via sysctlbyname, matching what createdump's CrashReportWriter::WriteSysctl does: OSVersion <- kern.osproductversion (e.g. '15.1') SystemModel <- hw.model (e.g. 'MacBookPro18,3', 'iPhone13,2') A small WriteSysctlString helper uses a CRASHREPORT_STRING_BUFFER_SIZE stack buffer (ample for both sysctl values) and writes an empty string on failure so the JSON schema remains stable for downstream consumers. sysctlbyname is async-signal-safe and avoids heap allocation, matching the createdump behavior. SystemManufacturer stays hardcoded to 'apple'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/inproccrashreporter.cpp | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index d3793bd1475fe4..b9beb5209f54c2 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -18,6 +18,9 @@ #include #include #include +#ifdef __APPLE__ +#include +#endif // Include the .NET version string instead of linking because it is "static". #if __has_include("_version.c") @@ -26,6 +29,26 @@ static char sccsid[] = "@(#)Version N/A"; #endif +#ifdef __APPLE__ +// Query a sysctl by name into a caller-supplied stack buffer and write it to the JSON writer. +// sysctlbyname is async-signal-safe and avoids heap allocation, matching the createdump +// behavior (see src/coreclr/debug/createdump/crashreportwriter.cpp WriteSysctl). +static void WriteSysctlString(SignalSafeJsonWriter* writer, const char* sysctlName, const char* valueName) +{ + char buffer[CRASHREPORT_STRING_BUFFER_SIZE]; + size_t size = sizeof(buffer); + if (sysctlbyname(sysctlName, buffer, &size, nullptr, 0) == 0 && size > 0) + { + buffer[sizeof(buffer) - 1] = '\0'; + writer->WriteString(valueName, buffer); + } + else + { + writer->WriteString(valueName, ""); + } +} +#endif // __APPLE__ + class ThreadEnumerationContext { public: @@ -276,6 +299,11 @@ InProcCrashReporter::CreateReport( m_jsonWriter.OpenObject("parameters"); m_jsonWriter.WriteSignedDecimalAsString("signal", static_cast(signal)); +#ifdef __APPLE__ + WriteSysctlString(&m_jsonWriter, "kern.osproductversion", "OSVersion"); + WriteSysctlString(&m_jsonWriter, "hw.model", "SystemModel"); + m_jsonWriter.WriteString("SystemManufacturer", "apple"); +#endif m_jsonWriter.CloseObject(); // parameters m_jsonWriter.CloseObject(); // root From ebf0fab6364cea8c61d26c007f4c56714d303f9b Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Tue, 5 May 2026 13:11:34 -0400 Subject: [PATCH 112/115] Address PR review feedback for OSVersion/SystemModel sysctl - Cache OSVersion (kern.osproductversion) and SystemModel (hw.model) at Initialize() time instead of querying sysctl from the signal handler. sysctlbyname is not on the POSIX async-signal-safe list, so calling it from a signal-time crash report context was unsafe. Matches the existing m_hostName cache-at-Initialize pattern. - Omit OSVersion/SystemModel from the report when the cache lookup failed and left an empty string, mirroring createdump's WriteSysctl behavior. - NUL-clamp the cached strings to bufferSize-1 to defend against unterminated sysctl returns. - Add explicit include alongside ; the mach helpers are currently pulled in transitively through on Darwin, but the explicit include matches createdump.h and the rest of the PAL conventions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../debug/crashreport/inproccrashreporter.cpp | 37 +++++++++++++------ .../debug/crashreport/inproccrashreporter.h | 4 ++ 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index b9beb5209f54c2..4472318a0799eb 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -19,6 +19,7 @@ #include #include #ifdef __APPLE__ +#include #include #endif @@ -30,21 +31,22 @@ static char sccsid[] = "@(#)Version N/A"; #endif #ifdef __APPLE__ -// Query a sysctl by name into a caller-supplied stack buffer and write it to the JSON writer. -// sysctlbyname is async-signal-safe and avoids heap allocation, matching the createdump -// behavior (see src/coreclr/debug/createdump/crashreportwriter.cpp WriteSysctl). -static void WriteSysctlString(SignalSafeJsonWriter* writer, const char* sysctlName, const char* valueName) +// Query a sysctl by name into a caller-supplied buffer. Called from Initialize, NOT from the +// signal handler -- sysctl/sysctlbyname is not on POSIX's async-signal-safe list, so the +// queried values are cached for use during crash reporting (mirrors the m_hostName / +// gethostname pattern). +static void CacheSysctlString(const char* sysctlName, char* buffer, size_t bufferSize) { - char buffer[CRASHREPORT_STRING_BUFFER_SIZE]; - size_t size = sizeof(buffer); + buffer[0] = '\0'; + size_t size = bufferSize; if (sysctlbyname(sysctlName, buffer, &size, nullptr, 0) == 0 && size > 0) { - buffer[sizeof(buffer) - 1] = '\0'; - writer->WriteString(valueName, buffer); + size_t terminatorIndex = (size < bufferSize) ? size : bufferSize - 1; + buffer[terminatorIndex] = '\0'; } else { - writer->WriteString(valueName, ""); + buffer[0] = '\0'; } } #endif // __APPLE__ @@ -300,8 +302,14 @@ InProcCrashReporter::CreateReport( m_jsonWriter.OpenObject("parameters"); m_jsonWriter.WriteSignedDecimalAsString("signal", static_cast(signal)); #ifdef __APPLE__ - WriteSysctlString(&m_jsonWriter, "kern.osproductversion", "OSVersion"); - WriteSysctlString(&m_jsonWriter, "hw.model", "SystemModel"); + if (m_osVersion[0] != '\0') + { + m_jsonWriter.WriteString("OSVersion", m_osVersion); + } + if (m_systemModel[0] != '\0') + { + m_jsonWriter.WriteString("SystemModel", m_systemModel); + } m_jsonWriter.WriteString("SystemManufacturer", "apple"); #endif m_jsonWriter.CloseObject(); // parameters @@ -377,6 +385,13 @@ InProcCrashReporter::Initialize( { m_hostName[0] = '\0'; } + +#ifdef __APPLE__ + // Cache sysctl values at Initialize because sysctl/sysctlbyname is not on POSIX's + // async-signal-safe list; CreateReport reads these from the signal-handler path. + CacheSysctlString("kern.osproductversion", m_osVersion, sizeof(m_osVersion)); + CacheSysctlString("hw.model", m_systemModel, sizeof(m_systemModel)); +#endif } void diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.h b/src/coreclr/debug/crashreport/inproccrashreporter.h index 01fa1e706c87da..5018f3b0d10793 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.h +++ b/src/coreclr/debug/crashreport/inproccrashreporter.h @@ -95,6 +95,10 @@ class InProcCrashReporter char m_reportPath[CRASHREPORT_PATH_BUFFER_SIZE] = {}; char m_processName[CRASHREPORT_STRING_BUFFER_SIZE] = {}; char m_hostName[CRASHREPORT_STRING_BUFFER_SIZE] = {}; +#ifdef __APPLE__ + char m_osVersion[CRASHREPORT_STRING_BUFFER_SIZE] = {}; + char m_systemModel[CRASHREPORT_STRING_BUFFER_SIZE] = {}; +#endif }; // Free-function entry point used by the runtime to wire the in-proc crash From 8d141ce370f7493a4d957ab5fc5176a919e145f9 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Tue, 5 May 2026 13:11:45 -0400 Subject: [PATCH 113/115] Log managed callstack on signal for iOS, tvOS, and MacCatalyst Broaden the HOST_ANDROID guard around the signal-handler managed callstack logging callback to also cover HOST_IOS, HOST_TVOS, and HOST_MACCATALYST so that the same managed stack trace surfaced in Android crash output is also surfaced on the other Apple POSIX targets that opt into the in-process crash reporter. The change covers: - The PAL_SetLogManagedCallstackForSignalCallback registration in ceemain.cpp so EEPolicy::LogManagedCallstackForSignal is wired up. - The EEPolicy::LogManagedCallstackForSignal declaration in eepolicy.h and its implementation in eepolicy.cpp. The PAL signal-handler call sites (PROCLogManagedCallstackForSignal and the underlying PAL hook) were already compiled for all TARGET_UNIX targets, so this change just extends the managed half of the integration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/ceemain.cpp | 4 ++-- src/coreclr/vm/eepolicy.cpp | 4 ++-- src/coreclr/vm/eepolicy.h | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index d7325f5e5796e3..613cb42fae511c 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -709,9 +709,9 @@ void EEStartupHelper() PAL_SetShutdownCallback(EESocketCleanupHelper); #endif // TARGET_UNIX -#ifdef HOST_ANDROID +#if defined(HOST_ANDROID) || defined(HOST_IOS) || defined(HOST_TVOS) || defined(HOST_MACCATALYST) PAL_SetLogManagedCallstackForSignalCallback(EEPolicy::LogManagedCallstackForSignal); -#endif // HOST_ANDROID +#endif #ifdef FEATURE_INPROC_CRASHREPORT CrashReportConfigure(); diff --git a/src/coreclr/vm/eepolicy.cpp b/src/coreclr/vm/eepolicy.cpp index 6fcbaec5aaf61a..1462aeb827f681 100644 --- a/src/coreclr/vm/eepolicy.cpp +++ b/src/coreclr/vm/eepolicy.cpp @@ -911,7 +911,7 @@ int NOINLINE EEPolicy::HandleFatalError(UINT exitCode, UINT_PTR address, LPCWSTR return -1; } -#ifdef HOST_ANDROID +#if defined(HOST_ANDROID) || defined(HOST_IOS) || defined(HOST_TVOS) || defined(HOST_MACCATALYST) // Logs the managed callstack when a signal is received. void EEPolicy::LogManagedCallstackForSignal(LPCWSTR signalName) { @@ -926,4 +926,4 @@ void EEPolicy::LogManagedCallstackForSignal(LPCWSTR signalName) LogInfoForFatalError(0, message.GetUnicode(), nullptr, nullptr, nullptr); } -#endif // HOST_ANDROID +#endif diff --git a/src/coreclr/vm/eepolicy.h b/src/coreclr/vm/eepolicy.h index d9103664195e8b..04b592aca5156c 100644 --- a/src/coreclr/vm/eepolicy.h +++ b/src/coreclr/vm/eepolicy.h @@ -38,7 +38,7 @@ class EEPolicy static void DECLSPEC_NORETURN HandleFatalStackOverflow(EXCEPTION_POINTERS *pException, BOOL fSkipDebugger = FALSE); -#ifdef HOST_ANDROID +#if defined(HOST_ANDROID) || defined(HOST_IOS) || defined(HOST_TVOS) || defined(HOST_MACCATALYST) static void LogManagedCallstackForSignal(LPCWSTR signalName); #endif From 125a32ef6512fa95c79511e3323ad0487d8b1bc8 Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Tue, 5 May 2026 14:00:57 -0400 Subject: [PATCH 114/115] Log managed callstack when chaining to default signal handler When a previously-registered signal handler is SIG_DFL, the runtime restores the default action and re-raises so the OS produces its standard crash artifact. On platforms that register a host callback for PAL_SetLogManagedCallstackForSignalCallback (Android, iOS, tvOS, MacCatalyst), invoke that callback first so the managed callstack is written alongside PROCNotifyProcessShutdown and PROCCreateCrashDumpIfEnabled, matching the chained-handler path's behavior. This brings parity for Apple POSIX targets where the previous handler is typically SIG_DFL, which previously caused signal-driven crashes (SIGABRT, SIGSEGV, SIGBUS, SIGFPE, SIGILL) to skip managed-callstack logging. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/pal/src/exception/signal.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/coreclr/pal/src/exception/signal.cpp b/src/coreclr/pal/src/exception/signal.cpp index cd47a557eb4648..c5b6886b562219 100644 --- a/src/coreclr/pal/src/exception/signal.cpp +++ b/src/coreclr/pal/src/exception/signal.cpp @@ -448,9 +448,11 @@ static void invoke_previous_action(struct sigaction* action, int code, siginfo_t { if (signalRestarts) { - // Shutdown and create the core dump before we restore the signal to the default handler. + // Shutdown, log the managed callstack (if a host callback is registered), + // and create the core dump before we restore the signal to the default handler. PROCNotifyProcessShutdown(IsRunningOnAlternateStack(context)); + PROCLogManagedCallstackForSignal(code); PROCCreateCrashDumpIfEnabled(code, siginfo, context, true); // Restore the original and restart h/w exception. From 1bc643e63551f42b8b364c2478559997440b0b0d Mon Sep 17 00:00:00 2001 From: mdh1418 Date: Tue, 5 May 2026 14:55:29 -0400 Subject: [PATCH 115/115] Strip PAC from PC when serializing on Apple arm64 Use arm_thread_state64_get_pc_fptr instead of arm_thread_state64_get_pc when reading the crash thread's instruction pointer. On arm64e devices (real hardware iOS/iPadOS/tvOS) the kernel populates ucontext_t::__pc with the raw PAC-signed value; emitting that into the JSON crash report yields opaque addresses with auth bits set, which are useless for post-mortem symbolication. The _fptr variant returns void* and, on builds with ptrauth enabled, strips the PAC via ptrauth_strip(); on simulator and pre-arm64e builds it reduces to the raw __pc load and is binary-equivalent. This matches the convention already used elsewhere in the codebase for serialized program counters: PAL machexception.cpp and context.cpp use _fptr when populating CONTEXT.Pc, and createdump/threadinfomac.cpp uses _fptr when serializing thread state into the minidump. The internal-only GetInstructionPointer helper in createdump still uses the non-_fptr variant because it is consumed only within the dumping process and never serialized. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/crashreport/inproccrashreporter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/debug/crashreport/inproccrashreporter.cpp b/src/coreclr/debug/crashreport/inproccrashreporter.cpp index 4472318a0799eb..fe771432eee5f4 100644 --- a/src/coreclr/debug/crashreport/inproccrashreporter.cpp +++ b/src/coreclr/debug/crashreport/inproccrashreporter.cpp @@ -713,7 +713,7 @@ CrashReportHelpers::GetInstructionPointer( #if defined(__APPLE__) && defined(__x86_64__) return static_cast(ucontext->uc_mcontext->__ss.__rip); #elif defined(__APPLE__) && defined(__aarch64__) - return static_cast(arm_thread_state64_get_pc(ucontext->uc_mcontext->__ss)); + return reinterpret_cast(arm_thread_state64_get_pc_fptr(ucontext->uc_mcontext->__ss)); #elif defined(__x86_64__) return static_cast(ucontext->uc_mcontext.gregs[REG_RIP]); #elif defined(__aarch64__)