Skip to content

Async support in the C API #7106

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

Merged
merged 23 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a60ad97
c-api: Add a feature for async
rockwotj Sep 28, 2023
c13ba81
c-api: Add support for async config
rockwotj Sep 28, 2023
42f38d0
c-api: Add support for calling async functions
rockwotj Sep 28, 2023
7f06025
c-api: Add ability to yield execution of Wasm in a store
rockwotj Sep 28, 2023
4739b6b
c-api: Introduce wasmtime_linker_instantiate_async
rockwotj Sep 28, 2023
eaf2bb6
c-api: Support defining async host functions
rockwotj Sep 28, 2023
2a7d830
gitignore: ignore cmake cache for examples
rockwotj Sep 28, 2023
6d183a4
examples: Add example of async API in C
rockwotj Sep 28, 2023
d09d856
c-api: Consolidate async functionality into a single place
rockwotj Sep 29, 2023
aa5e91f
c-api: Make async function safe
rockwotj Sep 29, 2023
9271efb
c-api: Remove wasmtime_call_future_get_results
rockwotj Sep 29, 2023
574de30
c-api: Simplify CHostCallFuture
rockwotj Sep 29, 2023
66da177
c-api: Simplify C continuation implementation
rockwotj Sep 29, 2023
44d9cac
c-api: Improve async.h documentation
rockwotj Sep 29, 2023
fadb7f3
c-api: Cleanup from previous changes
rockwotj Sep 29, 2023
855da7d
examples: Fix example
rockwotj Sep 29, 2023
549b655
c-api: Simplify continuation callback
rockwotj Oct 1, 2023
58894ca
c-api: Fix async.h documentation
rockwotj Oct 1, 2023
e6e8870
c-api: Fix documentation for async.h
rockwotj Oct 2, 2023
8aee8d3
c-api: Review feedback
rockwotj Oct 2, 2023
3c7616c
examples: Downgrade async.cpp example to C++11
rockwotj Oct 3, 2023
ca20623
c-api: initialize continuation with a panic callback
rockwotj Oct 3, 2023
9e7054a
prtest:full
rockwotj Oct 3, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ foo
publish
vendor
examples/build
examples/.cache
*.coredump
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion crates/c-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ wasmtime-wasi = { workspace = true, default-features = true, optional = true }
cap-std = { workspace = true, optional = true }
wasi-common = { workspace = true, optional = true }

# Optional dependencies for the `async` feature
futures = { workspace = true, optional = true }

[features]
default = ['jitdump', 'wat', 'wasi', 'cache', 'parallel-compilation']
default = ['jitdump', 'wat', 'wasi', 'cache', 'parallel-compilation', 'async']
async = ['wasmtime/async', 'futures']
jitdump = ["wasmtime/jitdump"]
cache = ["wasmtime/cache"]
parallel-compilation = ['wasmtime/parallel-compilation']
Expand Down
30 changes: 30 additions & 0 deletions crates/c-api/include/wasmtime/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,36 @@ WASM_API_EXTERN void wasmtime_config_cranelift_flag_enable(wasm_config_t*, const
*/
WASM_API_EXTERN void wasmtime_config_cranelift_flag_set(wasm_config_t*, const char *key, const char *value);

/**
* \brief Whether or not to enable support for asynchronous functions in Wasmtime.
*
* When enabled, the config can optionally define host functions with async.
* Instances created and functions called with this Config must be called through their asynchronous APIs, however.
* For example using wasmtime_func_call will panic when used with this config.
*
* For more information see the Rust documentation at
* https://docs.wasmtime.dev/api/wasmtime/struct.Config.html#method.async_support
*/
WASMTIME_CONFIG_PROP(void, async_support, bool)

/**
* \brief Configures the size of the stacks used for asynchronous execution.
*
* This setting configures the size of the stacks that are allocated for asynchronous execution.
*
* The value cannot be less than max_wasm_stack.
*
* The amount of stack space guaranteed for host functions is async_stack_size - max_wasm_stack, so take care
* not to set these two values close to one another; doing so may cause host functions to overflow the stack
* and abort the process.
*
* By default this option is 2 MiB.
*
* For more information see the Rust documentation at
* https://docs.wasmtime.dev/api/wasmtime/struct.Config.html#method.async_stack_size
*/
WASMTIME_CONFIG_PROP(void, async_stack_size, uint64_t)

#ifdef __cplusplus
} // extern "C"
#endif
Expand Down
134 changes: 134 additions & 0 deletions crates/c-api/include/wasmtime/func.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,66 @@ typedef wasm_trap_t* (*wasmtime_func_callback_t)(
wasmtime_val_t *results,
size_t nresults);

/**
* The callback to determine a continuation's current state.
*
* Return true if the host call has completed, otherwise false will
* continue to yield WebAssembly execution.
*
* \param if error is assigned a non `NULL` value then the called function will
* trap with the returned error. Note that ownership of error is transferred
* to wasmtime.
*/
typedef bool (*wasmtime_func_async_continuation_callback_t)(
void *env,
wasmtime_caller_t *caller,
wasm_trap_t **error);

/**
* A continuation for the current state of the host function's execution.
*
* This continutation can be polled via the callback and returns the current state.
*/
typedef struct wasmtime_async_continuation_t {
wasmtime_func_async_continuation_callback_t callback;
void *env;
void (*finalizer)(void *);
} wasmtime_async_continuation_t;

/**
* \brief Create a new continuation for the callback.
*
* \param callback the function to call each time the continuation is polled.
* \param env the parameter will be passed to the callback on each invocation.
* \param finalizer the function to delete env when the continuation is destroyed.
* May be `NULL` to omit any cleanup.
*/
WASM_API_EXTERN wasmtime_async_continuation_t *wasmtime_async_continuation_new(
wasmtime_func_async_continuation_callback_t callback,
void *env,
void (*finalizer)(void *));

/**
* \brief Callback signature for #wasmtime_linker_define_async_func.
*
* This is a host function that returns a continuation to be called later.
* The continuation returned is owned by wasmtime and will be deleted when it completes.
*
* All the arguments to this function will be kept alive until the continuation
* returns that it has errored or has completed.
*
* Only supported for async stores.
*
* See #wasmtime_func_callback_t for more information.
*/
typedef wasmtime_async_continuation_t *(*wasmtime_func_async_callback_t)(
void *env,
wasmtime_caller_t *caller,
const wasmtime_val_t *args,
size_t nargs,
wasmtime_val_t *results,
size_t nresults);

/**
* \brief Creates a new host-defined function.
*
Expand Down Expand Up @@ -307,6 +367,80 @@ WASM_API_EXTERN void *wasmtime_func_to_raw(
wasmtime_context_t* context,
const wasmtime_func_t *func);

/**
* \brief The structure representing a asynchronously running function.
*
* This structure is always owned by the caller and must be deleted using wasmtime_call_future_delete.
*
*
*
*/
typedef struct wasmtime_call_future wasmtime_call_future_t;

/**
* \brief Executes WebAssembly in the function.
*
* Returns true if the function call has completed, which then wasmtime_call_future_get_results should be called.
* After this function returns true, it should *not* be called again for a given future.
*
* This function returns false if execution has yielded either due to being out of fuel
* (see wasmtime_store_out_of_fuel_async_yield), or the epoch has been incremented enough
* (see wasmtime_store_epoch_deadline_async_yield_and_update).
*
* The function may also return false if asynchronous host functions have been called, which then calling this
* function will call the continuation from the async host function.
*
* For more see the information at
* https://docs.wasmtime.dev/api/wasmtime/struct.Config.html#asynchronous-wasm
*
*/
WASM_API_EXTERN bool wasmtime_call_future_poll(wasmtime_call_future_t *future);

/**
* /brief Frees the underlying memory for a future.
*
* All wasmtime_call_future_t are owned by the caller and should be deleted using this function no
* matter the result.
*/
WASM_API_EXTERN void wasmtime_call_future_delete(wasmtime_call_future_t *future);

/**
* \brief Obtains the results for a wasm call execution.
*
* This method should only be called on a wasmtime_call_future_t after wasmtime_call_future_poll has returned true.
*
* The `trap` pointer cannot be `NULL`.
*/
WASM_API_EXTERN wasmtime_error_t *wasmtime_call_future_get_results(
wasmtime_call_future_t *fut,
wasm_trap_t **trap);

/**
* \brief Invokes this function with the params given, returning the results asynchronously.
*
* This function is the same as wasmtime_func_call except that it is asynchronous.
* This is only compatible with stores associated with an asynchronous config.
*
* The result is a future that is owned by the caller and must be deleted via #wasmtime_call_future_delete.
*
* The `args` and `results` pointers may be `NULL` if the corresponding length is zero.
*
* Does not take ownership of #wasmtime_val_t arguments or #wasmtime_val_t results,
* the arguments and results must be kept alive until the returned #wasmtime_call_future_t is deleted.
*
* See #wasmtime_call_future_t for for more information.
*
* For more information see the Rust documentation at
* https://docs.wasmtime.dev/api/wasmtime/struct.Func.html#method.call_async
*/
WASM_API_EXTERN wasmtime_call_future_t* wasmtime_func_call_async(
wasmtime_context_t *context,
const wasmtime_func_t *func,
const wasmtime_val_t *args,
size_t nargs,
wasmtime_val_t *results,
size_t nresults);

#ifdef __cplusplus
} // extern "C"
#endif
Expand Down
36 changes: 36 additions & 0 deletions crates/c-api/include/wasmtime/linker.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
#include <wasmtime/error.h>
#include <wasmtime/store.h>
#include <wasmtime/extern.h>
#include <wasmtime/instance.h>
#include <wasmtime/module.h>

#ifdef __cplusplus
extern "C" {
Expand Down Expand Up @@ -119,6 +121,23 @@ WASM_API_EXTERN wasmtime_error_t* wasmtime_linker_define_func(
void (*finalizer)(void*)
);

/**
* \brief Defines a new async function in this linker.
*
* This function behaves similar to #wasmtime_linker_define_func, except it supports async
* callbacks
*/
Copy link
Member

Choose a reason for hiding this comment

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

This I think warrants a bit of expansion and more explanation. This might be another good use case for an async.h header file which could have a dedicated comment at the top discussing how async works.

  • All arguments to async functions are "captured" for the entire duration of the future. They must stay valid and no other modifications are allowed during the lifetime of the future, or it's UB. This is a guarantee to wasmtime_func_async_callback_t provided by Wasmtime, but also something embedders must guarantee when calling APIs such as wasmtime_linker_instantiate_async
  • One point to emphasize is that this includes store, meaning that concurrent invocations of futures are not allowed. Instead only one future per store can be active at any one point in time. My guess is that many people may get this wrong by accident so it's definitely something I want called out in the documentation. Ideally there'd be a double-check in the C API which sets/clears a flag and panics if the flag is cleared indicating that a future is active, but that's ok to do as a follow-up.
  • For wasmtime_linker_define_async_func it's worth noting that the native code defined by cb will not be invoked on the current stack but will be invoked on a separate stack (and a separate fiber on Windows). This can possibly be important for some embeddings.

Basically I think it's ok for the docs on individual functions to be a bit terse, but there are a huge number of gotchas with getting async right in C. There's no protection lifetime-wise when it's most critically required for async so I'd want to make sure that we at least have some longer-form documentation explaining some of the hazards and what embedders need to consider when designing their own async support. If you're up for starting the documentation I can try to help fill it in too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ideally there'd be a double-check in the C API which sets/clears a flag and panics if the flag is cleared indicating that a future is active, but that's ok to do as a follow-up.

Not that my embedding does this - but is it safe to call back into the VM from a async host function? That maybe a reason to not add this check.

I took a stab at the documentation. I will admit documentation is not my strong suit so help is appreciated (or I'm happy to rubber duck/review a followup PR on improving the documentation).

WASM_API_EXTERN wasmtime_error_t *wasmtime_linker_define_async_func(
wasmtime_linker_t *linker,
const char *module,
size_t module_len,
const char *name,
size_t name_len,
const wasm_functype_t *ty,
wasmtime_func_async_callback_t cb,
void *data,
void (*finalizer)(void *));

/**
* \brief Defines a new function in this linker.
*
Expand Down Expand Up @@ -218,6 +237,23 @@ WASM_API_EXTERN wasmtime_error_t* wasmtime_linker_instantiate(
wasm_trap_t **trap
);

/**
* \brief Instantiates a #wasm_module_t with the items defined in this linker for an async store.
*
* This is the same as #wasmtime_linker_instantiate but used for async stores
* (which requires functions are called asynchronously). The returning #wasmtime_call_future_t
* must be polled using #wasmtime_call_future_poll, and is owned and must be deleted using #wasmtime_call_future_delete.
* The future's results are retrieved using `wasmtime_call_future_get_results after polling has returned true marking
* the future as completed.
*
* All arguments to this function must outlive the returned future.
*/
WASM_API_EXTERN wasmtime_call_future_t *wasmtime_linker_instantiate_async(
const wasmtime_linker_t *linker,
wasmtime_context_t *store,
const wasmtime_module_t *module,
wasmtime_instance_t *instance);

/**
* \brief Defines automatic instantiations of a #wasm_module_t in this linker.
*
Expand Down
26 changes: 26 additions & 0 deletions crates/c-api/include/wasmtime/store.h
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,32 @@ WASM_API_EXTERN void wasmtime_context_set_epoch_deadline(wasmtime_context_t *con
*/
WASM_API_EXTERN void wasmtime_store_epoch_deadline_callback(wasmtime_store_t *store, wasmtime_error_t* (*func)(wasmtime_context_t*, void*, uint64_t*), void *data);

/**
* \brief Configures a Store to yield execution of async WebAssembly code periodically.
*
* When a Store is configured to consume fuel with #wasmtime_config_consume_fuel
* this method will configure what happens when fuel runs out. Specifically executing
* WebAssembly will be suspended and control will be yielded back to the caller.
*
* This is only suitable with use of a store associated with an async config because
* only then are futures used and yields are possible.
*/
WASM_API_EXTERN void wasmtime_context_out_of_fuel_async_yield(
wasmtime_context_t *context,
uint64_t injection_count,
uint64_t fuel_to_inject);

/**
* \brief Configures epoch-deadline expiration to yield to the async caller and the update the deadline.
*
* This is only suitable with use of a store associated with an async config because
* only then are futures used and yields are possible.
*
* See the Rust documentation for more:
* https://docs.wasmtime.dev/api/wasmtime/struct.Store.html#method.epoch_deadline_async_yield_and_update
*/
WASM_API_EXTERN void wasmtime_context_epoch_deadline_async_yield_and_update(wasmtime_context_t *context, uint64_t delta);

#ifdef __cplusplus
} // extern "C"
#endif
Expand Down
12 changes: 12 additions & 0 deletions crates/c-api/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,15 @@ pub unsafe extern "C" fn wasmtime_config_cranelift_flag_set(
let value = CStr::from_ptr(value).to_str().expect("not valid utf-8");
c.config.cranelift_flag_set(flag, value);
}

#[no_mangle]
#[cfg(feature = "async")]
pub extern "C" fn wasmtime_config_async_support_set(c: &mut wasm_config_t, enable: bool) {
c.config.async_support(enable);
}

#[no_mangle]
#[cfg(feature = "async")]
pub extern "C" fn wasmtime_config_async_stack_size_set(c: &mut wasm_config_t, size: usize) {
c.config.async_stack_size(size);
}
Loading