This document describes the plans for a new exception handling scheme for WebAssembly, regarding what each tool components does and how they interact with each other to implement the scheme.
Here we describe a new exception handling scheme for WebAssembly for the toolchain side to generate compatible wasm binary files to support the aforementioned WebAssembly exception handling proposal.
We use the exception IR designed for Windows EH for the Wasm IR because of its well-defined EH pad scopes and hierarchies between them, we use the Itanium EH ABI for the communication between user code and libraries.
WebAssembly's try
and catch
instructions are structured as follows:
try
instruction*
catch (C++ tag)
instruction*
catch (Another language's tag)
instruction*
...
catch_all
instruction*
try_end
A catch
instruction in WebAssembly does not correspond to a C++ catch
clause. In WebAssembly, there is a single catch
instruction for all C++
exceptions. catch
instruction with tags for other languages and catch_all
instructions are not generated for the current version of implementation. All
catch clauses for various C++ types go into this big catch (C++ tag)
block.
Each language will have a tag number assigned to it, so you can think the C++
tag as a fixed integer here. So the structure for the prototype will look like
try
instruction*
catch (C++ tag)
instruction*
try_end
So for example, if we have this C++ code:
try {
foo(); // may throw
} catch (int n) {
printf("int caught");
} catch (float f) {
printf("float caught");
}
The resulting grouping of WebAssembly instructions will be, in pseudocode, like this:
try
foo();
catch (C++ tag)
if thrown exception is int
printf("int caught");
else if thrown exception is float
printf("float caught");
else
throw exception to the caller
try_end
When a
[throw
][eh_proposal#throwing-an-exception]
instruction throws an exception within a try
block or functions called from
it, the control flow is transferred to catch (C++ tag)
instruction, after
which the thrown exception object is placed on top of WebAssembly value stack.
This means, from user code's point of view, catch
instruction returns the
thrown exception object. For more information, refer to Try-catch
blocks section in the exception proposal.
Here we only discuss C++ libraries because currenly we only support C++ exceptions, but every language that supports exceptions should have some kind of libraries that play similar roles, which we can extend to support WebAssembly exceptions once we add support for that language.
Two C++ runtime libraries participate in exception handling: C++ ABI library and Unwind library.
The C++ ABI library provides an implementation of the library portion of the Itanium C++ ABI, covering both the support functionality in the main Itanium C++ ABI document and Level II of the exception handling support. References to the functions and objects in this library are implicitly generated by Clang when compiling C++ code. Broadly used implementations of this spec include LLVM's libc++abi and GNU GCC's libsupc++.
The unwind library provides a family of _Unwind_*
functions implementing the
language-neutral stack unwinding portion of the Itanium C++ ABI (Level
I). It is a dependency of the C++ ABI library, and
sometimes is a dependency of other runtimes. GNU GCC's
libgcc_s has an
integrated unwinder. libunwind has a separate library for the unwinder and there
are several implementations including LLVM's libunwind.
Our implementation is based on LLVM's libc++abi and libunwind. Our ports of the libraries, which contain Wasm EH specific changes, are in https://github.com/emscripten-core/emscripten/tree/main/system/lib/libcxxabi and https://github.com/emscripten-core/emscripten/tree/main/system/lib/libunwind.
Currently there are several exception handling schemes supported by major compilers such as GCC or LLVM. LLVM supports four kinds of exception handling schemes: Dwarf CFI, SjLj (setjmp/longjmp), ARM (EABI), and WinEH. Then why do we need the support for a new exception handling scheme in a compiler as well as libraries supporting C++ ABI?
The most distinguished aspect about WebAssembly EH that necessiates a new exception handling scheme is WebAssembly code is not allowed to inspect or modify the call stack, and cannot jump indirectly. As a result, when an exception is thrown, the stack is unwound by not the unwind library but the VM. This affects various components of WebAssembly EH that will be discussed in detail in this document.
The definition of an exception handling scheme can be different when defined from a compiler's point of view and when from libraries. LLVM currently supports four exception handling schemes: Dwarf CFI, SjLj, ARMEH, and WinEH, where all of them use Itanium C++ ABI with an exception of WinEH. ARMEH resembles Dwarf CFI in many ways other than a few architectural differences. Unwind libraries implement different unwinding mechanism for many architectures, each of which can be considered as a separate scheme. We will refer to the WebAssembly exception handling scheme as WebAssembly EH in short in this document.
In this document we mostly describe WebAssembly EH using comparisons with with two Itanium-based schemes: Dwarf CFI and SjLj. Even though the unwinding process itself is very different, code transformation required by compiler and the way C++ ABI library and unwind library communicate partly resemble that of SjLj exception handling scheme.
For other schemes, stack unwinding is performed by the unwind library: for example, DWARF CFI scheme uses call frame information stored in DWARF format to access callers' frames, whereas SjLj scheme traverses in-memory chain of structures recorded for every try clause to find a matching catch site. And at every call frame with try-catch clauses, these exception handling schemes call the personality function in the C++ ABI library to check if we need to stop at the call frame, in which case there is a matching catch site or cleanup actions to perform.
Unlike this process, WebAssembly unwinding process is performed by a VM, and
it stops at every call frame that has catch (C++ tag)
instruction. From
WebAssembly code's point of view, after a
throw
instruction within a try block
throws, the control flow is magically transferred to a corresponding catch (C++ tag)
instruction, which returns an exception object. So the unwinding process
is completely hidden from WebAssembly code, which means the personality function
cannot be called before control returns to the compiler-generated user code.
For example, in Dwarf CFI scheme, after the personality function figures out
which frame to stop, the function does three things (in SjLj scheme the
personality function doesn't do the landing pad setting because it uses
longjmp
):
- Sets IP to the start of a matching landing pad block (so that the unwinder will jump to this block after the personality routine returns).
- Gives the address of the thrown exception object.
- Gives the selector value corresponding to the type of exception thrown.
Can WebAssembly EH get all this information without calling a personality
function? Program control flow is transferred to catch (C++ tag)
instruction
by the unwinder in a VM; WebAssembly code cannot access or modify IP.
WebAssembly catch
instruction's result is the address of a thrown object.
But we cannot get a selector without calling a personality function. In
WebAssembly EH, the personality function is directly called from the
compiler-generated user code rather than from the unwind library. To do that,
WebAssembly compiler inserts a call to the personality function at the start of
each landing pad.
In exception handling schemes based on Itanium C++ ABI, C++ throw
keyword is
compiled into a call to __cxa_throw
function, which
calls
_Unwind_RaiseException
(in Dwarf CFI scheme) or _Unwind_SjLj_RaiseException
(in SjLj scheme) to start an unwinding process. These _Unwind_RaiseException
/
_Unwind_SjLj_RaiseException
functions performs the actual stack unwinding
process and calls the personality function for each eligible call frame. You can
see libcxxabi's personality function implementation
here.
As discussed above, in WebAssembly EH stack unwinding is not done by the unwind
library, the compiler inserts a call to a personality function, more precisely,
a wrapper to the personality function after WebAssembly catch
instruction,
passing the thrown exception object returned from the catch
instruction. The
wrapper function lives in the unwind library, and its signature will be
_Unwind_Reason_Code _Unwind_CallPersonality(void *exception_ptr);
Even though the wrapper signature looks simple, we use an auxiliary data structure to pass more information from the compiler-generated user code to the personality function and retrieve a selector computed after the function returns. The structure is used as a communication channel: we set input parameters within the structure before calling the wrapper function, and reads output parameters after the function returns. This structure lives in the unwind library and this is how it looks like:
struct _Unwind_LandingPadContext {
// Input information to personality function
uintptr_t lpad_index; // landing pad index
__personality_routine personality; // personality function
uintptr_t lsda; // LSDA address
// Output information computed by personality function
uintptr_t selector; // selector value, used to select a C++ catch clause
};
// Communication channel between WebAssembly code and personality function
struct _Unwind_LandingPadContext __wasm_lpad_context = ...;
// Personality function wrapper
_Unwind_Reason_Code _Unwind_CallPersonality(void *exception_ptr) {
struct _Unwind_Exception *exception_obj =
(struct _Unwind_Exception *)exception_ptr;
// Call personality function
_Unwind_Reason_Code ret = (*__wasm_lpad_context->personality)(
1, _UA_CLEANUP_PHASE, exception_obj->exception_class, exception_obj,
(struct _Unwind_Context *)__wasm_lpad_context);
return ret;
}
As you can see in the code above, here's the list of input and output parameters
communicated throw __wasm_lpad_context
:
- Input parameters
- Landing pad index
- Personality function address
- LSDA information (exception handling table) address
- Output parameters
- Selector value
These three input parameters are not directly passed to the personality function
as arguments, but are read from it using various _Unwind_Get*
functions in
unwind library API. The output parameter, a selector value is also not directly
returned by the personality function but will be set by the personality function
using _Unwind_SetGR
. (SetGR
here means setting a general register, and it
does set a physical register in Dwarf CFI. But for SjLj and WebAssembly schemes
it sets some field in a data structure instead.)
When looking for a matching catch site in each call frame, what the personality
function does is querying the call site table within the current function's LSDA
(Language Specipic Data Area), also known as exception handling table or
gcc_except_table
, with a call site information. For example, the table answers
queries such as "If an exception is thrown at this call site, which action table
entry should I check?" In Dwarf CFI scheme, the offset of actual callsite
address relative to function start address is used as callsite information. In
SjLj scheme, each callsite that can throw has an index starting from 0, and the
indices serve as callsite information to query the action table. In WebAssembly
EH, because a call to the personality function wrapper is inserted at the start
of each landing pad, we give each landing pad an index starting from 0 and use
this as callsite information. We also need to pass the address of the
personality function so that the wrapper can call it. The address of LSDA for
the current function is also required because the personality function examines
the tables there to look for a matching catch site.
Putting it all together, below is an example of code snippet the compiler inserts at the beginning of each landing pad. (This is C-style pseudocode; the real code inserted will be in IR or assembly level.)
// Gets a thrown exception object
void *exn = catch(0); // WebAssembly catch instruction
// Set input parameters
__wasm_lpad_context.lpad_index = index;
__wasm_lpad_context.personality = __gxx_personality_v0;
__wasm_lpad_context.lsda = &GCC_except_table0; // LSDA symbol of this function
// Call personality wrapper function
_Unwind_CallPersonality(exn);
// Retrieve output parameter
int selector = __wasm_lpad_context.selector;
// use exn and selector hereafter
Itanium-style two-phase unwinding typically consists of two phases: search and cleanup. In the search phase, call frames are searched to find a matching catch site that can catch the type of exception thrown or one that needs some cleanup action as the stack is unwound. If one is found, it enters the cleanup phase in which the unwinder stops at the stack frame with the matching catch site found and starts to run the code there. (The whole search in the second phase is usually avoided by reusing cached information from the first search phase.) If no matching clause is found in the first phase, the program aborts.
WebAssembly unwinder does not perform two-phase unwinding. Therefore, effectively, it only runs the second, cleanup phase. As discussed, because the unwinding is done by a VM, the unwind library and the C++ ABI library cannot drive its two-phase unwinding. Because we do not have any cached information from the first search stage, we do full searches as in the first search stage of two-phase unwinding.
LSDA (Language Specific Data Area) contains various tables used by the
personality function to check if there is any matching catch sites or cleanup
code to run. Every function that has landing pads has its own LSDA information
area. Usually symbols with prefix GCC_except_table
or gcc_except_table
are
used to denote the start of a LSDA information. For some exception handling
schemes LSDA information is stored in its own section, but WebAssembly uses the
data section.
There are three tables in WebAssembly LSDA information:
- Call site table
- Maps call sites (landing pad indices) to action table entries.
- Action table
- Each entry contains the current action (type information and whether to catch it or filter it) and the next action entry to proceed. Refers to type information table on which type to catch or filter.
- Type information table
- List of type information
In WebAssembly EH, the formats of the action table and the type information table are the same with that of Dwarf CFI and SjLj scheme. The primary difference of our scheme is we use landing pad indices as call sites.
Exception Handling Table Layout:
+-----------------+--------+----------------------+
| lpStartEncoding | (char) | always DW_EH_PE_omit |
+---------+-------+--------+---------------+----------+
| lpStart | (encoded with lpStartEncoding) | Not used |
+---------+-----+--------+-----------------+---------------+
| ttypeEncoding | (char) | Encoding of the type_info table |
+---------------+-+------+----+----------------------------+----------------+
| classInfoOffset | (ULEB128) | Offset to type_info table, defaults to null |
+-----------------++--------+-+----------------------------+----------------+
| callSiteEncoding | (char) | Encoding for Call Site Table |
+------------------+--+-----+-----+------------------------+--------------------------+
| callSiteTableLength | (ULEB128) | Call Site Table length, used to find Action table |
+---------------------+-----------+---------------------------------------------------+
+---------------------+-----------+------------------------------------------------+
| Beginning of Call Site Table landing pad index is a index into this |
| table. |
| +-------------+---------------------------------+------------------------------+ |
| | landingPad | (ULEB128) | landingpad index | |
| | actionEntry | (ULEB128) | Action Table Index 1-based | |
| | | | actionEntry == 0 -> cleanup | |
| +-------------+---------------------------------+------------------------------+ |
| ... |
+----------------------------------------------------------------------------------+
+---------------------------------------------------------------------+
| Beginning of Action Table ttypeIndex == 0 : cleanup |
| ... ttypeIndex > 0 : catch |
| ttypeIndex < 0 : exception spec |
| +--------------+-----------+--------------------------------------+ |
| | ttypeIndex | (SLEB128) | Index into type_info Table (1-based) | |
| | actionOffset | (SLEB128) | Offset into next Action Table entry | |
| +--------------+-----------+--------------------------------------+ |
| ... |
+---------------------------------------------------------------------+-----------------+
| type_info Table, but classInfoOffset does *not* point here! |
| +----------------+------------------------------------------------+-----------------+ |
| | Nth type_info* | Encoded with ttypeEncoding, 0 means catch(...) | ttypeIndex == N | |
| +----------------+------------------------------------------------+-----------------+ |
| ... |
| +----------------+------------------------------------------------+-----------------+ |
| | 1st type_info* | Encoded with ttypeEncoding, 0 means catch(...) | ttypeIndex == 1 | |
| +----------------+------------------------------------------------+-----------------+ |
| +---------------------------------------+-----------+------------------------------+ |
| | 1st ttypeIndex for 1st exception spec | (ULEB128) | classInfoOffset points here! | |
| | ... | (ULEB128) | | |
| | Mth ttypeIndex for 1st exception spec | (ULEB128) | | |
| | 0 | (ULEB128) | | |
| +---------------------------------------+------------------------------------------+ |
| ... |
| +---------------------------------------+------------------------------------------+ |
| | 0 | (ULEB128) | throw() | |
| +---------------------------------------+------------------------------------------+ |
| ... |
| +---------------------------------------+------------------------------------------+ |
| | 1st ttypeIndex for Nth exception spec | (ULEB128) | | |
| | ... | (ULEB128) | | |
| | Mth ttypeIndex for Nth exception spec | (ULEB128) | | |
| | 0 | (ULEB128) | | |
| +---------------------------------------+------------------------------------------+ |
+---------------------------------------------------------------------------------------+
You can see the exception table structure for DwarfCFI and SjLj scheme here. Other than call site table, the structure for WebAssembly EH is mostly the same.
We discussed in a prior section about some additions required to the C++ ABI library and the unwind library to implement WebAssembly EH. Here we list up required additional data structure/functions and WebAssembly's implementation of required APIs.
This section describes compiler builtins that require support from compiler implementations.
void __builtin_wasm_throw(unsigned int, void *);
A call to this builtin function is converted to a WebAssembly
throw
instruction in the instruction
selection stage in the backend. This builtin function is used to implement
exception-throwing functions in the base ABI.
void __builtin_wasm_rethrow();
A call to this builtin function is converted to a WebAssembly
rethrow
instruction in the instruction
selection stage in the backend. This builtin function is used to implement
rethrowing exceptions in the base API.
This section defines the unwind library interface, expected to be provided by any Itanium ABI-compliant system. This is the interface on which the C++ ABI exception-handling facilities are built. This section describes what WebAssembly version of the ABI functions do and additional data structures or functions we need to add. For the complete Itanium C++ base ABI, refer to the spec here.
This serves as a communication channel between WebAssembly code and the
personality function. A global variable __wasm_lpad_context
is an instance of
this data structure.
struct _Unwind_LandingPadContext {
// Input information to personality function
uintptr_t lpad_index; // landing pad index
__personality_routine personality; // personality function
uintptr_t lsda; // LSDA address
// Output information computed by personality function
uintptr_t selector; // selector value
};
// Communication channel between WebAssembly code and personality function
struct _Unwind_LandingPadContext __wasm_lpad_context = ...;
_Unwind_Reason_Code
_Unwind_RaiseException(struct _Unwind_Exception *exception_object);
Raise an exception using the __builtin_wasm_throw
builtin, which will be converted to WebAssembly
throw
instruction. The arguments to the
builtin are the tag number for C++ and a pointer to an exception object.
Not used.
void _Unwind_Resume(struct _Unwind_Exception *exception_object);
Resume propagation of an existing exception. Unlike other _Unwind_*
functions
that are called from the C++ ABI library, this is called from compiler-generated
user code. In other exception handling schemes, this function is mostly used
when a call frame does not have a matching catch site but has cleanup code to
run so that the unwinder stops there only to run the cleanup and resume the
exception's propagation. But in WebAssembly EH, because the unwinder stops at
every call frame with landing pads, this runs on every call frame with landing
pads that does not have a matching catch site. This function also makes use of
__builtin_wasm_throw
builtin to resume the
propagation of an exception.
uint64 _Unwind_GetGR(struct _Unwind_Context *context, int index);
void
_Unwind_SetGR(struct _Unwind_Context *context, int index, uint64 new_value);
The meaning of the original API name is it gets/sets the value of the given
general register. But in WebAssembly EH, _Unwind_SetGR
is only used to set a
selector
value
to a data structure used as a communication channel
(__wasm_lpad_context.selector
).
In WebAssembly EH, _Unwind_SetGR
expects the first argument to be a pointer to
struct _Unwind_LandingPadContext
instance, and only 1 is accepted as the
second argument, in which case it sets the first argument's selector
field.
_Unwind_GetGR
is not used.
uint64 _Unwind_GetIP(struct _Unwind_Context *context);
void _Unwind_SetIP(struct _Unwind_Context *context, uint64 new_value);
This sets/gets a real IP address in Dwarf CFI, but in our scheme _Unwind_GetIP
returns the value of (landing pad index + 1). The landing pad index is set by
compiler-generated user code to __wasm_lpad_context.lpad_index
as discussed in
Landing Pad Code. This information is used in the
personality function to query the call site table. _Unwind_SetIP
is not used.
uint64 _Unwind_GetLanguageSpecificData(struct _Unwind_Context *context);
Returns the address of the current function's LSDA information
(__wasm_lpad_context.lsda
), set by compiler-generated user code as discussed
in Landing Pad Code.
Not used.
A wrapper function used to call the actual personality function. This is supposed to be called from compiler-generated user code. Refer to Landing Pad Code for details.
_Unwind_Reason_Code _Unwind_CallPersonality(void *exception_ptr) {
struct _Unwind_Exception *exception_obj =
(struct _Unwind_Exception *)exception_ptr;
// Call personality function
_Unwind_Reason_Code ret = (*__wasm_lpad_context->personality)(
1, _UA_CLEANUP_PHASE, exception_obj->exception_class, exception_obj,
(struct _Unwind_Context *)__wasm_lpad_context);
return ret;
}
Transferring program control to a landing pad is done by not the unwind library but the VM. Refer to Stack Unwinding and Personality Function section for details.
The second level of specification is the minimum required to allow interoperability of C++ implementations. This part contains the definition of an exception object, and various high-level APIs including functions required to allocate / throw / catch / rethrow an exception object. Functions in this section rely on the base API to do low-level architecture-dependent tasks. WebAssembly EH does not have a lot of things to add on this level because architecture-dependent components are usually taken care of in the base API level. But we still need some modifications on the personality function and functions called from it to handle some subtle differences between WebAssembly EH and other schemes, such as Dwarf CFI or SjLj. For the complete Itanium C++ ABI, refer to the spec here.