-
Notifications
You must be signed in to change notification settings - Fork 24
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
Implementing the exception handling proposal in Wasmtime #36
base: main
Are you sure you want to change the base?
Conversation
FYI, there are some discussions around how to support exception-throwing calls in the register allocator over in bytecodealliance/regalloc2#186 and some of that seems relevant for anyone interested in this RFC. |
We do not define a CLIF instruction for throwing an | ||
exception. Instead, exception throwing must be done indirectly via an | ||
imported function (e.g. a Wasmtime builtin libcall implemented in the | ||
host/engine). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suspect we might actually want an instruction for throwing exceptions: for the overflow-flag ABI approach, we ideally don't want to tail call out to a custom asm-implemented function just to move the exception payload into a particular register, set the overflow flag, and return. We want to do that stuff inline. But I think we would be forced to do that suboptimal approach if we don't have a dedicated instruction.
Of course, when we are doing C++ ABI exceptions and DWARF, we will want to call out to _Unwind_RaiseException
and friends instead.
I think we can choose between the two options in instruction selection with different lowering rules that look at the current calling convention. For the overflow-flags ABI, we'll need a custom calling convention, say tail-overflow-exceptions
or something instead of our existing tail
calling convention, and we can check for that or not.
The other option would be a cranelift setting that gets set by the clif producer, similar to how TLS is done. This is a little funky to me tho because we need the new calling convention either way to control whether we do things like clear flags before regular returns or not, and so setting this theoretical option to the overflow-flags version of exceptions can still only work with that special calling convention, so it feels like we'd end up with two knobs to control roughly the same thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for writing all this up! I'll cc @bjorn3 as well here since they've done work in this area with rustc_codegen_cranelift and likely have thoughts as well.
One thing which might also be worth noting in this RFC is that we probably can't do away with the longjmp/setjmp that Wasmtime uses today to implement traps. Notably that enables recovery from a signal handler and additionally enables O(1) recovery in "deep" situations like stack overflow. I was hoping we could use exceptions to implement that as well but I'm less sure of that now.
pointer-sized integer (morally the exception value), e.g. in CLIF | ||
syntax | ||
```clif | ||
catch block123(v456: i64): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's some discussion on bytecodealliance/regalloc2#186 about this too, but I think there's a case to be made to not do this because at least in wasm you can branch to unwind handlers just like normal blocks so wasm is at least one consumer who will need to work around this restriction otherwise.
into a `catch` block. Instead, the control flow edges to `catch` | ||
must come via a `try_call` instruction. | ||
|
||
* A new call instruction `try_call <ok_label>, <exception_label>`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One part perhaps worth pointing out here is that DWARF supports multiple unwind locations per try_call
, so <exception_label>
may want to actually be a list of labels. I believe that @bjorn3's initial work for rustc_codegen_cranelift modeled this with a JumpTable
where the "default label" was the ok_label
and everything else was an unwind location.
Before returning normally any function must clear the flag, e.g. | ||
|
||
``` | ||
test al, al |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One possible alternative to the overflow flag is the carry flag which has jc
for jumping and dedicated clc
and stc
instructions for clearing/setting the carry flag (they're a single byte too!)
We reckon this approach is relatively low overhead, and it something | ||
we can confidently implement correctly more quickly than the side | ||
table or DWARF unwinder strategies. Adopting this strategy would allow |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing worth pointing out about this approach is that given the non-zero-cost in the non-exceptional case it will likely prevent turning this proposal on by default. The cost would be incurred for users who don't use exceptions at all unless Wasmtime implements a form of detection of exception-using-instructions which I think could get particularly hairy in the cross-instance semantics below.
* How should we represent three-way results in the Wasmtime public | ||
API? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this open question I think it'd be reasonable to take inspiration from the JS API for exception handling, notably we'd have exported Tag
structures which the embedder could create or acquire from instances. Exceptions themselves would probably be modeled as something that can be converted to anyhow::Error
and then host functions would return that error to indicate they want to throw an exception. Afterwards how this is represented internally from that point is just an implementation detail.
* No support for the legacy exception handling | ||
revision. Justification: the legacy revision is being phased out. | ||
|
||
* No support for unwinding across host frames. Justification: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One could add to the justification that it is, in the general case, impossible to unwind arbitary host code. Examples:
- .NET Jitted host code - it won't have any unwind info Wasmtime could hope to understand (.NET runtime uses an internal Windows-like unwind info even on Unix OSes).
- C code compiled without unwind info (and with omitted frame pointers, for good measure).
- Windows x86 native code does not support virtual unwinding at all.
for exception handling is (at least) of interest to C++, Kotlin, and | ||
OCaml toolchains. The proposal is also a prerequisite for the [stack |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for exception handling is (at least) of interest to C++, Kotlin, and | |
OCaml toolchains. The proposal is also a prerequisite for the [stack | |
for exception handling is (at least) of interest to C++, .NET, Kotlin, and | |
OCaml toolchains. The proposal is also a prerequisite for the [stack |
The workaround we use in one of our toolchains to emulate exceptions has 10%+ code size cost (and probably even higher execution cost).
Thanks for writing this up! I have a few thoughts mostly on the design of the CLIF functionality to encode exceptional control flow; I see a few others have commented on the points about special ( To set a baseline first, I'll suggest the basic principle of: IR design should be as orthogonal as possible, i.e., features compose and special-cases or unsupported corners that require special handling are minimized. As a corollary of that, if existing analysis and transform passes can work without having to be modified to be aware of exceptions, all the better. (This is the end-game of "put exceptional edges into the ordinary CFG", IMHO, with CLIF Design QuestionsSo I see at least three design questions here:
Q1: Distinct Kind of Catch-BlocksI'd like to propose that the first question be resolved early to: catch-targets are ordinary CFG blocks (as also noted elsewhere in this PR). My reasoning is straightforward: a new kind of block, with its own restrictions, adds cognitive overhead and correctness questions to every analysis and pass in the compiler, increasing likelihood of bugs. Furthermore, it's not clear that there are reasons that require catch-targets to be distinct. We will want to codegen them as we do other basic blocks; an unwind that moves control to the handler address is just like an ordinary jump from the predecessor block. (If I'm missing some reason why they msut be distinct, please let me know!) Q2: BlockparamsThe next question is whether we allow blockparams on blocks that are targets of A few concrete examples of passes that work by adding blockparams, and would be difficult to write if we had an "exception catch blocks are a special case" rule:
One objection might be that exceptional control flow could interact poorly with edge-moves, i.e., the moves that the register allocator has to insert to actually put blockparams in place, since we don't codegen the branch, it just "happens" via the external unwinder. However a catch-block reached from a Q3:
|
💯
I had been assuming that we couldn't treat landing pads as normal blocks, and instead like alternative function entry points. But I think that was a mistaken assumption that I never questioned. I might have been assuming that we wouldn't allow the reuse of any values in the landing pad, as a simplification? But that seems overly extreme now. Anyways, if a block is used as both a landing pad and a regular control-flow successor, then the regalloc constraints of the Anyways, if the
I would say that if we allow splicing the payloads into the block parameters at arbitrary positions, eg
then we should/must keep the keywords. If we always append or prepend, then implicit seems fine by me. The generality of unconstrained splicing seems nice from a user perspective, but also maybe like something we won't actually need. I think if we can't think of a we-will-definitely-need-it-for-X use case, we should just implicitly append or prepend.
💯 |
I'm relatively convinced at least right now that "normal jump" is the best way to think of (and design to ensure) the unwinder -- we'll indeed want to restore callee-saves as we unwind more nested frames for this to be the case. In other words, my mental model for an exceptional return is "just like a normal return except PC/RIP is over here", plus register(s) set with exceptional state, just as register(s) are set with return values on normal returns. (Someone please correct me if this is wrong!) An alternative world where catch blocks are separate function entries seems much more "wild" to me in the sense that it breaks assumptions in lowering and regalloc. One slightly in-the-weeds but relevant detail here is that as we compute the lowering block order from CLIF, and generate VCode blocks, if a catch-block is also a target of a normal branch, we'll end up splitting the critical edge: the exceptional edge comes from a block with multiple successors, and goes to a block with multiple predecessors. This will require a little bit of care to get right when we generate the unwind tables (usually the main block is associated with the CLIF-level label but here we want the block with edge-moves). (EDIT: note this is also the case if we keep the catch-blocks as a separate kind of block, unreachable from normal branches, because two or more
Yeah, I'd tend to agree with the YAGNI principle here -- and between append and prepend, if no other reasons to lean either way, I might suggest that we append, because then we preserve the property that the block-call args on the targets line up with blockparams, and the new thing ( |
Exactly!
I believe I used prepend in my current implementation as cranelift-frontend appends the extra blockparams necessary for handling variables in SSA form to the user defined blockparams, so prepending the args for invoke/try_call makes cranelift-frontend handle it correctly without needing any changes. |
This RFC proposes to implement the exception handling proposal in Wasmtime. At the time of writing, exception handling is a phase 4 proposal.
I think there are lots of details worth discussing about possible designs and strategies on how to realise them. I am hoping that this document can be used to seed those discussions here.
I would like to give credit to @fitzgen for guidance on how to put this RFC together as well as helping with developing the ideas, thanks!
Rendered.