-
Notifications
You must be signed in to change notification settings - Fork 822
Add partial support for -fwasm-exceptions in Asyncify (#5343) #5475
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
base: main
Are you sure you want to change the base?
Conversation
0b27b2e to
6f1e8b5
Compare
kripken
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting idea! I think this makes sense to do.
6447962 to
5dbbd2c
Compare
|
Refactored, now I change only top-level catch blocks. Also added lit tests, for me seems correct. Dosbox-x works fine. // br_if case with condition and value
Index newLocal = builder->addVar(func, br->value->type);
auto setLocal = builder->makeLocalSet(newLocal, br->value);
auto getLocal = builder->makeLocalGet(newLocal, br->value->type);
auto condition = br->condition;
br->condition = nullptr;
br->value = getLocal;
auto decIf =
builder->makeIf(condition,
builder->makeSequence(builder->makeDec(), br),
getLocal);
replaceCurrent(builder->makeSequence(setLocal, decIf));Not sure how to deal if sideeffect will be in |
5dbbd2c to
3282bfe
Compare
|
@kripken is there anything preventing this PR from being merged? |
|
I'm sorry, I want to finish this and add the changes suggested by @kripken, I'll try to find some time to finish it (not sure I can do it anytime soon). Btw I've used it many times and it works great in the case of js-dos. |
|
Excellent work @caiiiycuk! I look forward to seeing this merged! |
|
@caiiiycuk I am currently testing your branch and have updated the binaries in
I think the warning is fine. But I am confused as to why |
|
I just it like this Did you check, maybe you set flag |
50dc3a6 to
0092287
Compare
7efcd66 to
15bfc21
Compare
|
@kripken I apologize for the delay. I have made all the changes you requested. Please take a look and let me know if there's anything else to do. |
|
@kripken friendly ping |
|
I have been using this for several weeks now and I haven't found any issues yet! Excellent work @caiiiycuk! |
|
@allsey87 thanks, great to know |
|
@caiiiycuk Sorry, I've been meaning to get to this but have been busy. Thanks for updating this! There has recently been a suggestion to make some changes in how wasm exceptions works in the spec, see WebAssembly/exception-handling#280 - I have no idea what will happen there, but it might mean that if we land this we'd need to then refactor it significantly. On the positive side, they do mention it is a goal to make things like Asyncify easier, so perhaps this will become simpler actually. Either way, though, I think we should wait on this PR to see what the spec changes are first, to avoid wasted work for all of us. |
|
@kripken No worries, all good. At least it works for my case. :) |
15bfc21 to
22a248b
Compare
|
Is there any chance that we can merge this? JSPI still need to take some time... |
|
The new wasm EH proposal I mentioned above is in the process of being implemented now. The main component is this: Lines 1473 to 1474 in 3049fb8
If we want to land this PR then targeting that would make sense, as the old EH will be removed. However, the new wasm EH is not done yet (e.g. parts of the optimizer don't work on it) so it's too early for that, I'm afraid. |
|
Yeah I haven't been doing anything tricky. The one catch I see is that indirect calls typically need to be instrumented for Asyncify, and Rust seems to use them a lot. It's very plausible that the Rust exceptions code would use indirect calls. I don't know whether that use would be covered by this PR or not. You can opt specific functions out of Asyncify instrumentation (asyncify_remove), but it's hard to know when it's safe to. My asyncify_advise log has over 1500 lines. |
|
Well. Our formatting code typically goes through a vtable for size reasons, which I believe winds up with an indirect call, and while if memory serves the unwinding itself doesn't involve formatting, the panic messaging sure does. |
|
It is a tad too late in the day for me to explain in detail what the difference is between "starting a panic", "running the panic handler" (which usually is the thing that does the message printing, if I am not somehow misremembering everything...), and "unwinding (due to a panic)" but these are technically all their own little thing and it's not entirely clear to me what ASYNCIFY needs and doesn't need, here. This is why "inscrutable binary editing with unclear parameters but it works, probably" is usually something to which we say, "Have fun with that!" Except that it is apparently core to some usage of the emscripten target, and, well, if we don't support what the emsdk can do, then what exactly does define the emscripten target for us? That is a philosophical question which may be useful to address one day but also may be nice to kick down the road a bit. |
|
Yes, sometimes it’s really hard to understand all the technical details, especially when you don’t fully know the supporting technology. If we simplify things a lot, code like this is considered normal: while (true) {
try {
// ...
asyncify_sleep(10);
} catch (...) {
// anything
}
}while code like this is not supported: while (true) {
try {
// ...
asyncify_sleep(10);
} catch (...) {
// anything
asyncify_sleep(10); // <--- not supported
}
}I believe this approach will succeed because code is typically written like in block 1 — a At least in the case of incorrect code organization, we’ll see a clear, understandable error indicating that asyncify_sleep was called in the wrong place. Overall, if we look only at Emscripten, I don’t see any negative side effects from merging this PR. I’ve been maintaining it for about two years without major issues. |
src/passes/Asyncify.cpp
Outdated
| }; | ||
|
|
||
| // Add catch block counters to verify that unwind is not called from catch | ||
| // block. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we depend on the IR structure to find if we are inside a catch? See ExpressionStackWalker which maintains a stack of parents. findBreakTarget() there shows an example of scanning the parents.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, I will try to adopt this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have been thinking about this task, and so far I do not understand how to make it more efficient. We do not actually need a parent stack, because we do not care which nested catch block we are currently in. Essentially, I only want to know whether we are inside a catch or not. In the current code, we have a counter for this, which is effectively just a boolean flag. If I am not mistaken, it is always either 0 or 1, which means we could add additional assert that checks it falls within that range.
The core problem is that at compile time we cannot determine whether a function that changes the Asyncify state called inside a catch or not. More precisely, we could determine that if there were no virtual function calls. I have not yet found a way to work around this at compile time, which is why a counter is used.
I still do not quite understand how to apply a parent stack here. I could maintain such a stack at runtime, but that seems worse than just using an integer counter.
Or is the idea to keep the counter, but update it using the information obtained from ExpressionStackWalker?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, obviously it can be greater than 1, but at least it cannot be less than 0.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The core problem is that at compile time we cannot determine whether a function that changes the Asyncify state called inside a catch or not.
We may not know if there is a catch further up, at runtime, but we will reach it when we unwind, I think? So the compile-time knowledge seems enough. Here is what I mean:
function foo() {
try {
..
} catch (e) {
bar();
// after call to bar
}
}
function bar() {
importThatPauses();
}Say we reach that call to bar, then it calls the import that pauses. Inside bar, we don't know that we are inside a catch from earlier. But after bar unwinds normally, we reach "after call to bar" and we can have a static assert there about not unwinding.
That is, each catch block can, statically, get added checks to not unwind, after each possibly-unwinding operation there?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(and iirc we already have a way to add checks for "should not be unwinding here", so I hope this is not hard to add)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh, sounds correct. But then we cannot support --pass-arg=asyncify-ignore-unwind-from-catch if I understood correctly. But we can add those static checks in case if this flag is not passed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see now, thanks. So that option makes it necessary to know immediately if we are nested under a catch...
Co-authored-by: Alon Zakai <[email protected]>
Co-authored-by: Alon Zakai <[email protected]>
Co-authored-by: Alon Zakai <[email protected]>
At compile time or run time? |
At runtime. However, you can also avoid this by using a special flag that ignores |
|
Specifically asyncify_sleep or all async functions? The other big question is if you think async in catch is a fundamental incompatibility, or instead a big difficulty that might be supportable with a lot more work? |
|
Is it possible to use any compile time analysis tool or runtime check to tell whether any rewind/unwind happening inside a catch block? |
Any async function — they’re all implemented the same way. But I don’t know if it’s even possible to make Asyncify work inside a
Yes, to some extent — it’s similar to automatically generating a list of functions that modify |
Potentially, it’s possible to implement a check with two outcomes:
|
src/passes/Asyncify.cpp
Outdated
| // Add catch block counters to verify that unwinding is not done from catch | ||
| // block. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // Add catch block counters to verify that unwinding is not done from catch | |
| // block. | |
| // Add catch block counters to verify that unwinding is not done from inside | |
| // a catch block. Note that we need to know if we are nested under a catch | |
| // immediately - we can't depend on unwinding noticing it when we reach | |
| // that parent catch - because of the option asyncify-ignore-unwind-from-catch. |
src/passes/Asyncify.cpp
Outdated
| // - break outside top-level/nested catch 0 (ignored) | ||
| // | ||
| // - exiting from top-level catch -1 | ||
| // - exiting from nested catch 0 (ignored) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, detecting an exit from a catch is not as simple as this. You would also need to handle br_on and return_call, off the top of my head.
We could in theory say this feature doesn't support GC or tail calls, but imagine this is linked with a module using those features, and then inlining happens, so it seems worrying.
How important is asyncify-ignore-unwind-from-catch? Without that, we could use the more static approach I mentioned before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Oh, and also try_table - people could mix exnref EH and non-exnref EH.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally I don't use asyncify-ignore-unwind-from-catch, I provided it for completeness, and because it is easy. So, I think can remove it, but can't guess if it can be used by someone else. Also I think it is easy to fix in code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean if needed devs can fix code to support something like ignore this asyncify_sleep
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I don't think anyone else mentioned a need for that option, so maybe it is best to remove it. Can maybe wait a bit to see if anyone on this PR responds.
Hmm, that may be slightly easier to run into than I thought! Though I still wouldn't expect it to happen via the Rust side because of how Rust code uses exceptions only, well, exceptionally. @curiousdannii It may be possible to mitigate the indirection problem if necessary... or at least help you with the |
|
Even with JS exceptions I need to work out what I can asyncify_remove. Here's what I'm working with. Wish me luck 😂 Edit: great news: I've discovered that at least one of the apps I build in my project can go back to using ASYNCIFY_IGNORE_INDIRECT. I'd disabled it when I first switched to Rust. Possibly changing to panic=abort made the difference. |
20ef2ec to
3ca7b8a
Compare
|
Please take a look at the last two commits. I attempted to implement the following algorithm:
More details on point 2, since I am not entirely sure the current implementation covers all cases—mainly because I may not be aware of every detail. I use I tested this code with js-dos, and so far I haven’t encountered any issues. However, I am still not completely confident in it overall. |
kripken
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks pretty good! I don't see anything incorrect.
| return std::make_unique<AsyncifyAssertUnwindCorrectness>(analyzer, module); | ||
| } | ||
|
|
||
| bool isFunctionParallel() override { return true; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a convention we prefer this at the top of the class, before all other internals.
| } | ||
|
|
||
| void visitCall(Call* curr) { | ||
| assert(!expressionStack.empty()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A return_call (curr->isReturn) can be ignored here: It returns first, leaving the Catch, before calling.
| bool isFunctionParallel() override { return true; } | ||
|
|
||
| void runOnFunction(Module*, Function* function) override { | ||
| auto builder = std::make_unique<Builder>(*module); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Builders are very cheap to construct - please create one in replaceCallWithCheck, as needed, which would be simpler.
| void addFunctions(Module* module, bool asserts) { | ||
| Builder builder(*module); | ||
| auto makeFunction = [&](Name name, bool setData, State state) { | ||
| auto* body = builder.makeBlock(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why move this?
| } | ||
|
|
||
| void addFunctions(Module* module) { | ||
| void addFunctions(Module* module, bool asserts) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New param seems unused?
| PassRunner runner(module); | ||
| runner.add(std::make_unique<AsyncifyAssertInNonInstrumented>( | ||
| &analyzer, pointerType, asyncifyMemory)); | ||
| if (asserts) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is already inside an if (asserts), line 1860
Co-authored-by: Alon Zakai <[email protected]>
Co-authored-by: Alon Zakai <[email protected]>
Co-authored-by: Alon Zakai <[email protected]>
Co-authored-by: Alon Zakai <[email protected]>
Co-authored-by: Alon Zakai <[email protected]>
Co-authored-by: Alon Zakai <[email protected]>
Co-authored-by: Alon Zakai <[email protected]>
You can download binaries of wasm-opt that support fwasm-exceptions and Asyncify here.
Replace
emsdk/upstream/bin/wasm-optwith downloaded version and then you can use -fwasm-exceptions with Asyncify.--
Hi. This PR does not do lowering wasm exceptions to MVP as suggested in #5343, so I need to explain situation:
I am trying to port a dosbox/dosbox-x core as is to WebAssembly. The dosbox core is syncrhnous, it runs emulation in forever while loop, events are processed inside this loop. To make this loop asynchrous I use Asyncify, however the unmodified core uses exceptions in certain situations.
Asyncify does not support exceptions, this PR should change the situation in some cases including dosbox/x case.
The main idea of emulation loop is follow:
The catch blocks is used to recover core, or to jump out from some of deep cpudecode calls. There is no situation when emulation loop is called when catch block somewhere in stack:
This also means that catch block can't change the state, and so only try block can. But on other hand try block it self should works fine with current asyncify code. When we are doing rewind we are only visiting try blocks and never catch blocks, so it work out of the box.
For such type of synchronous code we can add exceptions support, we need only verify that unwind didn't start from a catch block (when catch is not in stack). We can validate this requirment and throw exception if it breaks or ignore the unwind call.
This is very small overhead and simple solution to bring exceptions support in Asyncify, more over for some codebases it should be simple to rewrite the synchronous loop to not call unwind from a catch blocks.
This PR is tested on dosbox-x, it works fine.
emscripten-discuss initial context
There is one thing in impelementation that probably is not correct:
I have a global variable that I need to decrease on some amount just before
br_ifinstruction,but only when condition is true. I tried lot of other variants but no luck:
Result:
Result:
Just wrap the br_if but save condition result into local var like in 1. Results is sames as in 2.
Completely replace br_if with if and br:
Results:
OR
Block,Loop,If,Drop,Try, etc. I don't have straightforward algorithm in my mind.Thanks!