Skip to content

Conversation

@Arshia001
Copy link
Member

@Arshia001 Arshia001 commented Nov 25, 2025

Summary and explanation

This rather large PR add support for async execution (through Function::call_async and the Function::new_*_async family).

The design is explained in WARP-43.

A call to Function::call_async starts a new async context and a coroutine. The async context allows imported functions to suspend; such functions may be created with a call to Function::new_*_async, and passing in an async or otherwise future-returning host function.

New coroutines may be started simultaneously by making further calls to Function::call_async, either alongside the first call_async or within imported functions. The embedder shall be responsible for driving the resulting futures to completion.

Note that this PR changes how Store and AsStoreMut work fundamentally, including breaking changes to their API. In the new model, Stores use an Rc internally, which lets them stay alive as long as necessary. However, since many coroutines may be accessing the store simultaneously, the Store also uses an internal single-threaded read-write lock. To access the store's data, this lock must be taken, which is why Store itself no longer implements AsStoreRef and AsStoreMut. Embedders may gain access to an AsStoreMut by calling Store::as_mut, which acquires a write lock on the store and places it in the returned AsStoreMut implementation.

Since async functions may suspend, the lifetime of the write lock must be handled with more care; this is why Function::call_async receives a &Store directly. However, as long as the store remains locked, the future cannot progress; For example, this code results in a deadlock:

// Create a new store, and a clone of it
let store = Store::new();

// Hypothetically clone the store - this is possible because an Rc
// is used, but the API is not exposed to avoid this exact deadlock
// scenario
let store_clone = store.clone();

// Lock the store down
let mut store_mut = store.as_mut();

// Get a function reference
let func : Function = ...;

// Call the function via the clone of the store and await it
func.call_async(&store_clone).await;

// Deadlock happens - the internal lock is held by `store_mut`. As soon as the
// lock is dropped, the future will receive a notification and resume. However,
// since `store_mut` is owned by this function, and we're already waiting for
// the future to run, we have a deadlock.

Note that, as of this version, the futures spawned by Function::call_async are not Send. A single-thread async runtime may be used to drive multiple coroutines forward on the same thread.


Original PR description

This PR adds two functions mimicking the JS-promise-integration API for WebAssembly (jspi):
https://github.com/WebAssembly/js-promise-integration

Following the new JS methods, this PR introduces:

  • A new Function::new_async(...), Function::new_with_env_async(...), Function::new_typed_async(...) and Funciton::new_typed_with_env_async(...) as to mimic new WebAssembly.Suspending(...)
  • A new way of calling suspending-function that is as simple as function.call_async(...) instead of just function.call(...) to mimic WebAssembly.promising API

Tips for reviewers

This PR was designed in stages; as we discovered new potential for better usability and better APIs, we would go back to do more design work. As such, it is recommended to review the PR stage by stage. The stages are as follows:

syrusakbary and others added 27 commits November 18, 2025 17:57
* Add proper trap code for suspending in non-async context
Copilot AI review requested due to automatic review settings November 25, 2025 12:59
Copilot finished reviewing on behalf of Arshia001 November 25, 2025 13:00
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces async execution support to Wasmer through Function::call_async and Function::new_*_async APIs. The implementation follows the JSPI (JavaScript Promise Integration) proposal, allowing WebAssembly coroutines to suspend and resume during execution. Key architectural changes include:

  • New Store locking mechanism: Store now uses an internal Rc<LocalRwLock> to enable concurrent async contexts while maintaining single-threaded execution
  • Breaking API changes: Store no longer directly implements AsStoreRef/AsStoreMut; instead, embedders must call Store::as_ref() or Store::as_mut()
  • Coroutine support: Futures spawned by Function::call_async are !Send and must run on single-threaded async runtimes

Reviewed changes

Copilot reviewed 94 out of 95 changed files in this pull request and generated no comments.

Show a summary per file
File Description
lib/api/src/entities/store/local_rwlock.rs New single-threaded async-aware RwLock implementation for store access
lib/api/src/entities/store/context.rs Thread-local store context stack for managing active StoreMut instances
lib/api/src/entities/store/asynk.rs AsyncStore trait and async lock implementations
lib/api/src/entities/store/store_ref.rs Refactored AsStoreRef/AsStoreMut traits to work with new locking
lib/api/src/entities/store/mod.rs Updated Store with internal RwLock and new as_ref()/as_mut() methods
lib/api/src/entities/function/async_host.rs New AsyncHostFunction trait for async host functions
lib/api/src/entities/function/inner.rs Added async function creation and calling logic
lib/api/tests/jspi_async.rs Comprehensive tests for async execution including JSPI example
lib/api/tests/simple-greenthread.rs Tests for coroutine switching with async contexts
lib/wasix/src/syscalls/* Updated WASIX syscalls to use new Store API
tests/lib/wast/src/*.rs Updated test infrastructure for new Store API
tests/wasix/vfork/main.c Replaced invalid function pointer trap with __builtin_trap()

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Arshia001 and others added 4 commits November 25, 2025 18:25
…rom the store context

This can be useful if the embedder knows what they're doing, which is the case with our own c-api crate.
/// ```
/// # use wasmer::{Global, Mutability, Store, Value};
/// # let mut store = Store::default();
/// # let mut store = store.as_mut();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No no no , this should not be needed / required

/// ```should_panic
/// # use wasmer::{Global, Store, Value};
/// # let mut store = Store::default();
/// # let mut store = store.as_mut();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a non-desired change.

/// ```
/// # use wasmer::{Memory, MemoryType, Pages, Store, Type, Value};
/// # let mut store = Store::default();
/// # let mut store = store.as_mut();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is non desired change

/// (import "host" "func2" (func))
/// )"#;
/// let module = Module::new(&store, wat)?;
/// let module = Module::new(&store.engine(), wat)?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a no-go

// Tests that the table type of `table` is compatible with the export in the WAT
// This tests that `wasmer_types::types::is_table_compatible` works as expected.
let mut store = Store::default();
let module = Module::new(&store, WAT).unwrap();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UNDO

#[cfg_attr(feature = "wasmi", ignore = "wasmi does not support funcrefs")]
fn table_new() -> Result<(), String> {
let mut store = Store::default();
let mut store = store.as_mut();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No-go

let print_f32 = Function::new_typed(store, |val: f32| println!("{val}: f32"));
let print_f64 = Function::new_typed(store, |val: f64| println!("{val}: f64"));
let print_i32_f32 = Function::new_typed(store, |i: i32, f: f32| {
let mut store_mut = store.as_mut();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be needed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name this file async_ or _async

StoreObjects::same(&a.inner.objects, &b.inner.objects)
}
pub struct StoreMut {
pub(crate) inner: LocalWriteGuardRc<StoreInner>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This affects all the backend. It should not

/// # Usage:
/// ```no_run
/// use wasmer::{Store, Exports, Module, Instance, imports, Imports, Function, FunctionEnvMut};
/// # fn foo_test(mut store: &mut Store, module: Module) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UNDO

@zebreus
Copy link
Contributor

zebreus commented Nov 26, 2025

@Arshia001 I'll add my review after you addressed syrus comments

@Arshia001
Copy link
Member Author

Closing this in favor of #5920, which contains an improved design.

@Arshia001 Arshia001 closed this Nov 26, 2025
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