bytecodealliance / javy

JS to WebAssembly toolchain
Apache License 2.0
2.1k stars 101 forks source link

How to use Javy with asynchronous JavaScript code? (experimental_event_loop) #626

Closed konradkoschel closed 3 months ago

konradkoschel commented 3 months ago

What is your question?

Hi, I try to execute asynchronous JavaScript code using Javy. the simplified JavaScript code looks like this:

const input = JSON.parse(readInput()); // Read STDIN as described in README

(async () => {
  const result = await foo(input);

  writeOutput(result); // Write to STDOUT as described in README
})();

async function foo(params) {
  // ...
}

I am using a setup where I use the Javy Provider Module created via javy emit-provider -o javy-provider.wasm.

The JavaScript code itself is compiled via javy compile my.js -o out.wasm -d.

I execute everything in Wasmtime, and for sync code this works as expected. When using async code in the JavaScript code, I get the error Error while running JS: Adding tasks to the event queue is not supported.

I researched the origin of the issue and found out that in the file crates/core/src/execution.rs pending code (i.e. Promises) is only executed if the feature experimental_event_loop is set.

Therefore, I tried the following steps to fix my issue:

  1. I cloned the javy repository
  2. I installed all build prerequisites
  3. I ran cargo build -p javy-core --target wasm32-wasi -r --features experimental_event_loop
  4. I ran cargo build -p javy-cli -r --features experimental_event_loop
  5. I recompiled the JS with the same command as specified above
  6. I recreated the provider module with the same command as specified above
  7. I tested the execution again

Unfortunately, I ended up with the very same issue. Have I missed something?

Thanks for your help and clarification

P.S.: I am running on ARM-based MacOS, Rust 1.76.0, and built Javy on commit 8f4468c97c1bbf69321d40cc5e5e6ec14d5b685c. The wasmtime(-wasi)/wasi-common version for the execution of the WASM is 15.0.0

jeffcharles commented 3 months ago

I'm not able to reproduce the issue you ran into.

I created a module with this code:

(async () => {
  const result = await foo();

  console.log(result);
})();

async function foo(params) {
    return 'blah';
}

Then tested that compiling this with the default configuration outputs Error while running JS: Adding tasks to the event queue is not supported:

➜  javy git:(main) ✗ cargo build -p javy-cli -r      
   Compiling javy-cli v1.4.0 (/Users/jeffcharles/src/github.com/Shopify/javy/crates/cli)
    Finished release [optimized] target(s) in 1m 23s
➜  javy git:(main) ✗ target/release/javy compile -d index.js -o index.wasm
➜  javy git:(main) ✗ target/release/javy emit-provider -o provider.wasm                      
➜  javy git:(main) ✗ wasmtime run --preload javy_quickjs_provider_v1=provider.wasm index.wasm
Error while running JS: Adding tasks to the event queue is not supported
Error: failed to run main module `index.wasm`

Caused by:
    0: failed to invoke command default
    1: error while executing at wasm backtrace:
           0: 0xd5f17 - <unknown>!abort
           1: 0x283bd - <unknown>!std::sys::pal::wasi::abort_internal::h9244d020deeb80f3
           2: 0x3097 - <unknown>!std::process::abort::h9c7c1ac47130d7a9
           3: 0x28e9 - <unknown>!javy_quickjs_provider::execution::run_bytecode::h2795cc611b4a3c8a
           4: 0x506e - <unknown>!eval_bytecode
           5: 0xe0a90 - <unknown>!eval_bytecode.command_export
           6:   0xd0 - <unknown>!<wasm function 2>
       note: using the `WASMTIME_BACKTRACE_DETAILS=1` environment variable may show more debugging information
    2: wasm trap: wasm `unreachable` instruction executed

Then I tested building with event loop support enabled:

➜  javy git:(main) ✗ cargo build -p javy-core --target wasm32-wasi -r --features=experimental_event_loop
   Compiling javy-core v0.2.0 (/Users/jeffcharles/src/github.com/Shopify/javy/crates/core)
    Finished release [optimized] target(s) in 1.11s
➜  javy git:(main) ✗ cargo build -p javy-cli -r                                              
   Compiling javy-cli v1.4.0 (/Users/jeffcharles/src/github.com/Shopify/javy/crates/cli)
    Finished release [optimized] target(s) in 1m 23s
➜  javy git:(main) ✗ target/release/javy emit-provider -o provider.wasm                      
➜  javy git:(main) ✗ wasmtime run --preload javy_quickjs_provider_v1=provider.wasm index.wasm
blah

Note that the experimental_event_loop feature for the CLI, at the present time, just changes which integration tests are run by cargo test, so you can enable it but you don't have to. You do need to recompile the CLI with a core module that has the experimental event loop feature enabled however. You also don't need to recompile the JS module.

Can you double-check you're still seeing the issue if you run the steps I've outlined above in the same order? If you are seeing the same issue, can you please include all commands you ran in the exact order you ran them including any outputs from those commands?

konradkoschel commented 3 months ago

I tried your snippet and you are right. With this snippet, everything works as it should. However, I digged deeper to figure out why it doesn't work for my setup whereas it does for yours:

In the example that you provided, no actual async logic (such as IO or event loop activities) is performed. Most likely, your code is desugared under the hood to a Promise.resolve() call. I tested the following snippet:

(async () => {
  const result = await foo();

  console.log(result);
})();

async function foo(params) {
  await Promise.resolve("dummy");
  return 'blah';
}

This snippet worked and as expected, blah was outputed.

If actual async logic is involved, the issues come up. See this minimal example using setTimeout:

(async () => {
  console.log("start");
  const result = await foo();

  console.log(result);
})();

async function foo(params) {
  await (
    new Promise((resolve) => {
      setTimeout(() => {
        resolve();
      }, 1000);
    })
  );
  return 'blah';
}

Notice, I also added a log for "start" at the beginning of the async IIFE. When running this example, "start" is printed out, while "blah" is not.

For the execution, this time I used the wasmtime cli in the exact same way as you specified in your response.

jeffcharles commented 3 months ago

Ah! So you're not seeing the error message about adding tasks to the event queue not being supported. But rather you're expecting to see output but not seeing the output.

The script you provided won't execute successfully since Javy does not provide an implementation for setTimeout. Javy only provides ECMAScript APIs along with incomplete support for TextEncoder, TextDecoder, and a couple console methods. setTimeout is not an ECMAScript API. Though we'd be open to a contribution adding an implementation for it.

That said, there is a bug in that Javy should throw an error that an undefined function has been invoked.

Running

setTimeout(() => { console.log("hello") }, 1000);

for example results in

➜  javy git:(main) ✗ wasmtime run index.wasm                           
Error while running JS: Uncaught ReferenceError: 'setTimeout' is not defined
    at <anonymous> (function.mjs:1)

Error: failed to run main module `index.wasm`

Caused by:
    0: failed to invoke command default
    1: error while executing at wasm backtrace:
           0: 0x64894 - <unknown>!<wasm function 119>
           1: 0x738a8 - <unknown>!<wasm function 170>
           2: 0xbeaef - <unknown>!<wasm function 1042>
    2: wasm trap: wasm `unreachable` instruction executed
jeffcharles commented 3 months ago

Filed https://github.com/bytecodealliance/javy/issues/627 to track the lack of error messages and traps when there's an error in ~processing tasks in the event loop~ top-level async functions that are immediately executed.

konradkoschel commented 3 months ago

Yes, that the Error while running JS: Adding tasks to the event queue is not supported was still shown after I switched to the Javy Build with the experimental feature activated was a mistake on my setup – I was still using the original build.

Also, I wasn't aware that setTimeout is not supported at all (in fact I thought that this function was part of the ECMAScript specification). So thank you for the clarification and generally for your help!

That no error is thrown at all is therefore a bug as you said. Since this is captured in #627, I think my question is answered, so feel free to close the issue.