Exceptions vs. non-local control constructs vs. unwinding #142
Description
Exceptions, non-local control constructs, and unwinding are all related but different entities. It is important to understand the distinction between these concepts, and to see that distinction consider the following C++ program:
#include <csetjmp>
int main() {
jmp_buf env;
int val = setjmp(env); // val is 0 on first call
if (val==0) {
try {
longjmp(env, 5); // makes setjmp return 5 "instead"
} catch (...) {
return 1; // never executed
}
} else {
return 2; // executed
}
}
According to the C++ spec, this program returns 2, not 1. This is because although longjmp
is a non-local control construct, it is not an exception. (And to clarify, the behavior of this program does not depend on how catch (...)
is specified to interact with foreign exceptions because longjmp
is not considered a foreign exception either.) So exceptions are distinct (but closely related to) from non-local control constructs.
I was careful to make sure this example involves no unwinding. How longjmp
interacts with unwinding is not defined by the C++ spec, intentionally deferring it to the platform. (Similarly, the C++ spec does not specify how unwinding interacts with uncaught exceptions, intentionally deferring it to the platform because the behavior of single-phase vs. two-phase EH implementations differ here.) The GNU compilers do not have longjmp
cause unwinding, whereas Visual Studio does by default (though you can turn it off). (Visual Studio also lets you configure whether foreign/system exceptions should cause unwinding and discusses why you would want unwinding for some circumstances and why you would not want unwinding for other circumstances.) So non-local control constructs are distinct (but closely related to) from unwinding. (To clarify, I am not advocating to add non-unwinding non-local control constructs in this proposal.)
Okay, so why do these distinctions matter? Well, just as many languages compiling to C use setjmp
/longjmp
to implement their own non-local control constructs (as it is the only non-local option), many languages compiling to WebAssembly will use throw
/catch
to implement their own non-local control constructs (again, as it is the only non-local option). We should anticipate this. Similarly, WebAssembly eventually add other non-local control constructs. We should leave room for this. unwind
does both by providing a way to specifying unwinding code with no assumptions about why the stack is being unwound. It is closely related to unwinding clauses in other systems—fault
, (part of) finally
, unwind-protect
, and (part of) dynamic-wind
—all of which similarly specify/treat an unwinder as a block/function of type [] -> []
.
Now, one particular non-local control construct that will need to be emulated with throw
/catch
is setjmp
/longjmp
. Because the spec gives us the option to have longjmp
cause unwinding, this is mostly straightforward to do using some $longjmp
exception event. But there's a problem if one translates catch (...)
to catch_all
: the catch_all
will mistake the $longjmp
event for an exception. That would make our example C++ program above incorrectly return 1. And while yes, you could hack the compilation of catch (...)
to exclude the $longjmp
event, that only excludes your own long jumps, failing to exclude other C/C++-as-wasm program's long jumps as well as other languages' non-local control constructs, which catch (...)
seems to specifically not be intended to catch.
Hopefully this illustrates part of the rationale behind unwind
, and hopefully this more concrete example better illustrates the concern about compositionality of catch_all
that I had expressed more abstractly in #128.