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

Support Async/Await #376

Open
cromwellian opened this issue Dec 14, 2018 · 22 comments
Open

Support Async/Await #376

cromwellian opened this issue Dec 14, 2018 · 22 comments

Comments

@cromwellian
Copy link

A lot of existing typescript code relies on async/await. And while WASM doesn't have a concept of the JS event loop or generators, it seems like this could be supported in some form by first lowering to a style using switch/case like async/await leveling to ES3-style (See https://blog.mariusschulz.com/2016/12/09/typescript-2-1-async-await-for-es3-es5) in combination with an implementation of Promise in the runtime. Perhaps this code transformation could be copied from the TypeScript compiler, lowering it in the AST before compilation to WASM.

The resolve/reject functions would need to be exported to JS so that the JS could drive the resolution (e.g. Promise((resolve, reject) => {.... setTimeout(() => resolve()) }) would need to be rewritten by the developer so that resolve/reject are exported being passed through some kind of "AssemblyScriptPromise" class that can record when it's invoked and step the generator along. setTimeout is an external function which invokes the resolve)

Since interop with the web is invariably async, this would go a long way to reducing the impedance mismatch.

@dcodeIO
Copy link
Member

dcodeIO commented Dec 14, 2018

Absolutely, async/await is something that we'd like to support eventually. One missing building block at this point seems to be closure support, because promises and timeouts usually involve functions that access variables from a parent scope, though it might be possible to work around this with globals. Might also be good to note that deferring an operation to let's say the network or the filesystem is something that only the host can do at this point, and this leads us to interoperability concerns with the host, where waiting for host-bindings, reference-types et al. might make sense.

@cromwellian
Copy link
Author

Is there a bug to track lambdas? It seems that this could be split into two or three useful partials:

  1. lambdas with no captures
  2. lambdas that capture by value (read-only, equivalent to Java or C++ captures)
  3. lambdas that capture by reference (JS lambdas, C++ [&] lambdas)

#1 seems straight forward, #2 seems straight forward if translated like Java or C++ does it with synthetic functor classes. #3 is a little more difficult since you'd need to promote a local to heap storage, but I think #1 and #2 probably capture most of the functional style of programming I see in JS and TS, and cases like #3 could be temporarily worked around by the developer by boxing the local into a holder object, and relying on support for case #2

@kripken
Copy link

kripken commented Jul 5, 2019

Something that might help here: Binaryen now has support for pausing and resuming wasm in the new "Bysyncify" feature. Basically you get some special functions to unwind/rewind the call stack, and then Bysyncify does everything else for you - that is, it will automatically rewrite all the wasm that needs to be rewritten so that it can save/restore locals and the call stack. This adds overhead as you'd expect, but it's surprisingly small both in size and speed (thanks to integration with the Binaryen optimizer).

Attached is a wip blogpost (not public yet) with more details, including complete examples in pure wasm and in JS+wasm, and benchmarks.

If you're interested to use this, let me know if I can help!

Bysyncify.pdf

@dcodeIO
Copy link
Member

dcodeIO commented Jul 5, 2019

Thanks for the hint (and for making it possible in the first place ofc)! There are multiple things that I think can make use of the bysyncify pass, like

  • async/await once we have an idea how to design a Promise implementation and/or binding
  • Preliminary exception handling?
  • Re-implementations of fs.readFile etc. in context of [Feature] Implement Buffer Class #708 (Edit: Well, that's actually just a callback)
  • Maybe more?

The most important at this point seems to get something-exception-handling up, but I haven't thought about how feasible this would be with bysyncify yet. Would you say that makes sense? Also, there is the mention of "using the option bysyncify-imports to Bysyncify" which I think is not yet possible with the C-API, but that's certainly not a blocker and can be introduced with a PR, if it turns out we need it.

Edit: Just a crazy idea, but if it turns out that bysyncify can help with preliminary exception handling, would it be possible to "polyfill" it on that basis? Like, make actual try/catch blocks, and it would downlevel with a pass?

@kripken
Copy link

kripken commented Jul 6, 2019

Yeah, we could add a pass to lower wasm exceptions into a polyfill basically. Bysyncify has some useful tools for that, like analyzing which calls would need to be instrumented, but the new pass wouldn't need to think about locals etc., so it should have even less overhead.

I believe @aheejin will work on exceptions in binaryen soon. After that's done we can add a lowering/polyfill pass, should be straightforward (I can do it, if no one else wants to).

@dcodeIO
Copy link
Member

dcodeIO commented Jul 6, 2019

Nice, that would be super useful! In the meantime we can start thinking about Promises over here I guess :)

@MaxGraey MaxGraey pinned this issue Oct 22, 2019
@MaxGraey
Copy link
Member

Interesting article about stateless and stateful (fiber) coroutines:
http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2018/p1364r0.pdf

@MaxGraey
Copy link
Member

MaxGraey commented Jan 1, 2020

For async / await which import / export to/from host we could use binaryen's Asyncify, but for internal async / awayt just generate stateless coroutines or generators via FSM approach similar to Python, C# and facebook's regenerator

@dcodeIO dcodeIO unpinned this issue May 16, 2020
@unicomp21
Copy link

@MaxGraey are there examples somewhere showing the unwind/rewind being used in Assemblyscript?

@MaxGraey
Copy link
Member

MaxGraey commented Aug 16, 2020

For host side I saw only this wrapper. In general, you might be inspired this blog post probably

@unicomp21
Copy link

unicomp21 commented May 27, 2021

The co_await implementation in ASIO, could this be leveraged in emscripten? Which could then be leveraged by AssemblyScript?

@unicomp21
Copy link

chriskohlhoff/asio#643

@MaxGraey
Copy link
Member

MaxGraey commented May 27, 2021

co_await spawn + yield_context from boost's asio is stackful coroutines which required to explicit handle stack which not possible in WebAssembly without stack switch proposal or some kind of emulation via asyncify and some host's glue code which is not our philosophy. Btw ECMAScript uses stackless coroutines which also uses in C#, Rust and C++20 Coroutines

@unicomp21
Copy link

unicomp21 commented May 27, 2021

C++20 Coroutines

Isn't co_await part of C++20 coroutines? Not sure I understand.

https://en.cppreference.com/w/cpp/language/coroutines

uses the co_await operator to suspend execution until resumed

Coroutines are stackless

Maybe we're getting confused w/ the C++ fiber api?

@MaxGraey
Copy link
Member

Isn't co_await part of C++20 coroutines? Not sure I understand.

I updated prev comment. I ment routines from asio like spawn and etc. co_await, co_yield and etc it's C++20 already

@jtenner
Copy link
Contributor

jtenner commented May 27, 2021

Hey guys. Can we at least support the async await syntax? Now that visitor-as has some great support for generating ast nodes, it might be fun to start transforming that syntax.

@unicomp21
Copy link

@MaxGraey @dcodeIO If I'm understanding correctly, we simply need to implement the interfaces required by the the clang compiler, to get stackless coroutines using co_await in emscripten, right? And starting w/ the asio implementation might be a fast way of getting there? This implementation could then be leveraged by assemblyscript for async/await?

https://github.com/chriskohlhoff/asio/blob/7fe18ba1b3e2bfddb2ef8dd83883b4545d8444bc/asio/src/examples/cpp17/coroutines_ts/refactored_echo_server.cpp#L29

Do we agree what's being said here is true? Or am I completely confused?
emscripten-core/emscripten#10991 (comment)

@unicomp21
Copy link

unicomp21 commented Jun 4, 2021

The thinking is we could have a c++ microtask executor/scheduler, similar to javascript, which gets called/flushed periodically by a timer on the javascript side. In high performance cases it could be called by requestAnimationFrame. In doing things this way, assemblyscript could leverage clang/c++ co_await directly, right?

@kripken would this work?

@unicomp21
Copy link

I had no idea, co_await actually generates a state machine. Redpanda leverages this heavily. If this conversation comes out the way I hope, I'm wondering if assemblyscript could simply layer itself atop co_await?

WebAssembly/binaryen#4351

@unicomp21
Copy link

BTW, Gor is the guy who created co_await.

@klemens-morgenstern
Copy link

My two cents, after working a lot with C++20 coros.

A stackful coroutine is properly not the way to go with async/await. It is much closer to the stackless nature of the C++20 coros.

But I don't think you'd want to support those either. I've just finished writing some asio python bindings (https://github.com/klemens-morgenstern/asio-py experimental stuff) that allow integration between asio awaitables (the C++20 coros) and python async functions. I was just looking to assemblyscript to see if I maybe can build something similar, i.e. an assembly-script asio-based runtime, e.g. as a light-weight node.js replacement (still looking for a fitting wasm runtime)

My recommendation would be to decouple async as much as possible from the actual implementation. I.e. I would envision this, if assemblyscript can take the return value into account like this:

import asio

async some_coro(sock : asio.ip.tcp.socke) : asio.awaitable<i32>
{
    const bytes_written = await sock.async_write("Hello world!");
    return bytes_written;
}

By the same token one could provide a Promise<> from some-other place:

import {promise} from "my-runtime";
async some_coro(...) : promise<i32>;

This would be modeled on the C++20 design, where the return type dictates the type of coroutine that'll be used. That way my node.js runtime could use the imports from node, while my asio solution would explicitly import another set of classes.

If that is not possible, there needs to be a runtime that picks up the coro and execute it's SM. Python with asyncio does this explicitly:

async def main():
     print('hello')
     await asyncio.sleep(1)
     print('world')

asyncio.run(main())

I think this model would work too, if we add an implicit runtime-function that is not baked into the assembly script itself:

async function foobar() : promise<i32>;

explicitly_scheduler_it_somewhere(foobar.bind()); // similar to the above python
foobar(); // calls special function, e.g. __implicitly_schedule_it_on_my_runtime

That would allow me to plug assemblyscript into another runtime, though I reckon it might introduce some additional steps to glue that into the nodejs runtime.

@yichengxian
Copy link

所以 ,2023了要怎么做呢,有点不太理解
So, how to do it in 2023, a little bit ununderstood

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

No branches or pull requests

8 participants