Closed Bug 1759217 Opened 2 years ago Closed 2 years ago

Let exception handling ride the trains

Categories

(Core :: JavaScript: WebAssembly, task, P2)

task

Tracking

()

RESOLVED FIXED
100 Branch
Tracking Status
firefox100 --- fixed

People

(Reporter: rhunt, Assigned: rhunt)

References

(Blocks 1 open bug)

Details

(Keywords: dev-doc-complete)

Attachments

(1 file)

The feature is complete and enabled in nightly, we have implemented high value improvements, and the spec is stable. We should consider letting it ride the trains.

Keywords: dev-doc-needed
Pushed by rhunt@eqrion.net:
https://hg.mozilla.org/integration/autoland/rev/01bd43398a57
wasm: Let exception-handling ride the trains. r=lth
Status: NEW → RESOLVED
Closed: 2 years ago
Resolution: --- → FIXED
Target Milestone: --- → 100 Branch
Regressions: 1761257

FF100 Docs work for this should be trackable in https://github.com/mdn/content/issues/14392 (not yet assigned)

Question, https://webassembly.github.io/exception-handling/js-api/#runtime-exceptions dropped the attribute for sharing a stack trace which was present in the explainer. Does this mean that the stack trace doesn't propagate? Doesn't that make debugging hard?

Flags: needinfo?(rhunt)

Are you referring to the optional ExceptionOptions options in the Exception object constructor which has a traceStack property? The explainer is more up to date than the spec text on that one. Here's the issue for fixing that in the spec [1].

As a summary for how it works:

  1. Exceptions thrown in WebAssembly code will not have a stackTrace property attached to them. This was done to make throwing an exception fast.
  2. Similar to above, WebAssembly.Exception (the class of exceptions thrown by wasm) is not a subclass of the JS Error object, as that would imply collection of stack traces and other things. So they're unrelated.
  3. If wasm wants to throw an exception that has a stack trace, it needs to call a JS function which will use the WebAssembly.Exception JS-API constructor with traceStack = true, and then throw that exception. This feature is expected to be used with tools, so doing this isn't expected to be a large burden.

[1] https://github.com/WebAssembly/exception-handling/issues/201

Flags: needinfo?(rhunt)

Thanks @ryan!

Just to be clear for item "3", the WebAssembly.Exception is created in Javascript using a JavaScript function but I think you are saying that the exception will then be made available to the wasm module, which will then attach the stack track and throw it. I.e. it is thrown from WASM not javascript. Is that right?

I only ask because in item 1 you say "Exceptions thrown in WebAssembly code will not have a stackTrace property attached to them" - so it might be that the javascript function would also be passed the stack trace and then be expected to throw the exception.

I don't think that is the case, because the stack property is readonly, so you couldn't attach it in Javascript.

Is there any test code demonstrating this?

  1. My inference is that

    • before exception handling WebAssembly code would throw specific errors: WebAssembly.CompileError, WebAssembly.LinkError, WebAssembly.RuntimeError.
      • Note, examples are all a bit crap at those links because they appear to be thrown from javascript, and my assumption is that normally they would come from WASM code.
    • With exception handling we can now have exceptions in WASM. These are "generic" and are thrown to JS as WebAssembly:Exception.
    • These can be trapped in WASM code and rethrown.
    • These can be thrown in JS code called from WASM and caught in WASM.
    • How do these "fit in" with the "old" exceptions like WebAssembly.RuntimeError ?
  2. My understanding is that

    • a tag defines the type or structure of exceptions in WASM. It just consists of a sequence of data types and an attribute. Its telling you that at some point an exception might be thrown that has data fields of these data-types.
    • a specific exception thrown from WASM (or JS) has to match a tag. It will have runtime arguments with the same types/order as the associated tag.
    • The attribute can currently just indicate "an exception thrown from javascript (or not)", but might indicate something else in future.
      • I'm a bit confused by this because I would have thought the "where is this thrown from" would belong to the runtime exception not the tag.
    • The tag is represented in JavaScript by WebAssembly:Tag, so when you create a tag you're creating a type of exception. You have to pass this to the module so it can be actually used by WASM.
    • The runtime exceptions is represented in Javascript using WebAssembly.Exception. This tells WASM I have this type of exception (see the tag) and the arguments in this case are these fields.
    • I guess my problem is that "type of exception" is a bit abstract. It seems that the wasm and the javascript that calls it will have to have an intimate knowledge of each other to understand how to use the types of exceptions that they throw.
    • Does any of the above seem like I have a broad understand how this works?
  3. Is this a good place to ask these kinds of questions (and if not, is there a better one?)

Flags: needinfo?(rhunt)

With exception handling we can now have exceptions in WASM. These are "generic" and are thrown to JS as WebAssembly:Exception.
These can be trapped in WASM code and rethrown.
These can be thrown in JS code called from WASM and caught in WASM.

Yes. The core of wasm exception-handling is four components:

  1. (tag) definition or import - create or import a WA.Tag into the module
  2. throw $tag - construct a WA.Exception with the provided WA.Tag. This instruction takes as inputs the values that the tag requires.
  3. catch $tag - This catch will handle any thrown WA.Exception with tag matching $tag. The WA.Exception can be from a wasm 'throw $tag' instruction, or a from JS constructing and throwing a WA.Exception manually with the correct tag. This catch will unwrap the values contained in the WA.Exception as based off of the tag.
  4. catch_all - This catch will handle any thrown exception. It can be a WA.Exception, a JS Error, or any value that JS may throw. This catch does not provide any value for the caught exception, as it might be something that wasm doesn't understand.

So (1, 2, 3) all operate on WA.Exception and WA.Tag. (4) may handle general JS values that are thrown.

You can think of it as wasm really only understanding native WA.Exception's and WA.Tag's in it's throw-catch instructions, but being able to interop with general JS values being thrown via catch_all. And JS (via the WebAssembly JS-API) can use WA.Exception/Tag to interop if it wants.

before exception handling WebAssembly code would throw specific errors: WebAssembly.CompileError, WebAssembly.LinkError, WebAssembly.RuntimeError.

Note, examples are all a bit crap at those links because they appear to be thrown from javascript, and my assumption is that normally they would come from WASM code.

How do these "fit in" with the "old" exceptions like WebAssembly.RuntimeError ?

WebAssembly.CompileError/LinkError are normal JS errors (instanceof Error) thrown when constructing wasm module or instance. There is nothing special with them. As they are not a WA.Exception, they cannot be caught in a wasm tagged catch, but may be caught in a wasm catch_all.

WebAssembly.RuntimeError is the error thrown when 'wasm traps'. A wasm 'trap' is generated when a wasm instructions goes 'wrong' such as with divide by zero or memory out of bounds. Wasm traps are specified to not be catchable by wasm tagged catch or catch_all. They may be caught by JS try-catches.

So when a WA.RuntimeError is thrown due to a wasm trap, a special flag is attached to the WA.RuntimeError to indicate it's from a wasm trap. This disqualifies it from being caught in a wasm try-catch_all. If instead the WA.RuntimeError is created and thrown from JS code, it's catchable in a wasm try-catch_all.

This is the weirdest part of wasm exception-handling.

a tag defines the type or structure of exceptions in WASM. It just consists of a sequence of data types and an attribute. Its telling you that at some point an exception might be thrown that has data fields of these data-types.

Yes, a WA.Tag is the 'type' of a WA.Exception. The most important thing is the sequence of data types the WA.Exception will contain. A WA.Tag also has an identity. Two WA.Tag objects constructed with the same sequence of data types are not interchangeable.

This allows for a wasm module to define it's own private (not exported) WA.Tag for its use. This prevents any code outside of that module from being able to inspect the contents of a WA.Exception thrown from that module as that always requires providing the tag it was constructed with.

The attribute can currently just indicate "an exception thrown from javascript (or not)", but might indicate something else in future.

I'm a bit confused by this because I would have thought the "where is this thrown from" would belong to the runtime exception not the tag.

What attribute is this? WA.Tags only have 'identity' and 'sequence of data types'.

The only place where 'exception thrown for JS or not' matters is with the weirdness around WA.RuntimeError being from a wasm trap or from JS code.

The tag is represented in JavaScript by WebAssembly:Tag, so when you create a tag you're creating a type of exception. You have to pass this to the module so it can be actually used by WASM.

A tag can be created in JS via:

let tag = new WebAssembly.Tag(...)

Or by being defined in a wasm module via:

(module (tag $tagName))

A tag defined in JS may be given to a wasm module by being imported ((module (import (tag i32)))).
A tag defined in wasm may be given to JS by being exported ((module (export $tagName))).

As tags have identity, sometimes a tag needs to be defined in JS and imported into a module so that they can share access to created WA.Exceptions.

I guess my problem is that "type of exception" is a bit abstract. It seems that the wasm and the javascript that calls it will have to have an intimate knowledge of each other to understand how to use the types of exceptions that they throw.

Yes, that's roughly right.

If JS wants to inspect WA.Exceptions thrown from a wasm module, it needs the tags used by the module.
If wasm wants to inspect exceptions thrown from JS code, the JS code needs to use WA.Exceptions with a tag that the module has access to.

Wasm/JS can always catch and rethrow any of each other's exceptions even if they are not WA.Exceptions or don't have the WA.Tag. But to access the contents of the exception, they need to both be using WA.Exception with shared tags.

Is this a good place to ask these kinds of questions (and if not, is there a better one?)

Yeah, I'm happy to help. You may have some luck on the proposal repo (https://github.com/webassembly/exception-handling). But since this proposal has shipped everywhere, it has gotten very quiet there.

Just to be clear for item "3", the WebAssembly.Exception is created in Javascript using a JavaScript function but I think you are saying that the exception will then be made available to the wasm module, which will then attach the stack track and throw it. I.e. it is thrown from WASM not javascript. Is that right?

I only ask because in item 1 you say "Exceptions thrown in WebAssembly code will not have a stackTrace property attached to them" - so it might be that the javascript function would also be passed the stack trace and then be expected to throw the exception.

I don't think that is the case, because the stack property is readonly, so you couldn't attach it in Javascript.

So to get back and try to clarify this. A WA.Exception can be thrown in two ways:

  1. In wasm via throw instruction ((func throw $tag))
  2. In JS via throw new WebAssembly.Exception(tag, params, options)

Any WA.Exception created/thrown via (1) will not have a .stack property attached to the object.
A WA.Exception created/thrown via (2) will have a .stack property if options contains traceStack = true.

This was a compromise solution to allow toolchains targeting wasm to opt-in to stack trace collection (which can be expensive) by using the JS-API to throw their WA.Exceptions instead of using the wasm throw instruction.

Again, as WA.Tags have identities, the wasm module and JS code would need to share a tag in order for this to work. But it's expected that this isn't a burden for toolchain generated code.

Flags: needinfo?(rhunt)

Here is some sample code I wrote up for the use of traceStack = true. I have not tried to run it, but it should be mostly correct.

let moduleBytes = wasmTextToBinary(`
(module

(import "" "tag" $tag (tag i32))
(import "" "throwExceptionWithStack" $throwExceptionWithStack (param i32))
(func $throwException (param i32)
	local.get 0
	throw $tag
)
(func (export "run1")
	i32.const 1
	call $throwException
)
(func (export "run2")
	i32.const 2
	call $throwExceptionWithStack
)
`);

let tag = new WebAssembly.Tag(['i32']);
let throwExceptionWithStack = (param) => {
	throw new WebAssembly.Exception(tag, [param], {traceStack: true});
};
let module = new WebAssembly.Module(moduleBytes);
let instance = new WebAssembly.Instance(module, {
	"": {"tag": tag, "throwExceptionWithStack": throwExceptionWithStack}
});
let {run1, run2} = instance.exports;

try {
	run1(10);
} catch (e) {
	assert(e instanceof WebAssembly.Exception);
	assert(!(e instanceof WebAssembly.RuntimeError));
	assert(!(e instanceof Error));
	assert(e.stack === undefined);
	assert(e.getArg(tag, 0) === 10);
}

try {
	run2(20);
} catch (e) {
	assert(e instanceof WebAssembly.Exception);
	assert(!(e instanceof WebAssembly.RuntimeError));
	assert(!(e instanceof Error));
	assert(e.stack !== undefined);
	assert(e.getArg(tag, 0) === 20);
}

Thanks Ryan. That's a huge help. I'm pretty sure I get most of this now, which is nothing short of a miracle :-).

I have done the JavaScript class stuff in https://github.com/mdn/content/pull/14925 - feel free to check it out.
Still a bit rough around the edges.
After this I will provide additional context, which will be a lot of info from your post 2 above.

A few notes/questions

  1. The fact that tags have identity makes a lot of things more clear. Thanks.

  2. You only need the tag to throw an exception and to decode it. So if you create a tag in JS and then that JS is called from WASM and throws, you only need to also import the tag into the module IFF you want the module to be able to interpret the exception. You don't have to if you're happy for the exception to propagate up to JS to handle, or for it to be caught in a catch_all. That's what the examples do now in the linked docs anyway.

  3. FYI only ...

    What attribute is this? WA.Tags only have 'identity' and 'sequence of data types'.

    I am no longer sure. Ignore please.

  4. How do you run that wasmTextToBinary sample? Is there a shell or something where this method is defined?
    I'm running wat2wsm locally because https://webassembly.github.io/wabt/demo/wat2wasm/is not working for exceptions, but that is a bit slow.

  5. I can see how this works for me as a test code writer. What if I want to port a C++ app, that happens to throw a particular exception. Would it be that a tool author for C++ (say) would define some standard exceptions and map the standard libraries etc such that there would be a whole lot of exported "standard exceptions"? I guess I'm trying to understand if this is just a fundamental precondition that people writing code use, or is it something that will be an intermediate step for emscripten and others? Hope that makes sense.

Flags: needinfo?(rhunt)

Sorry for the delay, I was on leave last week.

(In reply to Hamish Willee from comment #10)

  1. You only need the tag to throw an exception and to decode it. So if you create a tag in JS and then that JS is called from WASM and throws, you only need to also import the tag into the module IFF you want the module to be able to interpret the exception. You don't have to if you're happy for the exception to propagate up to JS to handle, or for it to be caught in a catch_all. That's what the examples do now in the linked docs anyway.

Yes, that's correct.

  1. How do you run that wasmTextToBinary sample? Is there a shell or something where this method is defined?
    I'm running wat2wsm locally because https://webassembly.github.io/wabt/demo/wat2wasm/is not working for exceptions, but that is a bit slow.

The SpiderMonkey engine has a JS shell CLI program which contains a wasmTextToBinary function. I think you can use jsvu [1] to download a SpiderMonkey shell, but I haven't done that in a while. The other way would be to compile SM from source [2].

There's also wasm-tools [3] which should be runnable in CLI and contains the text to binary converter that we use in SpiderMonkey.

[1] https://github.com/GoogleChromeLabs/jsvu
[2] https://firefox-source-docs.mozilla.org/js/build.html
[3] https://github.com/bytecodealliance/wasm-tools/

  1. I can see how this works for me as a test code writer. What if I want to port a C++ app, that happens to throw a particular exception. Would it be that a tool author for C++ (say) would define some standard exceptions and map the standard libraries etc such that there would be a whole lot of exported "standard exceptions"? I guess I'm trying to understand if this is just a fundamental precondition that people writing code use, or is it something that will be an intermediate step for emscripten and others? Hope that makes sense.

This depends a bit on how the C++ toolchain wants to use the wasm feature, so it depends.

My understanding is that C++ compiled with LLVM will generate a single tag to represent 'any exception'. A C++ source level throw of any exception object will use this tag in the wasm throw instruction. The wasm tag/exception will contain a single i32 value which is a pointer into wasm memory referencing the thrown C++ exception object. So a C++ app would only have a single wasm tag for all C++ exceptions. This tag could be exported so that JS could decode C++ exceptions. This description of how C++ uses wasm exceptions may not be entirely correct, it's from viewing some disassembly and conversations - not thorough investigation.

I have also heard that LLVM may use the wasm exception-handling proposal to implement the setjmp/longjmp feature [1]. In this case they would define another tag for use in all setjmp/longjmp code to distinguish that from exceptions.

[1] https://en.cppreference.com/w/cpp/utility/program/setjmp

Flags: needinfo?(rhunt)
See Also: → 1791361
Regressions: 1797685
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: