Skip to content

Interpreter EH implementation #116046

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 2, 2025
Merged

Interpreter EH implementation #116046

merged 5 commits into from
Jun 2, 2025

Conversation

janvorli
Copy link
Member

This change implements EH support in the interpreter compiler and execution parts.

Here is a summary of the changes:

On the compilation side:

  • Adds support for CEE_THROW, CEE_RETHROW, CEE_ENDFILTER, CEE_ENDFINALLY, CEE_LEAVE and CEE_ISINST opcodes
  • Adds building of EH info with IR code offsets
  • Implements proper funclet handling in the same way the JIT does. All handlers and filters are moved to the end of the method and out of any try ranges recursively to enable proper behavior of the EH.
  • Fixes a bug related to wrong SVar and and an issue with IL offset of inserted instructions in AllocOffsets
  • Fixes a bug in the CEEOpcodeSize - off by one check for the end of the code

On the execution side:

  • Add funclet start address extraction
  • Add calling funclets in the interpreted code
  • Removed GCX_PREEMP_NO_DTOR from the CallDescrWorkerUnwindFrameChainHandler, because it was not correct
  • Added new IR opcodes:
    • INTOP_LOAD_FRAMEVAR to load parent frame stack pointer in filter funclets that need to run in the context of the parent, but at the top of the current interpreter stack.
    • INTOP_RETHROW to rethrow an exception
    • INTOP_CALL_FINALLY to call finally funclet in non-exceptional cases
    • INTOP_LEAVE_FILTER to exit a filter funclet and return the filter result
    • INTOP_LEAVE_CATCH to exit a catch handler and return the resume address
  • Added calls to COMPlusThrow for division by zero, stack overflow and few other exceptions where the interpreter had just TODO and assert for adding those.
  • Modified the InterpExecMethod so that extra information can be passed in case of a funclet invocation.

It also adds tests to verify various interesting EH scenarios

Here are some more details on moving out the funclets: For each finally funclet, we create a finally call island that stays in the code where the original finally was. That island calls the finally and then branches to the next (outer) finally if any. The last finally call island branches to the actual leave target.
The interpreter compiler generates a separate sequence of finally call islands for each leave instruction target. And when the leave is executed, it jumps to the beginning or into the middle of the chain. In other words, for example, if all leave instructions in the method go to the same target, there would be just one call finally island chain.

@janvorli janvorli added this to the 10.0.0 milestone May 27, 2025
@janvorli janvorli self-assigned this May 27, 2025
@Copilot Copilot AI review requested due to automatic review settings May 27, 2025 22:28
@janvorli janvorli requested review from BrzVlad and kg as code owners May 27, 2025 22:28
Copy link
Contributor

Tagging subscribers to this area: @BrzVlad, @janvorli, @kg
See info in area-owners.md if you want to be subscribed.

Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds full exception-handling (EH) support to the interpreter, covering both compilation and execution phases. It introduces new IR opcodes, builds EH metadata, implements funclet-based EH in the interpreter, and adds tests for various EH scenarios.

  • Added support for EH opcodes (throw, rethrow, leave, filters, finally) and IR-level exception clauses
  • Updated interpreter execution (InterpExecMethod) to dispatch filters, catches, and finally blocks
  • Built and emitted EH information (funclet offsets, EH tables) and wired new EH helpers

Reviewed Changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/coreclr/vm/prestub.cpp Introduce retVal storage for interpreter return value
src/coreclr/vm/jithelpers.cpp Swap in DispatchRethrownManagedException call
src/coreclr/vm/interpexec.h Extend InterpExecMethod signature with EH args
src/coreclr/vm/interpexec.cpp Handle new INTOP_* opcodes and null/overflow checks
src/coreclr/vm/exceptionhandling.h Declare DispatchRethrownManagedException overloads
src/coreclr/vm/exceptionhandling.cpp Implement DispatchRethrownManagedException and adjust dispatch logic
src/coreclr/vm/eetwain.cpp Flesh out InterpreterCodeManager::CallFunclet
src/coreclr/vm/codeman.h Update GetFuncletStartOffsets declaration
src/coreclr/vm/codeman.cpp Implement funclet start address/offset enumeration
src/coreclr/interpreter/intops.def Add EH-related INTOP definitions
src/coreclr/interpreter/intops.cpp Fix CEEOpcodeSize boundary condition
src/coreclr/interpreter/eeinterp.cpp Invoke BuildEHInfo() during compilation
src/coreclr/interpreter/compileropt.cpp Correct ilOffset propagation and SetSVar call
src/coreclr/interpreter/compiler.h Extend compiler IR and block structures for EH
Comments suppressed due to low confidence (3)

src/coreclr/vm/exceptionhandling.cpp:2138

  • Removing the GCX_PREEMP_NO_DTOR switch back to preemptive GC in the non-unwind (search) path can leave the runtime in cooperative mode and break stack walking. Reintroduce a mode switch before returning ExceptionContinueSearch.
else if (IS_UNWINDING(pExceptionRecord->ExceptionFlags))

src/coreclr/vm/jithelpers.cpp:1357

  • The identifier exceptionFrame is not declared in this scope, leading to a compile error. You need to pass a valid CONTEXT* (e.g., capture it or fetch it from the exception tracker) to DispatchRethrownManagedException.
DispatchRethrownManagedException(exceptionFrame.GetContext());

src/coreclr/vm/interpexec.h:77

  • There are now two overloads of InterpExecMethod (one with default ExceptionClauseArgs and the original three-argument version), which can be confusing. Consider removing the three-argument prototype and relying solely on the default parameter.
void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFrame *pFrame, InterpThreadContext *pThreadContext, ExceptionClauseArgs *pExceptionClauseArgs = NULL);

@janvorli janvorli force-pushed the interpreter-eh branch 2 times, most recently from aee764e to 02cb97b Compare May 27, 2025 22:34
This change implements EH support in the interpreter compiler and
execution parts.

Here is a summary of the changes:

On the compilation side:
* Adds support for CEE_THROW, CEE_RETHROW, CEE_ENDFILTER,
  CEE_ENDFINALLY, CEE_LEAVE and CEE_ISINST opcodes
* Adds building of EH info with IR code offsets
* Implements proper funclet handling in the same way the JIT does. All
  handlers and filters are moved to the end of the method and out of any
  try ranges recursively to enable proper behavior of the EH.
* Fixes a bug related to wrong SVar and and an issue with IL offset of
  inserted instructions in AllocOffsets
* Fixes a bug in the CEEOpcodeSize - off by one check for the end of the
  code

On the execution side:
* Add funclet start address extraction
* Add calling funclets in the interpreted code
* Removed GCX_PREEMP_NO_DTOR from the
  CallDescrWorkerUnwindFrameChainHandler, because it was not correct
* Added new IR opcodes:
  * INTOP_LOAD_FRAMEVAR to load parent frame stack pointer in filter
    funclets that need to run in the context of the parent, but at the
    top of the current interpreter stack.
  * INTOP_RETHROW to rethrow an exception
  * INTOP_CALL_FINALLY to call finally funclet in non-exceptional cases
  * INTOP_LEAVE_FILTER to exit a filter funclet and return the filter result
  * INTOP_LEAVE_CATCH to exit a catch handler and return the resume
    address
* Added calls to COMPlusThrow for division by zero, stack overflow and
  few other exceptions where the interpreter had just TODO and assert
  for adding those.
* Modified the InterpExecMethod so that extra information can be passed
  in case of a funclet invocation.

It also adds tests to verify various interesting EH scenarios

Here are some more details on moving out the funclets:
For each finally funclet, we create a finally call island that stays in
the code where the original finally was. That island calls the finally and
then branches to the next (outer) finally if any. The last finally call
island branches to the actual leave target.
The interpreter compiler generates a separate sequence of finally call
islands for each leave instruction target. And when the leave is
executed, it jumps to the beginning or into the middle of the chain.
In other words, for example, if all leave instructions in the method go
to the same target, there would be just one call finally island chain.
AddIns(INTOP_LOAD_FRAMEVAR);
PushInterpType(InterpTypeI, NULL);
m_pLastNewIns->SetDVar(m_pStackPointer[-1].var);
EmitStind(interpType, m_pVars[var].clsHnd, m_pVars[var].offset, true /* reverseSVarOrder */);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we need the indirect from LoadVar we should have it here too i think

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what you mean, can you please explain it in more detail?

@janvorli
Copy link
Member Author

There is some problem with the IL to native map generation when running with checked build of the runtime. Debug build works fine. I am looking into it.

The call finally islands were not classified by the clause they are in.
Their instructions had IL offsets set to the offset of the finally
block, but they need to have no IL offset as they are not generated
from IL.
@janvorli
Copy link
Member Author

I've fixed the problem with the checked build of the tests. The problem was that the call finally islands didn't have the clauseType set and so when generating native form of the EH info, they were missing in the try regions ranges.

@kg
Copy link
Member

kg commented May 29, 2025

I forgot to comment when I finished going over this, it all LGTM. I'm fine with landing it.

@BrzVlad
Copy link
Member

BrzVlad commented Jun 1, 2025

Had this written down after a call a while ago with @janvorli. Might be useful for visualization.

try {
    try {
        if
            leave end2; // INTOP_LEAVE -> jump to finally_island_1a 

        leave end1; // INTOP_LEAVE  -> jump to finally_island_1b
    } catch {
        leave end1; // INTOP_LEAVE -> jump to finally_island_1b
    } finally1 {
        endfinally
    }
    -- finally_island_1a --
    call_finally finally1
    br finally_island_2
    --------------------- 
    -- finally_island_1b --
    call_finally finally1
    br end1
    --------------------- 
    end1;
    leave end2; // INTOP_LEAVE -> jump to finally_island_2
} catch {
    leave end2; // INTOP_LEAVE -> jump to finally_island_2
} finally2 {
    endfinally
}
-- finally_island_2 --
call_finally finally2
br end2;
---------------------
end2;

@BrzVlad
Copy link
Member

BrzVlad commented Jun 2, 2025

Looks great! Thanks!

@janvorli janvorli merged commit 8d59055 into dotnet:main Jun 2, 2025
102 checks passed
@janvorli janvorli deleted the interpreter-eh branch June 2, 2025 16:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants