-
Notifications
You must be signed in to change notification settings - Fork 73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mapping Java null-pointer exceptions to WasmGC #208
Comments
Here's another idea (thanks to @ecmziegler for bringing it up):
I like this idea. One particular nicety is that we could easily make this a post-MVP follow-up (if we decide that long-term we want it for improved efficiency, but short-term it's not urgent enough to add to the MVP). |
I feel the "this would bloat the module size" argument takes us back to the discussion about compression, since nearly any compressor would do a good job compressing the branch-around + operate-safely pattern. It would be good to see some data. The wasm generator should itself be able to elide null checks where they are redundant, or binaryen might. The hook idea seems unpleasantly noncompositional and nonlocal and also like a new type of exception handling mechanism. I think it would be better to look for a local solution with properly exposed control flow. If the pair of nullcheck + operate is too large, maybe an instruction that merges the two is better, though eventually we'll have multibyte opcodes so there's a limit to how compact we can make that. |
It's more than that: a |
Supporting 'compile-time' functions aka macros would go a long way to addressing code size problems. |
As a writer of a java compiler I like the idea of registering a global function without any parameter as a hook. The registering should be pro trap type. Then it is possible to register the same function, different functions or nothing. The hook functions must throw an exception. possible trap types of interest:
If we want add add parameters to the hook functions then every hook needs different parameters
|
Perhaps I'm biased toward having lots of instructions by working on SIMD, but I think the simplest and most composable solution would be to create throwing versions of all the relevant instructions that currently trap. For example, we could have |
At least for use in browsers, can't JS catch a Wasm trap and convert it into an exception of whatever? I understand we'd prefer a solution in pure Wasm, but this could work for J2CL presumably? Is anything specified about what a WASI host can do with Wasm traps? |
Yes it can, but doing so unrolls the Wasm stack all the way to its entry point (i.e. the topmost JS frame). So if you have a Java function |
I guess for now the options are to trampoline through JS before executing trapping operations to convert traps into exceptions or to guard against trapping operations explicitly in the Wasm. Based on our experience doing something like the former to support C++ exceptions in Emscripten without the EH proposal, my guess is that the explicit guards in WebAssembly would be more attractive. |
IMO, having a 'standard' trap->exception function seems like a 'bad idea'(tm). It makes the specification much more difficult to reason about. This is especially true if that becomes an environmental aspect (like whether the code is running in a browser or not). |
Well, since this issue is considering all sorts of possible new additions to Wasm, having a way to create a new JS frame on top of the stack upon trap would be a possibility? Basically tell the JS API to not unwind. Or are there engine/JS limitations that would make this a bad idea? Could be useful for other things than just catching Java NPEs. |
FYI, this is a summary of the rationale on why we decided not to catch traps, after long discussions: WebAssembly/exception-handling#1 (comment) I think reasons in this summary still stand, and I don't think making all traps catchable is a good idea. But I agree that there are ways we can think more about, including suggestions in this issue. To summarize what have been suggested here:
|
For 5. and 6. |
I sounds like that we are trying to work around existing assumptions due existing languages target of WASM. In managed environments, apps cannot result in trap / segmentation fault or anything that would cause a crash (assuming no VM bugs). For JVM in particular, every app level problem including even stack overflow, hitting heap memory limits or even excessive GC are catchable at application level so they could be handled gracefully. So I believe having traps and being them not catchable is already favors particular language style instead of making it less language dependent. I think having ability to influence this behavior at module level (either opting in to make traps catchable or ability to change what is thrown with a hook) seems reasonable to me. |
With respect to instrumentation option: Generally speaking, I believe having more bloat makes it harder to reason about the code at optimization (either offline or engine level). Macros seems generally useful to reduce the size in this context and many other patterns that we generate with J2CL but it doesn't directly address the complexity impact from the optimizations perspective. |
I was just about to suggest something similar to the hook function, and then I saw it was the second comment. This is the least invasive change to Wasm, but it has problems with composability that need to be worked around. For one, composing modules from different languages would be difficult if the hook is global--it might need to be per-module, at the least. While wasm doesn't yet have threads or thread-local state, it would probably be best to specify the hook in a way that is forward-compatible with thread-local variables, i.e. that it can be mutated at least by the thread itself. E.g. there might be different contexts within one module that want to handle traps in different ways. In general, having implicit nullchecks (via trapping on As @aheejin mentions, I think catching traps by default is not a good option, because Java exceptions generally have stacktraces associated with them, and I think we want to avoid requiring engines to collect stacktraces for exceptions if we can. Even though one can suppress stacktraces for Java exceptions, and VMs sometimes optimize them away, it is generally the case that they are needed. |
From @aheejin's list, I think (2) is by far the most attractive option.
I agree this isn't a good idea, and as @aheejin points out, that is already the recorded consensus of the CG.
This wouldn't be too bad, I believe. There aren't that many instructions that trap, and not all of them need catchable alternatives. For example, memory out of bounds accesses, i.e., loads and stores probably don't. And it's only fairly few other cases, e.g, I count 5 relevant instructions in the GC MVP. (And even in case we wanted catchable loads/stores, they could be represented with just a flag bit in the instruction encoding.)
That seems more complex and less clear and adaptable than (2). For example, there are certain cases of null that are programmatically useful in suitable contexts (e.g., when a struct is null), while others can only ever originate from fatal compiler bugs (e.g., when an RTT is null).
As @lars-t-hansen points out, this does not compose. As a general rule, there must not be any stateful behaviour modes that are global or tied to modules, because either fundamentally conflicts with modular composition, transformations, and refactorings. The only way in which this would not cause serious issues is as an explicit, scoped control-flow construct, essentially like an exception handler. But then it's simpler to reuse exceptions themselves.
AFAICS, this doesn't address this use case well, since an engine would still have to recognise certain instruction patterns to optimise them. |
It doesn't have to be an exception handler. If we have thread-local variables, we could have a control flow construct that introduces a |
Adding new throwing instructions seems clearly simpler than adding hooks or new control flow constructs because it does not introduce anything fundamentally new to the spec and does not raise any composability or forward compatibility questions (at least so far!). For those who prefer a hook mechanism, what downside do you see in adding new throwing instructions? |
Where would you resume after the hook was invoked? AFAICS, it can only be after the end of that construct. And then it's pretty much isomorphic to an exception handler. |
It is a trap handler hook, so if the handler didn't explicitly throw or
otherwise set a resumption point, the runtime should trap.
…On Wed, Apr 28, 2021 at 1:59 AM Andreas Rossberg ***@***.***> wrote:
It doesn't have to be an exception handler. If we have thread-local
variables, we could have a control flow construct that introduces a let-like
scope, assigning a value to a thread-local variable at the beginning of the
scope and restoring the variable to the previous value upon exit (all
paths) from the scope.
Where would you resume after the hook was invoked? AFAICS, it can only be
after the end of that construct. And then it's pretty much isomorphic to an
exception handler.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#208 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AC46VVBCEN3TGBE72N73NHDTK6W5JANCNFSM43SXNGGQ>
.
|
If you are going to have an instruction throw an exception on trap, you must also solve the next two problems: what is the nature of that exception and (assuming you solve the first) how do I map that exception to C++ exceptions/Python exceptions/JS exceptions. |
@tlively with throwing instruction there is the problem that the exception must be converted to an exception that the languages are expected. For example for Java there must be allocated an object NullPointerException on the heap. With a hook this can be simple throw an exception that is compatible to all the try catch constructs of the language. |
Adding to that, if ever WebAssembly wants to support in-application stack tracing for exceptions, then the handler should happen "in place" so that it can use the stack marks or the like at that location to determine the relevant stack info. The aforementioned issue with compositionality by having the trap handler be specified per function rather than per module. Due to interop constraints (e.g. calling to imported functions), any trap-handler design/implementation will likely have to reasonably accommodate at least per-function granularity anyways. Finer grained than that would probably not be useful and would be challenging for engines (as trap handlers might do "meaningful" things that an optimizer has to account for). For that same reason, the trap handler should probably always be specified through lexical scope rather than dynamic scope; that is, a function's trap handler has no effect on how traps within functions it calls are handled. |
It solves the most problematic parts; array and property accesses but not the overall problem. Anything else that may result in trap need to be instrumented (casts, arithmetic, etc) or need a throwing version of the same instruction.
That should not be a problem for our case; we had the same issue in the JS land and on catch we create the proper exception type expected from Java. |
Oh right, I hadn't thought about all the user-space setup a runtime would have to do before actually throwing an exception. That being said, throwing instructions still seems like the right solution to me. The exception thrown from e.g. a divide by zero would be caught (probably nearby in the same function) by a handler that allocates whatever objects are necessary to construct the "real" exception then throws that exception. This is similar to the per-function trap handler idea, except not tied to function granularity and composed of concepts that already exist. |
If this wasn't a hook but there was a module level setting that let the module to choose to catch traps or not (i.e. not configurable after declaration nor configurable per trap), would that still have the same concerns of having hooks? |
Yes, WebAssembly has so far (almost) avoided having module-level configuration bits and hooks like that to keep modules composable and decomposable. Whether the module-level configuration were a bit or a hook, it would still make it impossible to statically merge and optimize two modules that use different configurations. |
My concern was primarily about scalability with existing instructions. But as @rossberg suggested if it can be done by setting a flag, I think this can be a viable option too.
I think different languages should use different sets of instructions that make sense to them. For example, throwing version of trapping instructions wouldn't make sense in C++, so C++ will continue to use the original trapping version. |
Yes, but it may be worth the advantages of keeping compilation simpler. It's not clear to me that there are many applications for inlining functions with different handlers (except where the inlined function's handler behaves the same on the traps the inlined function might incur). |
That said, you could have a |
@RossTate A block-scoped |
Throwing versions of instructions does not support the "catching traps from uncooperative modules for testing purposes" use case that I mentioned above. |
It's really no worse than inlining a function that has |
The tooling and engine should be able to statically determine the trap handler. Otherwise completely arbitrary code could be run during any trap, which can make it hard to optimize and compile. (For example, can two divide instructions be reordered?) For this same reason, we want modules to be able to specify very few (e.g. typically one) trap handler so that tooling and optimizers can analyze and summarize it (i.e. what effects do the various handlers have that the optimizer/compiler needs to be conscious of). You also want to think about calls into other wasm modules compiled from other languages (possibly using the "default" trap handlers, i.e. trapping). Your trap handler should not affect how their traps are handled (except possibly for if the call itself traps, i.e. except for unhandled traps - which are then handled as the So for both implementation and semantic reasons, my current thinking is that any design that does not ensure trap handlers are statically determinable is problematic. |
Traps are already catchable in JavaScript, so their precise ordering is observable. That's a good thing, because we don't want engines reordering traps for the same reason we don't want them (observably) reordering stores to memory or other side effects. |
In the current spec, division instructions are reorderable/parallelizable with cast instructions (among many other examples). That would not be the case with arbitrary trap handlers. Also, non-locally-catchable traps are specific to the JS embedder, and my understanding is other embedders are interested in avoiding that partly because of the significant compilation constraints that imposes, instead making sure that a trap terminates all computation that has access to the intermediate state that would expose at which point the trap occurred (such as in the coarsest strategy where a trap simply terminates the whole process). |
That reminds me, another thing to consider in this space is resumable trap handlers. This can be useful for two reasons. The first is fault-tolerant programming and compilation. In this space, it's more important for a program to just keep going even if it's possibly going incorrectly. For example, rather than having The second is optimizability. The above trap handlers for But for resumable trap handlers to be manageable and to benefit from the above observation, it's very important that they be statically determinable. And we wouldn't need to add support for them right away; for now, we could require all trap handlers to have |
@fgmccabe Which concrete data do you need? |
It would be useful to to see how much having to instrument accesses and arithmetic to throw rather than trap under the current proposal increases code size and reduces performance compared to assuming no accesses or arithmetic would trap/throw. Once we have a better understanding of the current costs, we will be able to make more informed assessments of the various proposed solutions. A good next step might be to prototype some of the solutions. |
The answer to that depends on whether we want WebAssembly to (eventually) support generators that would error provenance, whether for semantic or debugging purposes. If we do, then the cost comes down to the combination of frequency of relevant (surface-level) instructions and size increase of their lowerings. For example, an array access in Java for an
Similar for field access via Of course, code size is only one part of the problem. |
Another neat idea due to @sunfishcode: We could have versions of trapping instructions that take a function index immediate and call the function when the normal version of the instruction would otherwise trap. The main idea is that instead of each function having a code block for preparing and throwing an exception, this could be factored out into a single function referenced by all the non-trapping instructions. This is similar to having a global trap handler, but it also uses existing mechanisms and is composable. The precise semantics regarding function arguments, results, whether or not to trap on non-exceptional return from the handler function, etc. can all be fleshed out and bikeshedded if the core idea sounds worth pursuing. |
That still does not address the implementation and optimization concerns above about per-instruction-granularity trap handling. At this point, I don't think the issue is about composability (a function-granularity has module composability, and block-granularity further has function composability). I also don't think this is so much about reusing existing concepts. Rather than making a new variant of every instruction specifying a function for each kind of trap, we could decide to define trap handlers to just specify, for each trapping instruction and each way the instruction could trap (that the handler cares about), a function to call of the appropriate type. To me, the issue is about granularity. Enabling a module to have just a couple of trap handlers would make it reasonably possible (though still non-trivial) for engines to be optimize code with custom trap handlers. I don't believe that will be so easy if every instruction can have custom behavior (at the least optimization will be much more ad hoc). Furthermore, putting optimization aside, it will be easier for engines to implement custom trap handlers via code ranges, for reasons already given above. I'm inferring that this is unappealing to some, and it would be helpful have reasons articulated. |
For the optimization concerns you mentioned, are you thinking about optimizations on the engine side? On the producer side, I don't think there would be any extra optimization issues due to granularity. The important part on the producer side would be that the handler is statically determinable, as you mentioned. I would be interested to hear from engine folks about the tradeoffs around handler granularity. I imagine that much of the existing infrastructure for detecting traps and turning them into JS exceptions could be reused to execute trap handlers with arbitrary granularity, but I may be missing some considerations. |
More so on the engine side, though it's useful to know that on the producer side you don't think there would be a scaling issue. I echo your question for engine folks. My concern is that the existing infrastructure you mention has some slackness to it that the semantic considerations of finer granularity would not permit. Also, my understanding is that not all engines turn traps into exceptions with debug information and so don't need to worry about generally preserving provenance information during compilation, which fine-grained trap handling would impose. Thanks! |
I like this idea. It's a more flexible solution than function or block trap handlers that doesn't require us to go into the weeds of how to define the table that associates each kind of trap with its handler (e.g. NPE for I'm not saying that engines couldn't get some hypothetical benefit from a function-level handler approach, but I'd prefer to go with the most flexible solution as a default and wait for a case from engines that they'd prefer the alternative.
Could you go into more detail about why this is more of a problem for instruction-level trap handling compared to function-level trap handing? In my head, engines would have code generation and optimisation paths for "there is a trap handler associated with the trapping case of this instruction" which (in your scenario) would require provenance information to be preserved, irrespective of whether other code generation/optimisation paths do not preserve provenance, and irrespective of whether the trap handler was defined at an instruction or function granularity. |
From an engine point of view, I'm not concerned about (tail-?)calling a custom function instead of producing a trap: generating a trap is a function call too. Whether that custom function is specified per-module, per-function, per-block, or per-instruction shouldn't make much of a difference for the engine, or for the optimizations it can perform. Example: if the engine is able to optimize away a null-check by propagating some non-null-ness information, then it doesn't matter whether the check that was optimized away would have called the internal "generate a 'null' trap" function or some custom function. Similarly, an engine that builds type propagation infrastructure can determine that a value is non-null after having passed a null-check, regardless of whether that null-check would have produced a trap or an exception or called a function upon failure. |
It seems to me that there are a lot of proposed 'solutions' to a problem that we are not sure is real. (I too am guilty as charged). |
Agreed with @fgmccabe. FWIW, this is a reoccurring kind of discussion. So far, Wasm's design has almost always erred on the side of not introducing "macro instructions". Like, it doesn't even have i32.neg, you need 4 bytes to express that. |
I agree that code size alone is probably not enough to motivate a lot of work here. I guess I've been thinking that avoiding duplicated null checks in the engine and user space would be beneficial, but perhaps that would be trivial for engines to optimize anyway. We should gather data on that as well before committing to pursuing any of these ideas further. That being said, I've found the brainstorming on this thread useful. A number of ideas have come up that I would not have initially considered. I think it's useful to continue sharing ideas in this space as long as we don't get bogged down bikeshedding or debating the details before we have data demonstrating that there is a problem that needs solving. |
It is not only a duplicate null check. There are many more:
For an array index access
This are minimum 14 extra instructions for one array index access. A good solution can be to move this all in an addition function call. But the WASM type check required a different function for every array type. |
While we have the general goal of not adding macro-instructions just for size benefits, wasm has thus far aimed to reliably eliminate runtime checks as a way to increase predictability of performance (reducing dependency on compiler magic) and to help out "baseline" compilers. So +1 to thinking about explicitly parameterizing trapping instructions to have non-trapping paths. Separately, to the question of why we should or shouldn't feel uneasy allowing wasm to handle traps: while I agree there are use cases for allowing wasm to handle traps, and I think we need to eventually address these use cases, I think there is pretty serious ecosystem risk if we allow arbitrary core wasm code to handle traps. In general, a trap means a Bad thing happened, and if arbitrary wasm code can handle traps, I think we'll end up with swallowed traps leading to corrupt continued execution (which is a bad outcome). Rather, I think we should define some explicit "blast zone" unit that semantically contains a set of core instances and low-level mutable state, where a trap always takes down its entire containing blast zone, leaving instances outside the blast zone alive and able to observe the failure. Importantly, having an explicit blast zone unit gives developers a fighting chance to ensure that all state transitively corruptible by a trap is taken down with the trap. (FWIW, how "blast zones" relate to components is an open question to me. Currently, I imagine a blast zone containing multiple component instances; the shared-nothingness of components being a necessary but not sufficient condition for bounding corruption.) |
@lukewagner I agree completely @Horcrux7 the longer list of special operations confirms me in my preferred approach: support some form of macro/inline capability. The logic of this is pretty straightforward: without them Wasm is guaranteed to be less compact than JVM at representing JVM code. (The same is true for any language with a byte code.) |
A macro system or other compression scheme is certainly valuable, but is out of scope for and orthogonal to the GC proposal. Let's move further discussion of macros to a separate thread. You may be interested in the prior proposal for compressing WebAssembly: https://github.com/WebAssembly/decompressor-prototype/blob/master/CompressionLayer1.md. Unfortunately that proposal has been inactive for the last five (!) years. |
I think it has been good to have some higher-level discussion (e.g. should we solve this problem now), but now I'm wondering what the status of this discussion is. There were some lower-level questions asked that I'm happy to continue on, but only if the group is still interested in pursuing such questions. I myself am fine either way. |
Personally, I would like to continue collecting high-level ideas about directions for possible solutions while we await data on the magnitude and character of the problem to guide more detailed and low-level discussions. Of course any new information about the problem space would be very welcome, too. If there is appetite to discuss specific potential solutions in detail right now, perhaps those conversations can be split into separate threads; this high-level thread is already quite long. |
Some more feedback from the J2CL team:
Many operations in Java, such as reading a member field of an object, throw a
NullPointerException
if the object isnull
. Translating such a member field access to a plainstruct.get
operation does not preserve Java semantics, because per the current proposal,struct.get
traps if the object isnull
, and traps are not catchable in Wasm. (One might assume that NPE-throwing Java programs are buggy anyway and hence will not be executed in production; however unfortunately it turns out that such an assumption would be overly idealistic: real-world programs do rely on catching their own NPEs.) The obvious workaround is to put custom instruction sequences into the Wasm module, such asbr_on_null
before every{struct,array}.{get,set}
. There is a concern that these custom checks bloat module size, and possibly hurt performance (depending on what exactly they'll look like, they might e.g. duplicate a check that the engine has to perform anyway -- though I expect that at least in some/simple cases, engines will be able to optimize away implicit checks when an explicit check has just been performed, i.e.struct.get
should be able to not perform a null check if it follows abr_on_null
instruction). The J2CL team is actively working on finalizing their implementation of such custom checks, which should give us some concrete numbers about their cost. I'll definitely follow up here once we have such data.In the meantime, I already wanted to create awareness of this issue. Assuming the cost of custom checks ends up being painful, there are a few things we could do instead:
we could make traps catchable, or introduce a new kind of catchable trap. (I expect that this idea will be unpopular; just mentioning it for completeness. There's no debate that uncatchable Wasm traps are a good way to represent C/C++ segfaults; however with the GC proposal we are clearly moving way beyond the conventions of C-like languages, and it may be worth re-evaluating this choice.)
we could rebase the GC proposal on top of the exception handling proposal, and specify
struct.get
and friends to throw an exception instead of trapping. That would make WasmGC's own semantics a closer match to those managed languages that use NPEs or similar concepts, letting those languages piggyback more often on default Wasm behavior, instead of having to wrap so many operations in their own error-handling implementations.other ideas?
(Of course, we can also do nothing. As discussed many times before, it's impossible for WasmGC to be a perfect match for every source language anyway, and maybe we shouldn't even try to match some source languages particularly well, so as to be source language agnostic. On the other hand, there is still the concern that WasmGC has to prove its usefulness in competitive comparison to existing, well-established and well-optimized technologies such as compiling to JS, so catering especially to certain popular use cases might just be the thing that makes the overall project viable and desirable.)
The text was updated successfully, but these errors were encountered: