Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
422 lines (301 sloc) 16.4 KB

Level 1 exception handling

Level 1 of exception handling is the MVP (minimal viable proposal) for implementing exceptions in WebAssembly. As such, it doesn't include higher-level concepts and structures. These concept and structures are introduced in later levels, and either:

  1. Improves readability by combining concepts in Level 1 into higher-level constructs, thereby reducing code size.
  2. Allow performance improvements in the VM.
  3. Introduce additional new functionality not available in Level 1.

This document supersedes the original Exceptions Proposal.

Overview

Exception handling allows code to break control flow when an exception is thrown. The exception can be any exception known by the WebAssembly module, or it may an unknown exception that was thrown by a called imported function.

One of the problems with exception handling is that both WebAssembly and an embedder have different notions of what exceptions are, but both must be aware of the other.

It is difficult to define exceptions in WebAssembly because (in general) it doesn't have knowledge of any embedder. Further, adding such knowledge to WebAssembly would limit the ability for other embedders to support WebAssembly exceptions.

One issue is that both sides need to know if an exception was thrown by the other, because cleanup may need to be performed.

Another problem is that WebAssembly doesn't have direct access to an embedder's memory. As a result, WebAssembly defers the handling of exceptions to the host VM.

To access exceptions, WebAssembly provides instructions to check if the exception is one that WebAssembly understands. If so, the data of the WebAssembly exception is extracted and copied onto the stack, allowing succeeding instructions to process the data.

Lastly, exception lifetimes may be maintained by the embedder, so that it can collect and reuse the memory used by exceptions. This implies that an embedder needs to know where exceptions are stored, so that it can determine when an exception can be garbage collected.

This also implies that that embedders must provide a garbage collector for exceptions. For embedders that have garbage collection (such as JavaScript), this is not a problem.

However, not all embedders may have a garbage collector. For this reason, WebAssembly exceptions are designed to allow other storage management methods, such as reference counting, to perform the garbage collection in the embedder.

To do this, WebAssembly exceptions are immutable once created, to avoid cyclic data structures that cannot be garbage collected. It also means that exceptions can't be stored into linear memory. The rationale for this is twofold:

  • For security. Loads and stores do not guarantee that the data read was of the same type as stored. This allows spoofing of exception references that may allow a WebAssembly module to access data it should not know in the host VM.

  • The embedder does not know the layout of data in linear memory, so it can't find places where exception references are stored.

Hence, while an exception reference is a new first class type, this proposal disallows their usage in linear memory.

A WebAssembly exception is created when you throw it with the throw instruction. Thrown exceptions are handled as follows:

  1. They can be caught by a catch block in an enclosing try block of a function body. The caught exception is pushed onto the stack.

  2. Throws not caught within a function body continue up the call stack, popping call frames, until an enclosing try block is found.

  3. If the call stack is exhausted without any enclosing try blocks, the embedder defines how to handle the uncaught exception.

Event handling

This proposal adds exception handling to WebAssembly. Part of this proposal is to define a new section to declare exceptions. However, rather than limiting this new section to just defining exceptions, it defines a more general format that allows the declaration of other forms of events.

In general, an event handler allows one to process an event generated by a block of code. Events suspend the current execution and look for a corresponding event handler. If found, the corresponding event handler is run. Some event handlers may send values back to the suspended instruction, allowing the originating code to resume.

Exceptions are a special case of an event in that they never resume. Similarly, a throw instruction is the suspending event of an exception. The catch block associated with a try block defines how to handle the throw.

WebAssembly events (i.e. exceptions) are defined by a new event section of a WebAssembly module. The event section is a list of declared events associated with the module.

Each event has an attribute and a type. Currently, the attribute can only specify that the event is an exception. In the future, additional attribute values may be added when other events are added to WebAssembly.

The type of an event is denoted by an index to a function signature defined in the type section. The parameters of the function signature defines the list of values associated with the exception event. The result type must be 'void'.

An event tag is a value to distinguish different events, while an event index is a numeric name to refer to an (imported or defined) event tag within a module (see event index space for details).

Exceptions

An exception is an internal construct in WebAssembly . WebAssembly exceptions are defined in the event and import sections of a module. Each event (with an exception attribute) defines an exception. The event index is also called the exception index. Similarly, the corresponding event tag is called an exception tag.

Exception indices are used by:

  1. The throw instruction which creates a WebAssembly exception with the corresponding exception tag, and then throws it.

  2. The if_except instruction queries an exception to see if the corresponding exception tag denoted by the exception index. If true it pushes the corresponding values of the exception onto the stack.

The exception reference data type

Data types are extended to have a new except_ref type, that refers to an exception. The representation of an exception is left to the implementation.

Try and catch blocks

A try block defines a list of instructions that may need to process exceptions and/or clean up state when an exception is thrown. Like other higher-level constructs, a try block begins with a try instruction, and ends with an end instruction. That is, a try block is sequence of instructions having the following form:

try block_type
  instruction*
catch
  instruction*
end

A try block ends with a catch block that is defined by the list of instructions after the catch instruction.

Try blocks, like control-flow blocks, have a block type. The block type of a try block defines the values yielded by the evaluation the try block when either no exception is thrown, or the exception is successfully caught by the catch block.

In the initial implementation, try blocks may only yield 0 or 1 values.

Throwing an exception

The throw instruction takes an exception index as an immediate argument. That index is used to identify the exception tag to use to create and throw the corresponding exception.

The values on top of the stack must correspond to the type associated with the exception. These values are popped of the stack and are used (along with the corresponding exception tag) to create the corresponding exception. That exception is then thrown.

When an exception is thrown, the embedder searches for the nearest enclosing try block body that execution is in. That try block is called the catching try block.

If the throw appears within the body of a try block, it is the catching try block.

If a throw occurs within a function body, and it doesn't appear inside the body of a try block, the throw continues up the call stack until it is in the body of an an enclosing try block, or the call stack is flushed. If the call stack is flushed, the embedder defines how to handle uncaught exceptions. Otherwise, the found enclosing try block is the catching try block.

A throw inside the body of a catch block is never caught by the corresponding try block of the catch block, since instructions in the body of the catch block are not in the body of the try block.

Once a catching try block is found for the thrown exception, the operand stack is popped back to the size the operand stack had when the try block was entered, and then an except_ref referring to the caught exception is pushed back onto the operand stack.

If control is transferred to the body of a catch block, and the last instruction in the body is executed, control then exits the try block.

If the selected catch block does not throw an exception, it must yield the value(s) expected by the corresponding catching try block. This includes popping the caught exception.

Note that a caught exception can be rethrown using the rethrow instruction.

Rethrowing an exception

The rethrow instruction takes the exception associated with the except_ref on top of the stack, and rethrows the exception. A rethrow has the same effect as a throw, other than an exception is not created. Rather, the referenced exception on top of the stack is popped and then thrown.

Exception data extraction

The if_except block begins with an if_except instruction, and has two instruction blocks,

That is, the forms of an if_except block is:

if_except block_type except_index
  Instruction*
end

if_except block_type except_index
  Instruction*
else
  Instruction*
end

In the first form, the instructions between the if_except and 'end' define the then block. In the second form, the instructions between the if_except and else define the then block, while the instructions between the else and the end define the else block.

The conditional query of an exception checks the exception tag of exception on top of the stack. It succeeds only if the exception index of the instruction matches the corresponding exception tag. Once the query completes, the exception is popped off the stack.

If the query succeeds the values (associated with the popped exception) are extracted and pushed onto the stack, and control transfers to the instructions in the then block.

If the query fails, it either enters the else block, or transfer control to the end of the if_except block if there is no else block.

Stack traces

When an exception is thrown, the runtime will pop the stack across function calls until a corresponding, enclosing try block is found. It may also associate a stack trace that can be used to report uncaught exceptions. However, the details of this is left to the embedder.

Changes to the text format.

This section describes change in the instruction syntax document.

New instructions

The following rules are added to instructions:

  try resulttype instruction* catch instruction* end |
  except except_index |
  throw except_index |
  rethrow |
  if_except resulttype except_index then instruction* end |
  if_except resulttype except_index then instruction* else instruction* end

Like the block, loop, and if instructions, the try and if_except instructions are structured control flow instructions, and can be labeled. This allows branch instructions to exit try and if_except blocks.

The except_index of the throw and if_except instructions defines the exception (and hence, exception tag) to create/extract from. See exception index space for further clarification of exception tags.

Changes to Modules document.

This section describes change in the Modules document.

Event index space

The event index space indexes all imported and internally-defined events, assigning monotonically-increasing indices based on the order defined in the import and event sections. Thus, the index space starts at zero with imported events, followed by internally-defined events in the event section.

The event index space defines the (module) static version of runtine event tags. For event indices that are not imported/exported, the corresponding event tag is guaranteed to be unique over all loaded modules. Events that are imported or exported alias the respective events defined elsewhere, and use the same tag.

Changes to the binary model

This section describes changes in the binary encoding design document.

Data Types

except_ref

An exception reference points to an exception.

Language Types

Opcode Type constructor
-0x18 except_ref

value_type

A varint7 indicating a value_type is extended to include except_ref as encoded above.

Other Types

event_type

The set of event attributes are:

Name Value
Exception 0

Each event type has the fields:

Field Type Description
attribute varuint32 The attribute of the event.
type varuint32 The type index for its corresponding type signature
external_kind

A single-byte unsigned integer indicating the kind of definition being imported or defined:

Module structure

High-level structure

A new event section is introduced and is named event. If included, it must appear after immediately after the global section.

Event section

The section section is the named section 'event'. The event section declares a list of event types as follows:

Field Type Description
count varuint32 count of the number of events to follow
type except_type* The definitions of the event types
Import section

The import section is extended to include event definitions by extending an import_entry as follows:

If the kind is Event:

Field Type Description
type event_type the event being imported
Export section

The export section is extended to reference event types by extending an export_entry as follows:

If the kind is Event:

Field Type Description
index varuint32 the index into the corresponding event index space
Name section

The set of known values for name_type of a name section is extended as follows:

Name Type Code Description
Function 1 Assigns names to functions
Local 2 Assigns names to locals in functions
Event 3 Assigns names to event types
Event names

The event names subsection is a name_map which assigns names to a subset of the event indices (Used for both imports and module-defined).

Control flow operators

The control flow operators are extended to define try blocks, catch blocks, throws, and rethrows as follows:

Name Opcode Immediates Description
try 0x06 sig : block_type begins a block which can handle thrown exceptions
catch 0x07 begins the catch block of the try block
throw 0x08 index : varint32 Creates an exception defined by the exception indexand then throws it
rethrow 0x09 Pops the except_ref on top of the stack and throws it
if_except 0x0a index : varuint32, sig : block_type Begin exception data extraction if exception on stack was created using the corresponding exception index

The sig fields of block, if, try and if_except operators are block signatures which describe their use of the operand stack.