Remove ExInfo::m_hThrowable — use direct pointer for exception objects#127300
Remove ExInfo::m_hThrowable — use direct pointer for exception objects#127300max-charlamb wants to merge 3 commits intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR removes ExInfo::m_hThrowable (GC-handle indirection) and standardizes on the existing direct OBJECTREF m_exception path, aligning CoreCLR with NativeAOT and updating DAC/cDAC consumers accordingly.
Changes:
- Remove
m_hThrowableusage and migrate exception-object reads tom_exceptionacross EH, interop propagation, debugger/DAC paths. - Add explicit GC root scanning for the
ExInfochain to keep superseded exception objects alive without handles. - Update cDAC contracts/tests and DAC exception state plumbing to reflect the new representation.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/native/managed/cdac/tests/ThreadTests.cs | Updates tests to use the new thrown-object representation. |
| src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs | Updates mock ExceptionInfo layout to expose ThrownObject instead of ThrownObjectHandle. |
| src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs | Reads ThrownObject as a pointer field instead of a handle wrapper. |
| src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs | Returns current exception “handle” using ThrownObject and updates Watson bucket lookup accordingly. |
| src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Exception_1.cs | Updates nested exception info to return direct thrown object pointer. |
| src/coreclr/vm/threads.h | Removes handle-based throwable accessors and adjusts HasException/IsThrowableNull logic. |
| src/coreclr/vm/threads.cpp | Updates last-thrown synchronization to use OBJECTREF throwable. |
| src/coreclr/vm/interoplibinterface_shared.cpp | Changes propagating-exception callback signature/GC mode to take OBJECTREF. |
| src/coreclr/vm/interoplibinterface_objc.cpp | Switches ObjC propagation callback to accept OBJECTREF and removes handle dereference. |
| src/coreclr/vm/interoplibinterface.h | Updates declarations to match OBJECTREF callback signatures. |
| src/coreclr/vm/gcenv.ee.cpp | Adds GC root scanning of ExInfo chain for direct exception object references. |
| src/coreclr/vm/exstate.h | Removes GetThrowableAsHandle from ThreadExceptionState. |
| src/coreclr/vm/exstate.cpp | Removes handle-based throwable retrieval; GetThrowable returns m_exception. |
| src/coreclr/vm/exinfo.h | Removes m_hThrowable and returns m_exception directly from GetThrowable(). |
| src/coreclr/vm/exinfo.cpp | Drops handle lifecycle management; clears m_exception during resource release. |
| src/coreclr/vm/exceptionhandling.cpp | Updates DAC memory enumeration and stacktrace append paths to use m_exception. |
| src/coreclr/vm/excep.cpp | Updates stacktrace appending to preserve foreign/preallocated semantics with OBJECTREF. |
| src/coreclr/vm/eedbginterfaceimpl.cpp | Switches debugger exception retrieval logic to rely on m_LastThrownObjectHandle. |
| src/coreclr/vm/datadescriptor/datadescriptor.inc | Updates cDAC descriptor field to ThrownObject at offsetof(ExInfo, m_exception). |
| src/coreclr/debug/ee/debugger.cpp | Uses m_LastThrownObjectHandle for force-catch-handler lookup. |
| src/coreclr/debug/daccess/task.cpp | Changes ClrDataExceptionState::m_throwable type to TADDR and passes &m_exception. |
| src/coreclr/debug/daccess/request.cpp | Reads exception object directly from m_exception and updates Watson bucket retrieval. |
| src/coreclr/debug/daccess/dacimpl.h | Updates ClrDataExceptionState signature/storage for TADDR throwable. |
| src/coreclr/debug/daccess/dacdbiimpl.cpp | Switches “current exception” debugger handle to m_LastThrownObjectHandle. |
| src/coreclr/System.Private.CoreLib/src/System/Runtime/ExceptionServices/AsmOffsets.cs | Updates managed EH asm offsets to reflect the new ExInfo layout. |
db309be to
e6688b3
Compare
e6688b3 to
2812642
Compare
2812642 to
9d54eec
Compare
9d54eec to
e761d3e
Compare
e761d3e to
34f7963
Compare
34f7963 to
adffa37
Compare
Replace the GCHandle-based m_hThrowable field in ExInfo with direct use of the existing m_exception OBJECTREF field, matching NativeAOT's approach. Key changes: - Remove OBJECTHANDLE m_hThrowable from ExInfo, saving 8 bytes (64-bit) - Update AsmOffsets constants for the new field layout - Add GC root scanning of ExInfo chain in ScanStackRoots (gcenv.ee.cpp), mirroring NativeAOT's GcScanRootsWorker pattern - Simplify GetThrowable() to return m_exception directly - SetThrowable() no longer creates GC handles for ExInfo - Remove GetThrowableAsHandle() entirely — all callers migrated to use GetThrowable() (OBJECTREF) or m_LastThrownObjectHandle (real handle) - Update StackTraceInfo::AppendElement OBJECTREF overload to preserve foreign-exception semantics and preallocated exception checks - Update Interop propagation callback to take OBJECTREF instead of handle - Update DAC code (request.cpp, task.cpp, dacdbiimpl.cpp) to use m_exception directly - Update debugger code (eedbginterfaceimpl.cpp, debugger.cpp) to use m_LastThrownObjectHandle for handle-based APIs - Update cDAC: ThrownObjectHandle -> ThrownObject (direct pointer) - Update cDAC contracts, data classes, and tests This eliminates ~5 interlocked handle alloc/destroy ops per exception throw, removes OOM fallback paths, and unblocks cDAC unification. Thread::m_LastThrownObjectHandle remains as-is (separate work item). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
adffa37 to
7aae554
Compare
The ExInfo::m_exception field was being reported to the GC twice: once via GCPROTECT_BEGIN and once via ExInfo chain scanning in ScanStackRoots. The CLR code guide (section 2.1.5) explicitly states that reporting the same location twice corrupts the GC's relocation logic. Remove the GCPROTECT_BEGIN/END for m_exception and rely solely on chain scanning (matching NativeAOT's model). Add Thread::ObjectRefProtected calls in checked builds to satisfy the debug OBJECTREF tracking table. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| // before any debugger notification fires. Assert this invariant in debug builds. | ||
| #ifdef _DEBUG | ||
| ExInfo* pTracker = pThread->GetExceptionState()->GetCurrentExceptionTracker(); | ||
| if (pTracker != NULL && pTracker->m_exception != NULL && pThread->m_LastThrownObjectHandle != NULL) |
There was a problem hiding this comment.
Are there any case where we have the exception stashed in m_LastThrownObjectHandle, but the exception tracker does not exist anymore?
I am wondering why we need m_LastThrownObjectHandle with the complicated system to keep it in sync with the tracker.
There was a problem hiding this comment.
Yes, m_LastThrownObjectHandle is read after the tracker is gone by EX_CATCH block via CLRLastThrownObjectException.
For the debugger path you, the tracker IS alive, but we need the handle for the MODE_ANY contract (or modify it). Removing LTO entirely would require reworking CLRLastThrownObjectException.
There was a problem hiding this comment.
@janvorli Do you think it would be feasible to stop synchronizing m_LastThrownObjectHandle with pTracker->m_exception?
There was a problem hiding this comment.
So, the m_LastThrownObjectHandle is needed to bridge the time when managed exception crosses runtime (on Windows also external native code). When SEH COM exception reaches a managed frame after the exception passed through native frames, the ProcessCLRException on Windows or the handler of the PAL_SEHException on Unix uses it to continue propagating the same managed exception object. It seems to me though that we could just create the handle (or set its value) when we are getting rid of the ExInfo. That would simplify the maintenance. But there may be some dragons hidden.
There was a problem hiding this comment.
It seems to me though that we could just create the handle (or set its value) when we are getting rid of the ExInfo
Yes, it is what I meant.
|
@EgorBot -amd --filter "Exceptions.Handling.CatchAndThrowOtherDeep*" |
This comment was marked as outdated.
This comment was marked as outdated.
1 similar comment
This comment was marked as outdated.
This comment was marked as outdated.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🤖 Copilot Code Review — PR #127300Note This review was generated by GitHub Copilot using multi-model analysis (Claude Opus 4.6 primary + GPT-5.3-Codex cross-validation). Holistic AssessmentMotivation: Well-justified. Replacing the GC handle ( Approach: The approach is sound and mirrors NativeAOT's Summary: Detailed Findings✅ GC Root Scanning — Correct and completeThe new ExInfo chain scanning in No GC window exists: The ExInfo constructor links into the chain ( Flagged by: primary review + GPT-5.3-Codex (both agree) ✅ GCPROTECT Removal — Correct per CLR code guideThe three removed The remaining ✅ DAC Pseudo-Handle Pattern — Correct for dump analysisThe DAC code (task.cpp, dacimpl.h) passes
Flagged by: primary review + GPT-5.3-Codex (both agree safe) ✅ ObjC Interop — MODE_COOPERATIVE transition correctThe signature change from ✅ StackTraceInfo::AppendElement — Type-pun is functionally correctThe calls passing
|
Note
This PR was authored with the assistance of GitHub Copilot.
Summary
Replace the GCHandle-based
m_hThrowablefield in ExInfo with direct use of the existingm_exceptionOBJECTREF field, matching NativeAOT's approach.Motivation
CoreCLR's ExInfo stored exception objects via two redundant fields:
OBJECTHANDLE m_hThrowable(GC handle table indirection) andOBJECTREF m_exception(direct pointer used by the new EH path shared with NativeAOT). NativeAOT only hasm_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
OBJECTHANDLE m_hThrowablefrom ExInfo, saving 8 bytes (x64) / 4 bytes (x86)ScanStackRoots(gcenv.ee.cpp), mirroring NativeAOT'sGcScanRootsWorker— this keeps superseded exception objects alive without handlesGCPROTECT_BEGIN(exInfo.m_exception)from all 3 dispatch entry points — the chain scanning already reports&m_exceptionto the GC, and reporting the same location twice corrupts the GC's relocation logic (clr-code-guide.md §2.1.5). Debug OBJECTREF tracking is satisfied viaThread::ObjectRefProtectedin the ExInfo constructor.GetThrowableAsHandle()entirely — all callers migrated to useGetThrowable()(OBJECTREF) orm_LastThrownObjectHandle(real handle on Thread)SetThrowable()entirely — managed EH code writesm_exceptiondirectly;SafeSetThrowablesnow only updatesm_LastThrownObjectHandleviaSetLastThrownObject. TheSetThrowableErrorCheckingenum andSTEC_*constants are also removed.StackTraceInfo::AppendElementpreallocated-exception check: changed fromIsPreallocatedExceptionHandle(compares handle pointer against globals — always false for pseudo-handles) toIsPreallocatedExceptionObject(ObjectFromHandle(...))(compares actual object identity)m_LastThrownObjectHandle(real handle) for current exception, and target address ofm_exceptionfield for nested exception iterationThrownObjectHandle→ThrownObject(direct pointer, no handle dereference);GetCurrentExceptionHandlereturns field address as pseudo-handle for backward compatibilityWhat stays unchanged
Thread::m_LastThrownObjectHandleremains as an OBJECTHANDLE — it is required by the ICorDebug managed debugging protocol (SendExceptionHelperAndBlockisMODE_ANY, right-side debugger reads through handle cross-process viaBuildFromGCHandle).Efficiency
Per exception throw, this eliminates:
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