Implementing the exception handling proposal in Wasmtime#36
Implementing the exception handling proposal in Wasmtime#36fitzgen merged 7 commits intobytecodealliance:mainfrom
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. |
alexcrichton
left a comment
There was a problem hiding this comment.
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.
|
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. |
|
Chris, Nick, and I talked about this in-person at the CG meeting over dinner yesterday so I wanted to drop some notes here from what we talked about so we don't forget:
|
|
@dhil is it okay if I take over this RFC to get it across the finish line? |
Yes absolutely! |
|
Just pushed a pretty major overhaul to this RFC, incorporating changes from this discussion and others:
Please take another look! |
alexcrichton
left a comment
There was a problem hiding this comment.
Thanks for taking this on @fitzgen, it's looking very good to me 👍
cfallin
left a comment
There was a problem hiding this comment.
Thanks for putting this together; the details all basically look good to me! Just one uncertainty/question below.
cfallin
left a comment
There was a problem hiding this comment.
A few more thoughts on another read-through. Two things to clarify / add more detail on; no issues with the approach at all.
* No two-phase exceptions * Unwinding regular `call`s * Why we do not have `try_call_indirect` * Why no `throw` or `try` instruction
|
As we seem to have reached pretty broad consensus, and feedback has moved more into the realm of typo fixes and minor clarifications, I'd like to propose that we merge this RFC! Motion to FinalizeDisposition: Merge As always, details on the RFC process can be found here: https://github.com/bytecodealliance/rfcs/blob/main/accepted/rfc-process.md#making-a-decision-merge-or-close |
|
I'd second the motion to merge 👍 |
cfallin
left a comment
There was a problem hiding this comment.
👍
At the risk of going to pun-jail, I have to note that I find this RFC... exceptional. Seconding!
|
As there has been signoff from representatives of two different BA stakeholder organizations, this RFC is now entering its 10-day Final Comment Periodand the last day to raise concerns before this RFC merges is 2025-03-21. Thanks everyone! |
|
At the risk of slightly re-opening Pandora's box, I have a... final comment... during the Final Comment Period. I'm ~1.5 days into implementing the Cranelift side of exception handling right now, and I have been realizing in a fairly deep way how the block-parameter strategy we have in this RFC may be suboptimal. In brief, the problem is that breaking the invariant that the block-call arguments in the target match up with the parameters on that block is leading to a lot of refactoring and gross hacks throughout the compiler. I don't doubt that I could continue to bulldoze through, but I'm worried that this is a strong signal about design coherence that will lead to future pain and bugs as we work with the IR, so I took a step back to think a bit (and talk with @fitzgen offline earlier today). We originally decided above that we would write We took this approach because we wanted to achieve a few goals: successor blocks should not be a special category or specially restricted; the IR should encode the invariant that function returns are only valid on normal return, and an exception payload is only valid on catching an exception; and we shouldn't do anything weird with SSA, like define return values and immediately use them in the I believe I have a better solution now though that still preserves these properties, and avoids the higher-than-anticipated impedance mismatches. Namely:
The latter point means that the normal-return values are defined even on exceptional returns. This may seem like a semantic lie. I have what I think is a reasonable answer that technically avoids adding UB to the IR: we could provide in the unwind metadata information about where return values go (registers/stack locations), and specify that it is up to the embedder to either fill those values in, or guarantee that it does not use the values on exceptional paths. (In effect, the values are always "returned", it's just that the unwinder decides the return values on unwinding; and Wasmtime is free to decide that the current value in Anyway, I'd be curious what others think, and I'm happy to fill in more about the pain-points discovered. (Slightly tangential but worth saying, IMHO: I personally think we should always at least begin implementation or prototype before fully merging an RFC, and this is why -- there are some things we just can't know fully until we work through all the details!) |
cg_clif absolutely needs it. |
|
Hmm, that is good to know. And there isn't a way to tunnel the state through, e.g., a TLS slot? I suppose the issue here is that you don't control the stdlib so we basically need the same capability as LLVM for the native-code case... I think one way to express what I'm finding in implementation is that there has to be something weird and different about exceptional edges, because they do introduce this payload. That payload has to come out of thin air somehow. The ways we have are:
I suppose the last option -- one I don't like, but just to name it -- is to say that both the normal returns and exceptional returns arise as normal defs, so the instruction results for a So we either punt on the problem, or we have awkward logic on edges, or awkward logic on blocks, or awkward logic on calls. The native-code case apparently means we can't punt on the problem. I'm not sure what to make of this -- we can discuss more in the Cranelift meeting tomorrow I suppose. |
Yeah, rust_eh_personality only passes it as landingpad argument. Did you see how I did the landingpad arguments in my branch by the way. While it adds some complexity, it didn't seem like it added all that much complexity. |
|
We discussed this in the Cranelift meeting this morning and came to a relatively reasonable design point, which I'm actually fairly happy about: we decided to reify the "arising in the middle of an edge" values, the return values and exception payloads, as a new kind of "placeholder argument" in the block calls. There are some sketches of this above but basically it looks like: Importantly, the args themselves are a sum-type of "normal value" or "placeholder" (try-call return, try-call exception payload, other in the future?). We don't define a new kind of value; these args are not values. This is nice for a few reasons:
I'll continue building this and will say more if there are other unanticipated issues, but I don't expect there to be at the moment! |
Rather than concatenating or appending them onto the block calls. Chris's prototyping revealed that this is a better approach for our existing code base.
|
I've updated the RFC based on the final comments and the CLIF design tweaks Chris made during his prototyping. Given that the prototyping has merged into Cranelift Final Comment PeriodDisposition: merge. The last day to raise concerns before this RFC merges is 2025-05-03. Thanks again, everyone! |
|
Whoops, forgot to merge this on the third, so it got some extra FCP time. Thanks @dhil for writing the first draft of this RFC and to everyone who provided feedback, brainstormed, and helped improve it! |
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.