Open
Description
With #2145 we will have the basic runtime mechanism for dispatching exceptions in place, however, much work still remains for the implementation to reach non-prototype level of quality.
Correctness:
- Inter-frame dispatch and filter ordering.
- Capture the shadow stack of the throwing frame inside the native exception.
- Call dispatchers "on" this shadow stack (will require some dispatch pieces to be moved to native code).
- Capture the list of pending fault/finally handlers in the first pass and invoke them in the second.
- Should the managed or native exception carry this list? Can we avoid heap allocations for this case by using the shadow stack somehow?
- Hard problem: dynamic
stackalloc
live into handlers.- Current plan of action is to implement this with a custom stack-like allocator.
- We should find/write an extensive set of tests for this scenario.
- Fixed in [NativeAOT-LLVM] Implement two-pass exception handling #2284.
- Capture the shadow stack of the throwing frame inside the native exception.
- We need to be able to identify the "top frame", one after which the exception will go unhanded. This implies wrapping RPI methods inside catch-all
try/catch
(perhaps a native one):- Verify: generated (managed)
main
,Thread.Start
entrypoints. - Fixed in [NativeAOT-LLVM] Implement unhandled exception handling #2274.
- Verify: generated (managed)
- Related: we need to properly handle unhandled exceptions.
- Invoke relevant SPCL callbacks.
- Dump the stack trace.
- Fixed in [NativeAOT-LLVM] Implement unhandled exception handling #2274.
- I think there are also some first-chance handlers we need to invoke when throwing.
- Exceptional exits from runtime imports will fail to restore the shadow stack, leading to shadow stack top corruption. This should be fixed by moving the shadow stack top restore logic into RPI frames.
- Fixed in [NativeAOT-LLVM] Implement RPI #2208.
throw null
should be translated intothrow new NullReferenceException()
.- Rethrow currently recaptures the stack trace. It should not.
- Throwing filters should return "continue search".
- Throwing faults are not handled correctly, see
src\tests\JIT\Methodical\eh\nested\general\methodthrowsinfinally_d.csproj
. wasi-wasm
needs to be done using "manual" unwind (in both passes). Fortunately, that is feasible because exceptions cannot propagate across any managed<->unmanaged boundaries, even when it comes the the native runtime (in other words, the runtime can only invoke managed code when in a tail position).
Performance / code size:
- Utilize WASM EH instead of JS-based Emscripten exceptions. We will still want to retain the C++-based implementation so that porting to non-WASM platforms is possible.
- Review the generated WASM code for obvious inefficiencies in code size; adapt codegen and dispatchers as appropriate.
- See [NativeAOT-LLVM] Implement WASH-EH-based exception dispatch #2291 (comment). Nothing planned at this point in time.
- We currently capture the stack trace when throwing the exception. This is expensive. Design a cheaper scheme.
- Transition to funclets only for finally/filter clauses.
- Insert unhandled exception handlers in the RPI helper.
- Coalesce virtual unwind frame pop on method exit with the last NOT_IN_TRY setting.
- Do not require pop calls for top-level faults which do not access the unwind index.
- Better CQ heuristics for unwind index insertion: do not expand the group if the predecessor is in a different protected region, or is a throw helper block, etc.
- Skip exceptional predecessors in unwind index insertion for which we know the edge is dead because the source block never throws.
- Possible ideas:
- Do not pass the shadow stack to the catch helper (somehow).
- Throw in the catch helper instead of using rethrow (measured perf degradation is ~2.5x).
- Create specialized catch helpers for common indices (2/3).
- Define the unwind index on entry to catch handlers via the helper.
- Shrink the EH info by not wasting leading bytes on padding. Will require adding support for unaligned relocations to the object writer, using
Unsafe.ReadUnaliged
at runtime and reworking the format a bit. - Do not force locals not live cross-funclet to memory. See notes in [NativeAOT-LLVM] Do not use funclets for faults #2443.
Debugging:
- Generate proper function-level DWARF metadata for funclets.
- Implement .NET-level stack trace (with collapsed funclets). Note we are currently emitting the MethodRVA-to-token map, which is not used.
- Aborts induced by unhandled exceptions should not print a stack trace - we will have printed our own already.
Documentation:
- Write a comprehensive document about the design and implementation of the scheme we'll end up with.
Code cleanup:
- Do not include
ExceptionHandling.cs
i. e. the usual EH dispatch, in the WASM build.