Skip to content
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

Add partial support for -fwasm-exceptions in Asyncify (#5343) #5475

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

caiiiycuk
Copy link
Contributor

@caiiiycuk caiiiycuk commented Feb 2, 2023

You can download binaries of wasm-opt that support fwasm-exceptions and Asyncify here.
Replace emsdk/upstream/bin/wasm-opt with 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:

void emulation() {
  do {
    em_sleep(); // <-- **START_UNWIND**
    try {
      int ret = cpudecode();
      // rest logic
    } catch (PageFault&) {
      // load failed page 
    } catch (Some&) {
      // do some 
    }
  } while (ret);
}

int cpudecode() {
  // some logic that also can call **emulation** recursively
}

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:

void some() {
  while () {
    try {
      ...
      some(); // can call start_unwind
    } catch () {
      // never calls to start_unwind
    }
  }
}

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:

            auto decIf = builder->makeIf(
              br->condition,
              builder->makeSequence(builder->makeDec(amount), br),
              br->value);
            br->condition = nullptr;
            replaceCurrent(decIf);

I have a global variable that I need to decrease on some amount just before br_if instruction,
but only when condition is true. I tried lot of other variants but no luck:

  1. Adding new instruction into condition body:
            Index newLocal = builder->addVar(func, br->condition->type);
            auto setLocal = builder->makeLocalSet(newLocal, br->condition);
            auto getLocal =
              builder->makeLocalGet(newLocal, br->condition->type);
            
            br->condition = builder->makeBlock(
              {setLocal,
               builder->makeIf(getLocal, builder->makeDec(amount)),
               getLocal},
              br->condition->type);

Result:

Assertion failed: (!resultUsed || curr->type != Type::none), function optimize, file Vacuum.cpp, line 95.

>    // resultUsed only makes sense when the type is concrete
>    assert(!resultUsed || curr->type != Type::none);  <-- **ASSERT**
>    // If we actually need the result, then we must not change anything.
>    // TODO: maybe there is something clever though?
>    if (resultUsed) {
>      return curr;
>    }
  1. Just wrap the br_if with new block:
            replaceCurrent(builder->makeSequence(
              builder->makeIf(br->condition, builder->makeDec(amount)), br));

Result:

Assertion failed: (values.size() == 1), function getSingleValue, file wasm-interpreter.h, line 67.

>    if (curr->condition) {
>      Flow conditionFlow = visit(curr->condition);
>      if (conditionFlow.breaking()) {
>        return conditionFlow;
>      }
>      condition = conditionFlow.getSingleValue().getInteger() != 0; // <-- **ASSERT**
  1. Just wrap the br_if but save condition result into local var like in 1. Results is sames as in 2.

  2. Completely replace br_if with if and br:

            auto decIf = builder->makeIf(
              br->condition,
              builder->makeSequence(builder->makeDec(amount), br));
            br->condition = nullptr;
            replaceCurrent(decIf);

Results:

Works fine on small wasm files
On big wasm file have this error:

[wasm-validator error in function CDROM_Interface_Image::LoadCueSheet\28char*\29] unexpected true: if without else must not return a value in body, on

>(if
> (i32.and
>  (i32.load8_u offset=16
>   (i32.add
>    (local.get $8)
>    (i32.load
>     (i32.sub
>      (i32.load
>       (local.get $8)
>      )
>      (i32.const 12)
>     )
>    )
>   )
>  )
>  (i32.const 5)
> )
> (block (result i32)
>  (global.set $__asyncify_catch_counter
>   (i32.sub
>    (global.get $__asyncify_catch_counter)
>    (i32.const 2)
>   )
>  )
>  (br $label$1
>   (i32.const 0)
>  )
> )
>)

OR

[wasm-validator error in function CDROM_Interface_Image::LoadCueSheet\28char*\29] unexpected false: can only drop a valid value, on

>(drop
> (if
>  (i32.and
>   (i32.load8_u offset=16
>    (i32.add
>     (local.get $8)
>     (i32.load
>      (i32.sub
>       (i32.load
>        (local.get $8)
>       )
>       (i32.const 12)
>      )
>     )
>    )
>   )
>   (i32.const 5)
>  )
>  (block (result i32)
>   (global.set $__asyncify_catch_counter
>    (i32.sub
>     (global.get $__asyncify_catch_counter)
>     (i32.const 2)
>    )
>   )
>   (br $label$1
>    (i32.const 0)
>   )
>  )
> )
>)
  1. I thinking to not use replaceCurrent, find the parent block of br_if and insert builder->makeDec just before br, but seems even more cumbersome cause parent block can be Block, Loop, If, Drop, Try, etc. I don't have straightforward algorithm in my mind.

Thanks!

Copy link
Member

@kripken kripken left a 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.

src/passes/Asyncify.cpp Outdated Show resolved Hide resolved
@caiiiycuk
Copy link
Contributor Author

caiiiycuk commented Feb 8, 2023

Refactored, now I change only top-level catch blocks. Also added lit tests, for me seems correct. Dosbox-x works fine.
Only here could be a problem:

// 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 setLocal, currenlty it executes before condition....
@kripken please take a look

src/passes/Asyncify.cpp Outdated Show resolved Hide resolved
src/passes/Asyncify.cpp Outdated Show resolved Hide resolved
src/passes/Asyncify.cpp Outdated Show resolved Hide resolved
src/passes/Asyncify.cpp Show resolved Hide resolved
src/passes/Asyncify.cpp Outdated Show resolved Hide resolved
src/passes/Asyncify.cpp Outdated Show resolved Hide resolved
src/passes/Asyncify.cpp Outdated Show resolved Hide resolved
@allsey87
Copy link

@kripken is there anything preventing this PR from being merged?

@caiiiycuk
Copy link
Contributor Author

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.

@allsey87
Copy link

Excellent work @caiiiycuk! I look forward to seeing this merged!

@allsey87
Copy link

allsey87 commented Jun 15, 2023

@caiiiycuk I am currently testing your branch and have updated the binaries in emsdk/upstream/bin with those from the build. However I am getting some errors when I try to build my test program:

em++: warning: ASYNCIFY=1 is not compatible with -fwasm-exceptions. Parts of the program that mix ASYNCIFY and exceptions will not compile. [-Wemcc]
error: DISABLE_EXCEPTION_THROWING was set (likely due to -fno-exceptions), which means no C++ exception throwing support code is linked in, but exception catching code appears. Either do not set DISABLE_EXCEPTION_THROWING (if you do want exception throwing) or compile all source files with -fno-except (so that no exceptions support code is required); also make sure DISABLE_EXCEPTION_CATCHING is set to the right value - if you want exceptions, it should be off, and vice versa.

I think the warning is fine. But I am confused as to why DISABLE_EXCEPTION_THROWING is being set. I have added -fwasm-exceptions to both the linker flags and compiler flags (both to the C++ and C flags (I guess the latter is not necessary)). Do have any suggestions on how to fix this? I am using Emscripten 3.1.32.

@caiiiycuk
Copy link
Contributor Author

caiiiycuk commented Jun 15, 2023

I just it like this

            -fwasm-exceptions
            "-sASYNCIFY=1"
            "-sASYNCIFY_IMPORTS=['syncSleep']"
            "-sASYNCIFY_WHITELIST=@${TARGETS_DIR}/dosbox-x-asyncify.txt"

Did you check, maybe you set flag -fno-exceptions somewhere.
Also you need to set -fwasm-exceptions both on compile and linking stages.

@caiiiycuk caiiiycuk changed the title Add partial support for -fwasm-exceptions in Asyncify (#5343) WIP: Add partial support for -fwasm-exceptions in Asyncify (#5343) Jun 22, 2023
@caiiiycuk caiiiycuk marked this pull request as draft June 22, 2023 15:39
@caiiiycuk caiiiycuk changed the title WIP: Add partial support for -fwasm-exceptions in Asyncify (#5343) Add partial support for -fwasm-exceptions in Asyncify (#5343) Jun 22, 2023
@caiiiycuk caiiiycuk force-pushed the fwasm-exceptions branch 3 times, most recently from 7efcd66 to 15bfc21 Compare June 23, 2023 20:44
@caiiiycuk caiiiycuk marked this pull request as ready for review June 23, 2023 20:45
@caiiiycuk
Copy link
Contributor Author

@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.

@caiiiycuk
Copy link
Contributor Author

@kripken friendly ping

@allsey87
Copy link

I have been using this for several weeks now and I haven't found any issues yet! Excellent work @caiiiycuk!

@caiiiycuk
Copy link
Contributor Author

@allsey87 thanks, great to know

@kripken
Copy link
Member

kripken commented Jul 26, 2023

@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.

@caiiiycuk
Copy link
Contributor Author

@kripken No worries, all good. At least it works for my case. :)

@fs-eire
Copy link

fs-eire commented Jan 20, 2024

Is there any chance that we can merge this? JSPI still need to take some time...

@kripken
Copy link
Member

kripken commented Jan 22, 2024

The new wasm EH proposal I mentioned above is in the process of being implemented now. The main component is this:

binaryen/src/wasm.h

Lines 1473 to 1474 in 3049fb8

// 'try_table' from the new EH proposal
class TryTable : public SpecificExpression<Expression::TryTableId> {

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants