Proposal idea: adding `must_throw` signature
I spent my share of time debugging errors like: null function or function signature mismatch that, as the error suggest, it's unclear to pin point.
must_throw
Preliminary proposal
Adding a new function signature, must_throw. Function with signature must_throw take no inputs, have no outputs, and must provably throw an exception.
Changes in validation rules
Functions with signature must_throw can be called with call and call_indirect of any signature without being checked, given they guarantee to throw and so are compatible with a variable amount of inputs and outputs.
Functions with signature must_throw must provably throw. A strict version could be no control flow allowed and ending up in a throw. Or only forward control flow plus throws in all path.
Use case
Primary usage I had in mind is for this feature is for having a must_throw function placed at index 0 in a function table, so that performing load + call_indirect, that is a common pattern for possibly unset virtual calls (due to null pointer dereferences), will not anymore trigger a RuntimeError due to signature mismatch BUT will call the relevant error function, that allows to customize behaviour, and allows toolchains to offer better error messages.
Alternatives
Alternative to offer a better developer or user experience currently do incur a runtime cost and this pattern is, as far as I am aware, still prevalent and somewhat of an clunky angle of WebAssembly.
Open questions
There is some design space on exact validation rules, such as:
- what control flow are allowed for
must_throwfunctions? - can
must_throwcall other functions? with what rules? (it becomes hard to guarantee loops can be avoided, if that is a concern) - can a
callorcall_indirectalso havemust_throwsignature? Single block, no further calls, and only used in declarations is enough for my scope, but they might be unnecessary restrictive and prevent further usage of thismust_throw.
Does this is covered by already existing proposals or have interactions with them?
Also questions whether this in some form has a reasonable shot at standisation and feedback from toolchain developers on whether this might actually solve a problem would be cool.
Very much looking forward for feedback, thanks!
A very much pseudo-wast example:
(module
(func $simple-func)
(func $complex-func (param f64 i64 i32))
(func $will-throw-error (must_throw))
(type $empty-signature (func))
(type $complex-signature (func (param i64 f64 i32)))
(type $throw-signature (must_throw))
(table funcref
(elem
$throw-signature $simple-signature $complex-signature)
)
(func (export "some_simple_function")
)
(func (export "some_more_complex_function")
;; valid paths
(call_indirect (type $simple-func) (i32.const 1))
(call_indirect (type $complex-signature) (f64.const 0) (i64.const 0) (i32.const 0) (i32.const 2))
;; RuntimeErrors due to mismatched function
(call_indirect (type $simple-func) (i32.const 2))
(call_indirect (type $complex-signature) (f64.const 0) (i64.const 0) (i32.const 0) (i32.const 1))
;; those will throw whathever $will-throw-error implements
(call_indirect (type $simple-func) (i32.const 0))
(call_indirect (type $complex-signature) (f64.const 0) (i64.const 0) (i32.const 0) (i32.const 0))
;; note that 0 is valid for both, even though they do not share the correct signature
)
(func
;; cleanup / some other code
;; set exception
throw
)
)
I wonder if proposing a new kind of function signature, which would make verification more complicated for all implementors, might be overmuch for the goal, which is simply for toolchains to provide better error messages when index 0 of a function table is accessed. Rather than addressing this at the WebAssembly spec level, it might be better for runtimes to provide a better, more descriptive error message when accessing an empty index 0 of a table. That way it wouldn't be restricted to null function pointers, but also null ref-type pointers, null object pointers and so on.
@IFcoltransG: I don't see how can you really distinguish, with 0 runtime cost, between actual calling a wrong signature or having a special index (0 for C-based languages) that is actually special. I do see this might be adding too much surface to be worth, thanks for the feedback!
Another feature that can be implemented on top of must_throw is allowing, at the WebAssembly level, to customize RuntimeErrors, potentially implementing them as simple WebAssembly level throws to be then catched in the WebAssembly stack.
For that having a special signature / tag similar to must_throw might be handy, and on top there needs to be a way to specify which need to become the hook to implement each error (possibly a special optional table that maps error indexes to functions handling them)
Can you elaborate on what most_throw would be used for? I'm not fully understanding the use case, especially because I wouldn't have expected overhead to be a concern for responding to an unrecoverable error.
You are likely right that this is over-engineering, but I don't see how you can, within Wasm, to mark a function as special and say "index 0" is just different from the others / can have a different failure mode.
I think the bigger goal would be allow WebAssembly to handle (within the language) try catches on runtime problems, like:
;; some code
call_indirect %some-signature ;; this might throw a RuntimeError
;; some other code for the happy path
catch {
;; code to catch the exception thrown by call_indirect and handling it however implementer/toolchain see fit
}
and what I though would be a way would be having call_indirect succeed but calling a must_throw function, and then this function can then throw the relevant Wasm-level exception that can be caught down the stack.
Does this make more sense? Thanks
I think I'm following. So the idea is that they provide special exceptions for calling certain parts of the function table, ones that could presumably be handled from within WebAssembly at a lower cost than null-checking all your pointers in advance. It seems like it's less about better error messages and more about being able to handle the exception conditions. I'll propose another alternative (for after Exception Handling is standardised): rather than using WebAssembly's untyped function reference tables, use multiple typed tables so that you can know the type of each indirect call statically. Then for each function type in a table, set index 0 of that table to a function that takes the parameters and throws the desired exception. I believe you could then catch the exception. Would that tradeoff work with this use case?
Another alternative possible today would be to replace call_indirect with a sequence of instructions like this:
call_indirect $f
=>
block (param t1* i32) (result t2*)
table.get $indirect_call_table
block (param funcref) (result (ref $f))
br_on_cast 0 funcref (ref $f)
call $throw-invalid-call-error
unreachable
end
call_ref $f
end
where $f is (func (param t1*) (result t2*))
If the relaxed dead code validation proposal advances, another option would be to allow the bottom instruction type to appear syntactically in function types. Functions with these types would be proven by validation to trap or throw. This isn't quite enough for your use case, though, because the function you place at index zero would still have to have some specific function type returning bottom, and that function type may not be a subtype of the intended type of the call_indirect, so you would still get an unhelpful error message when call_indirect tries and fails to cast to the expected type. What you really need is a way to type functions with some near-bottom function type that is a subtype of all defined function types and a supertype of nofunc. This seems fairly far fetched unless we can show e.g. significant performance improvements as the result of new engine optimizations it would unlock, though.
@tlively: thanks for your comment.
Another angle, that at least it's somehow connected, would it be possible to allow overriding of errors with call to functions that can be proven will throw? Basically to provide a way to override RuntimeErrors with functions that then throw a WebAssembly-level exception? (that than can be caught within WebAssembly or throw externally)
Here I don't see this completely, but there is any previous material on this?
Being able to catch and handle traps is a common feature request. Here's one discussion on it with links to other discussions as well: https://github.com/WebAssembly/exception-handling/issues/206