-
Notifications
You must be signed in to change notification settings - Fork 35
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
Understanding catch_all #128
Comments
I agree with everything in this section. Is it correct that you're not identifying any problems in this section, but rather just writing down your understanding of the situation to make sure everyone is on the same page?
This does seem unfortunate, but for native C++ exceptions,
Meta-comment: Arguing that something is not strictly necessary but predicating that argument on something else having different semantics is quite a rhetorical leap and reads very much like "we shouldn't use your proposed semantics because my proposed semantics work, too." When you make this kind of argument, it gives the impression that you have a vendetta against certain proposed features, in this case
IIUC, this is essentially the same problem you raised in #101, where the general conclusion was that although it is true that modules can use
I don't fully follow the examples in this section, but it looks like you're identifying a new forward-compatibility problem. Could you break this out into its own issue with a more detailed illustration of the problem? It would also be good to reference specific languages/systems because I'm not familiar with any of the behaviors you describe. (Prompting the user for a value!?)
I fully agree with this statement. It's important to note that ABI compliance cannot (and should not) be validated by engines. So if one ABI wants to disallow interfering with foreign exceptions, that's fine, and if another ABI wants to allow such interference, that should be fine, too. This opening post was a big mix of explanations, discussions of issues, statements of principles, etc. The issues I identified in it were the composability issue originally discussed in #101 and the issue of interactions with various foreign exceptions systems, which I requested to be split into its own issue. Are there any issues you raised here that I missed? Can you provide a brief summary of the action items you're introducing, i.e. investigations we need to do and issues we need to settle? |
My understanding is that the primary motivation for
catch_all
comes from C++'scatch (...)
, which is required to catch all C++ exceptions, but which in some ABIs is supposed to catch foreign exceptions. Note that there is flexibility here, and in general C++ has a fair amount of flexibility in its semantics for exceptions. Another example of flexibility is that destructors may or may not execute for uncaught exceptions; that choice is left up to the ABI, and I believe largely in order to permit both single-phase and two-phase exception-handling implementations. Similarly, I believe leaving the handling of foreign exceptions up to the ABI was done in recognition of the fact that there are many semantics for and implementations of exceptions in other systems, and there is no way a single C++ semantics could anticipate all those systems and how they could/would interact with C++.In other issues, I suggested that C++'s
catch (...)
could be implemented by anunwind
that branches out of theunwind
block. @aheejin and @dschuff raised concerns about how this would interact with two-phase exception handling, giving good arguments for why there deserves to be a distinction between that approach andcatch_all
. But those concerns made me wonder more generally howcatch_all
is supposed to interact with other exception systems we would like WebAssembly to be able to support. I wanted to devote this issue to understanding those interactions.Bypassing
catch_all
First, I want to point out what
catch_all
cannot (or at least should not) do.catch_all
cannot prevent control from moving from inside thecatch_all
to outside thecatch_all
within the current stack through other means. Here are three examples illustrating why.Suppose we extend WebAssembly with stack inspection so that, say, a program can mark its own linear-memory-managed GC roots on the stack. That likely requires executing code associated with frames on the current stack belong with the current program. Some of these frames might be outside a
catch_all
, so it would be problematic ifcatch_all
prevented code from executing on those frames. That code could than branch (without unwinding) outside of the inspection code. There's no exception here, so the body ofcatch_all
shouldn't be expected to catch here.The
catch_all
could be executed on a "child" stack, and a function call within thecatch_all
could switch control (or suspend control) to a "parent" stack.The program could trap within the
catch_all
and the embedding language could catch the trap outside thecatch_all
.Note that in all these cases the code outside of the
catch_all
has to set up something for the code insidecatch_all
to connect to, whether it's a stack mark, a stack allocation, or host privilege. So it's not like control is able to go anywhere arbitrarily. In fact this pattern is useful for supporting sandboxing. Also, notice that in each case the portion of the stack withcatch_all
is not unwound. This again is useful to support, say for sandboxing purposes (and fault-tolerant programming already has to consider unwinders as not guaranteed to execute for a variety of reasons, like trapping).Supporting
throw;
withincatch (...)
This GitHub issue overlaps with #127 because the expectation is that
rethrow
would be needed to support C++throw;
withincatch (...)
. But at present therethrow
instruction would only be able to implement the most trivial uses ofthrow;
, i.e. the ones that occur syntactically within thecatch (...)
. Those uses could also be supported by havingend
ofcatch_all
rethrow the exception, so arethrow
instruction is strictly speaking not necessary for this use case.But let's take a step back and consider whether we should support more advanced uses of
throw;
for foreign "exceptions", i.e. uses that occur within different dynamic contexts. This would make it possible (through some layers of cross-program calls) for program A to intercept exceptional control flow of program B (which may or may not be conceptual "exceptions/errors") and then have that exceptional control flow be evaluated within a different evaluation context of program B than its original intent without B exporting its exception event. For first-class stacks, we decided not to provide direct support for stack duplication because we felt it could be used as a way to put programs in situations they were not designed for and cannot easily protect against without them even knowing it, and this ability to rethrow foreign exceptions within different contexts seems to bare similar issues.Now let's consider how we could support such advanced uses of
throw;
withincatch (...)
. There are two-phase exception-handling systems that support resumable/restartable exceptions (i.e. no continuations/first-class-stacks required). One way is for the first-phase handler to throw an exception that the code that initiated the first phase can catch. So if WebAssembly eventually gets an expressive two-phase exception-handling system, then C++throw;
could be implemented by initiating a first-phase search for a containing (compiled) C++catch
that then throws the caught exception within the first-phase handler. If a C++catch (...)
were compiled usingcatch_all
, then that could be done by executingrethrow
in the first-phase handler. This means thatcatch_all
/rethrow
combined with expressive two-phase exception handling would enable the above situation (that at least I believe is concerning). On the other hand, the variant where theend
ofcatch_all
rethrows the exception, rather than arethrow
instruction, would not have this problem. (It's still possible to implement another ABI for rethrowing foreign exceptions, though that seems too detailed a discussion for now.)Interacting with foreign exception systems
I mentioned resumable/restartable exceptions, which are one of many examples of other exception systems foreign to C++. It is unclear how this particular example should interact with
catch_all
. Suppose some code throws a resumable/restartable exception within acatch_all
within a first-phase handler that could resume/restart the exception. What should happen? At the point in time where the search propagates to thecatch_all
, the search still does not know whether it is actually an exception or not. Consider the case where the code throwing the exception knows there is a handler and just needs to know the handler's response. In this case, it would be problematic forcatch_all
to interfere, so I would think we should let the search for handlers pass throughcatch_all
. But suppose this search finds nothing, in which case different exception systems do different things. Some trap, some unwind the stack, some prompt the user for the missing value, and some generate a dummy value in attempt to keep the system going. What should thecatch_all
do in each of these situations? Or suppose the search finds a handler, but that handler catches the exception rather than resuming/restarting it. What shouldcatch_all
do here?Closing thoughts
My sense is that, like C++, rather than putting WebAssembly into a position of having to bake in various policy decisions about an ever-expanding range of potential interactions between exception systems, we should defer those to the ABI. WebAssembly's exception-event system is generative (i.e. instances get distinct events unless they import/export) for the sake of preventing unintended interferences and enabling compositional reasoning. For example, a
throw;
in one C++ module instance won't rethrow the most recent exception caught by a different C++ module instance unless they explicitly coordinate exceptions, which seems like what one would want. Butcatch_all
seems to have conflicts with both of these desirables, especially if it has arethrow
instruction rather than rethrowing at itsend
.For now, an ABI for C++ that likely addresses the major use cases is one where
catch (...)
explicitly catches host/JS exceptions (i.e. by importing an exception event from the host with payloadexternref
) and C++ exceptions thrown within the same module instance (or ones that were instantiated with the same__cpp_exception
event). JS programs already have to deal with their exceptions being thrown in different dynamic contexts than where they originated, so that concern withthrow;
should be fine. The ABI could also/alternatively useunwind
to implementcatch (...)
, which would intercept all unwindings, which is a reasonable approximation of exceptions (especially for a single-phase exception ABI).In the future, if we add two-phase exception handling, then an ABI for C++ could compile
catch (...)
to use a filter for C++ (and host/JS?) exceptions that always indicates to handle the exception. This would miss other foreign exceptions, but that might be necessary given the diversity of foreign exception systems (e.g. resumable/restartable exceptions). For those, the ABI could still useunwind
to catch at least unwindings.In the long future, an inter-language ABI might develop that would facilitate interactions between various exception systems. It's hard to say now what this would look like, but my preference is to make WebAssembly flexible/expressive enough for the community to evolve these conventions on their own as they develop an understanding of what sorts of inter-program/language coordination does and does not need to be done.
Or we might find that the first ABI is enough. The point is that the recent change makes the EH proposal composable enough for programs to reasonably interop with other programs compiled with different/future ABIs, and so we can reasonably make this an ABI problem rather than a core-spec problem.
The text was updated successfully, but these errors were encountered: