Stream: git-wasmtime

Topic: wasmtime / issue #3638 Question: With async support enabl...


view this post on Zulip Wasmtime GitHub notifications bot (Jan 03 2022 at 12:37):

xpepermint opened issue #3638:

It's difficult to understand the whole codebase so I'd better ask experts here to enlighten me. If I understand all these async logic provided by wasmtime, exported WASM functions actually work as async functions when async support is enabled.

So let's see an example (host and module pseudo):

// host
#[tokio::main(flavor = "current_thread")]
pub async fn main() {
    let mut config = Config::new();
    config.async_support(true);
    let engine = Engine::new(&config).unwrap();
    let module = Module::from_file(&engine, "./target/wasm32-wasi/xxx.wasm").unwrap();
   ...
    let mut linker = Linker::new(&engine);
    linker.func_wrap1_async("host", "process", |_caller, x: i32| Box::new(async move {
        sleep(Duration::from_millis(5000)).await;
        Ok(x * 100)
    })).unwrap();
    wasmtime_wasi::add_to_linker(&mut linker, |s| s).unwrap();
    ...
    for _ in 0..10 {
      tokio::spawn(async move {
         ...
         let mut store = Store::new(&engine, wasi);
         let instance = linker.instantiate_async(&mut store, &module).await.unwrap();

         let run = instance_echo.get_typed_func::<(), (), _>(&mut store, "run").unwrap();
         alloc.call_async(&mut store, ()).await.unwrap();

        localasync().await;

         println!("Done");
       });
    }
}
async fn localasync() { ... }
// module
#[link(wasm_import_module = "host")]
extern "C" {
    #[link_name = "process"]
    fn process(a: i32) -> i32;
}
#[no_mangle]
extern "C" fn run() {
  process(1); // can this be threated as safe/true async?
  process(2);  // can this be threated as safe/true async?
  println!("Done");
}

Will run and process be spawned in a dedicated thread where they will block or will the callable sequence of host functions inside run be somehow magically converted to "safe" non-blocking async sequence? I'd like to understand this in-depth so I'd appreciate someone explaining the behind-the-scene execution flow.

view this post on Zulip Wasmtime GitHub notifications bot (Jan 03 2022 at 12:57):

bjorn3 commented on issue #3638:

As far as I know it runs the wasm on the current thread, but on a different stack. When the wasm calls into an async function for which polling results in Poll::Pending, it switched back to the original thread and then returns Poll::Pending from the poll method of the original call into the wasm code,

view this post on Zulip Wasmtime GitHub notifications bot (Jan 03 2022 at 13:04):

xpepermint commented on issue #3638:

@bjorn3 that's how I understand it as well. I'm always afraid of what I don't see/understand thus I"d just like to make sure this approach is not blocking. So it shouldn't block, no meter what you do there, right? I wonder how this separate stack execution works.

view this post on Zulip Wasmtime GitHub notifications bot (Jan 03 2022 at 13:18):

xpepermint edited a comment on issue #3638:

@bjorn3 that's how I understand it as well. I'm always afraid of what I don't see/understand thus I"d just like to make sure this approach is not blocking. So it shouldn't block, no meter what you do there, right? I wonder how this separate stack execution works. Isn't there a stack per process but you can have multiple threads or is it that you have main and sub-thread or smth?

view this post on Zulip Wasmtime GitHub notifications bot (Jan 03 2022 at 13:19):

xpepermint edited a comment on issue #3638:

@bjorn3 that's how I understand it as well. I'm always afraid of what I don't see/understand thus I"d just like to make sure this approach is not blocking. So it shouldn't block, no meter what you do there, right? I wonder how this separate stack execution works. Isn't there a stack per process but you can have multiple threads or is it that you have main and sub-thread which is referred to as a "separate stack" or smth?

view this post on Zulip Wasmtime GitHub notifications bot (Jan 04 2022 at 15:54):

alexcrichton commented on issue #3638:

Wasmtime doesn't spawn any tasks or threads internally, all async happens in the original async task which wasmtime was invoked on. As a practical detail-specific implementation the WebAssembly must be able to be suspended because the host function may not be ready when invoked (as is the case in your sleep example). The Wasm itself uses stack switching to handle that but you're insulated from that in the sense that it's handled by Wasmtime and there's nothing you need to do about it.

Another way to think about this is that the poll function for the future returned from call_async will execute WebAssembly internally up to the point that a host future needs to block and returns NotReady, then the poll function propagates that signal.

view this post on Zulip Wasmtime GitHub notifications bot (Jan 04 2022 at 16:06):

xpepermint commented on issue #3638:

@alexcrichton thank you, that's what I was looking for. So basically wasm implementation is smart enough, won't block threads, and will rather switch between stacks. I also realized that the so-called "stack switching" relates to underlying Crainlift and not OS (e.g Linux).

view this post on Zulip Wasmtime GitHub notifications bot (Jan 04 2022 at 16:06):

xpepermint closed issue #3638:

It's difficult to understand the whole codebase so I'd better ask experts here to enlighten me. If I understand all these async logic provided by wasmtime, exported WASM functions actually work as async functions when async support is enabled.

So let's see an example (host and module pseudo):

// host
#[tokio::main(flavor = "current_thread")]
pub async fn main() {
    let mut config = Config::new();
    config.async_support(true);
    let engine = Engine::new(&config).unwrap();
    let module = Module::from_file(&engine, "./target/wasm32-wasi/xxx.wasm").unwrap();
   ...
    let mut linker = Linker::new(&engine);
    linker.func_wrap1_async("host", "process", |_caller, x: i32| Box::new(async move {
        sleep(Duration::from_millis(5000)).await;
        Ok(x * 100)
    })).unwrap();
    wasmtime_wasi::add_to_linker(&mut linker, |s| s).unwrap();
    ...
    for _ in 0..10 {
      tokio::spawn(async move {
         ...
         let mut store = Store::new(&engine, wasi);
         let instance = linker.instantiate_async(&mut store, &module).await.unwrap();

         let run = instance_echo.get_typed_func::<(), (), _>(&mut store, "run").unwrap();
         alloc.call_async(&mut store, ()).await.unwrap();

        localasync().await;

         println!("Done");
       });
    }
}
async fn localasync() { ... }
// module
#[link(wasm_import_module = "host")]
extern "C" {
    #[link_name = "process"]
    fn process(a: i32) -> i32;
}
#[no_mangle]
extern "C" fn run() {
  process(1); // can this be threated as safe/true async?
  process(2);  // can this be threated as safe/true async?
  println!("Done");
}

Will run and process be spawned in a dedicated thread where they will block or will the callable sequence of host functions inside run be somehow magically converted to "safe" non-blocking async sequence? I'd like to understand this in-depth so I'd appreciate someone explaining the behind-the-scene execution flow.


Last updated: Dec 23 2024 at 12:05 UTC