Implement shared host functions. (#2625)
* Implement defining host functions at the Config level. This commit introduces defining host functions at the `Config` rather than with `Func` tied to a `Store`. The intention here is to enable a host to define all of the functions once with a `Config` and then use a `Linker` (or directly with `Store::get_host_func`) to use the functions when instantiating a module. This should help improve the performance of use cases where a `Store` is short-lived and redefining the functions at every module instantiation is a noticeable performance hit. This commit adds `add_to_config` to the code generation for Wasmtime's `Wasi` type. The new method adds the WASI functions to the given config as host functions. This commit adds context functions to `Store`: `get` to get a context of a particular type and `set` to set the context on the store. For safety, `set` cannot replace an existing context value of the same type. `Wasi::set_context` was added to set the WASI context for a `Store` when using `Wasi::add_to_config`. * Add `Config::define_host_func_async`. * Make config "async" rather than store. This commit moves the concept of "async-ness" to `Config` rather than `Store`. Note: this is a breaking API change for anyone that's already adopted the new async support in Wasmtime. Now `Config::new_async` is used to create an "async" config and any `Store` associated with that config is inherently "async". This is needed for async shared host functions to have some sanity check during their execution (async host functions, like "async" `Func`, need to be called with the "async" variants). * Update async function tests to smoke async shared host functions. This commit updates the async function tests to also smoke the shared host functions, plus `Func::wrap0_async`. This also changes the "wrap async" method names on `Config` to `wrap$N_host_func_async` to slightly better match what is on `Func`. * Move the instance allocator into `Engine`. This commit moves the instantiated instance allocator from `Config` into `Engine`. This makes certain settings in `Config` no longer order-dependent, which is how `Config` should ideally be. This also removes the confusing concept of the "default" instance allocator, instead opting to construct the on-demand instance allocator when needed. This does alter the semantics of the instance allocator as now each `Engine` gets its own instance allocator rather than sharing a single one between all engines created from a configuration. * Make `Engine::new` return `Result`. This is a breaking API change for anyone using `Engine::new`. As creating the pooling instance allocator may fail (likely cause is not enough memory for the provided limits), instead of panicking when creating an `Engine`, `Engine::new` now returns a `Result`. * Remove `Config::new_async`. This commit removes `Config::new_async` in favor of treating "async support" as any other setting on `Config`. The setting is `Config::async_support`. * Remove order dependency when defining async host functions in `Config`. This commit removes the order dependency where async support must be enabled on the `Config` prior to defining async host functions. The check is now delayed to when an `Engine` is created from the config. * Update WASI example to use shared `Wasi::add_to_config`. This commit updates the WASI example to use `Wasi::add_to_config`. As only a single store and instance are used in the example, it has no semantic difference from the previous example, but the intention is to steer users towards defining WASI on the config and only using `Wasi::add_to_linker` when more explicit scoping of the WASI context is required.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
|
||||
use quote::quote;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::parse_macro_input;
|
||||
use wiggle_generate::Names;
|
||||
|
||||
@@ -102,14 +102,23 @@ fn generate_module(
|
||||
let module_id = names.module(&module.name);
|
||||
let target_module = quote! { #target_path::#module_id };
|
||||
|
||||
let ctor_externs = module.funcs().map(|f| {
|
||||
let mut fns = Vec::new();
|
||||
let mut ctor_externs = Vec::new();
|
||||
let mut host_funcs = Vec::new();
|
||||
|
||||
for f in module.funcs() {
|
||||
generate_func(
|
||||
&module_id,
|
||||
&f,
|
||||
names,
|
||||
&target_module,
|
||||
ctx_type,
|
||||
async_conf.is_async(module.name.as_str(), f.name.as_str()),
|
||||
)
|
||||
});
|
||||
&mut fns,
|
||||
&mut ctor_externs,
|
||||
&mut host_funcs,
|
||||
);
|
||||
}
|
||||
|
||||
let type_name = module_conf.name.clone();
|
||||
let type_docs = module_conf
|
||||
@@ -121,7 +130,7 @@ fn generate_module(
|
||||
"Creates a new [`{}`] instance.
|
||||
|
||||
External values are allocated into the `store` provided and
|
||||
configuration of the wasi instance itself should be all
|
||||
configuration of the instance itself should be all
|
||||
contained in the `cx` parameter.",
|
||||
module_conf.name.to_string()
|
||||
);
|
||||
@@ -134,7 +143,7 @@ contained in the `cx` parameter.",
|
||||
|
||||
impl #type_name {
|
||||
#[doc = #constructor_docs]
|
||||
pub fn new(store: &wasmtime::Store, cx: std::rc::Rc<std::cell::RefCell<#ctx_type>>) -> Self {
|
||||
pub fn new(store: &wasmtime::Store, ctx: std::rc::Rc<std::cell::RefCell<#ctx_type>>) -> Self {
|
||||
#(#ctor_externs)*
|
||||
|
||||
Self {
|
||||
@@ -159,16 +168,47 @@ contained in the `cx` parameter.",
|
||||
#(#linker_add)*
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds the host functions to the given [`wasmtime::Config`].
|
||||
///
|
||||
/// Host functions added to the config expect [`set_context`] to be called.
|
||||
///
|
||||
/// Host functions will trap if the context is not set in the calling [`wasmtime::Store`].
|
||||
pub fn add_to_config(config: &mut wasmtime::Config) {
|
||||
#(#host_funcs)*
|
||||
}
|
||||
|
||||
/// Sets the context in the given store.
|
||||
///
|
||||
/// Context must be set in the store when using [`add_to_config`] and prior to any
|
||||
/// host function being called.
|
||||
///
|
||||
/// If the context is already set in the store, the given context is returned as an error.
|
||||
pub fn set_context(store: &wasmtime::Store, ctx: #ctx_type) -> Result<(), #ctx_type> {
|
||||
store.set(std::rc::Rc::new(std::cell::RefCell::new(ctx))).map_err(|ctx| {
|
||||
match std::rc::Rc::try_unwrap(ctx) {
|
||||
Ok(ctx) => ctx.into_inner(),
|
||||
Err(_) => unreachable!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#(#fns)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_func(
|
||||
module_ident: &Ident,
|
||||
func: &witx::InterfaceFunc,
|
||||
names: &Names,
|
||||
target_module: &TokenStream2,
|
||||
ctx_type: &syn::Type,
|
||||
is_async: bool,
|
||||
) -> TokenStream2 {
|
||||
fns: &mut Vec<TokenStream2>,
|
||||
ctors: &mut Vec<TokenStream2>,
|
||||
host_funcs: &mut Vec<TokenStream2>,
|
||||
) {
|
||||
let name_ident = names.func(&func.name);
|
||||
|
||||
let (params, results) = func.wasm_signature();
|
||||
@@ -176,11 +216,15 @@ fn generate_func(
|
||||
let arg_names = (0..params.len())
|
||||
.map(|i| Ident::new(&format!("arg{}", i), Span::call_site()))
|
||||
.collect::<Vec<_>>();
|
||||
let arg_decls = params.iter().enumerate().map(|(i, ty)| {
|
||||
let name = &arg_names[i];
|
||||
let wasm = names.wasm_type(*ty);
|
||||
quote! { #name: #wasm }
|
||||
});
|
||||
let arg_decls = params
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, ty)| {
|
||||
let name = &arg_names[i];
|
||||
let wasm = names.wasm_type(*ty);
|
||||
quote! { #name: #wasm }
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let ret_ty = match results.len() {
|
||||
0 => quote!(()),
|
||||
@@ -188,54 +232,87 @@ fn generate_func(
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
|
||||
let runtime = names.runtime_mod();
|
||||
|
||||
let async_ = if is_async { quote!(async) } else { quote!() };
|
||||
let await_ = if is_async { quote!(.await) } else { quote!() };
|
||||
|
||||
let closure_body = quote! {
|
||||
unsafe {
|
||||
let mem = match caller.get_export("memory") {
|
||||
Some(wasmtime::Extern::Memory(m)) => m,
|
||||
_ => {
|
||||
return Err(wasmtime::Trap::new("missing required memory export"));
|
||||
}
|
||||
};
|
||||
let mem = #runtime::WasmtimeGuestMemory::new(mem);
|
||||
let result = #target_module::#name_ident(
|
||||
&mut *my_cx.borrow_mut(),
|
||||
&mem,
|
||||
#(#arg_names),*
|
||||
) #await_;
|
||||
match result {
|
||||
Ok(r) => Ok(r.into()),
|
||||
Err(wasmtime_wiggle::Trap::String(err)) => Err(wasmtime::Trap::new(err)),
|
||||
Err(wasmtime_wiggle::Trap::I32Exit(err)) => Err(wasmtime::Trap::i32_exit(err)),
|
||||
}
|
||||
}
|
||||
let runtime = names.runtime_mod();
|
||||
let fn_ident = format_ident!("{}_{}", module_ident, name_ident);
|
||||
|
||||
};
|
||||
if is_async {
|
||||
let wrapper = quote::format_ident!("wrap{}_async", params.len());
|
||||
quote! {
|
||||
let #name_ident = wasmtime::Func::#wrapper(
|
||||
store,
|
||||
cx.clone(),
|
||||
move |caller: wasmtime::Caller<'_>, my_cx: &Rc<RefCell<_>> #(,#arg_decls)*|
|
||||
-> Box<dyn std::future::Future<Output = Result<#ret_ty, wasmtime::Trap>>>
|
||||
{
|
||||
Box::new(async move { #closure_body })
|
||||
fns.push(quote! {
|
||||
#async_ fn #fn_ident(caller: &wasmtime::Caller<'_>, ctx: &mut #ctx_type #(, #arg_decls)*) -> Result<#ret_ty, wasmtime::Trap> {
|
||||
unsafe {
|
||||
let mem = match caller.get_export("memory") {
|
||||
Some(wasmtime::Extern::Memory(m)) => m,
|
||||
_ => {
|
||||
return Err(wasmtime::Trap::new("missing required memory export"));
|
||||
}
|
||||
};
|
||||
let mem = #runtime::WasmtimeGuestMemory::new(mem);
|
||||
match #target_module::#name_ident(ctx, &mem #(, #arg_names)*) #await_ {
|
||||
Ok(r) => Ok(r.into()),
|
||||
Err(wasmtime_wiggle::Trap::String(err)) => Err(wasmtime::Trap::new(err)),
|
||||
Err(wasmtime_wiggle::Trap::I32Exit(err)) => Err(wasmtime::Trap::i32_exit(err)),
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
let my_cx = cx.clone();
|
||||
let #name_ident = wasmtime::Func::wrap(
|
||||
});
|
||||
|
||||
if is_async {
|
||||
let wrapper = format_ident!("wrap{}_async", params.len());
|
||||
ctors.push(quote! {
|
||||
let #name_ident = wasmtime::Func::#wrapper(
|
||||
store,
|
||||
move |caller: wasmtime::Caller<'_> #(,#arg_decls)*| -> Result<#ret_ty, wasmtime::Trap> {
|
||||
#closure_body
|
||||
ctx.clone(),
|
||||
move |caller: wasmtime::Caller<'_>, my_ctx: &Rc<RefCell<_>> #(,#arg_decls)*|
|
||||
-> Box<dyn std::future::Future<Output = Result<#ret_ty, wasmtime::Trap>>> {
|
||||
Box::new(async move { Self::#fn_ident(&caller, &mut my_ctx.borrow_mut() #(, #arg_names)*).await })
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ctors.push(quote! {
|
||||
let my_ctx = ctx.clone();
|
||||
let #name_ident = wasmtime::Func::wrap(
|
||||
store,
|
||||
move |caller: wasmtime::Caller #(, #arg_decls)*| -> Result<#ret_ty, wasmtime::Trap> {
|
||||
Self::#fn_ident(&caller, &mut my_ctx.borrow_mut() #(, #arg_names)*)
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if is_async {
|
||||
let wrapper = format_ident!("wrap{}_host_func_async", params.len());
|
||||
host_funcs.push(quote! {
|
||||
config.#wrapper(
|
||||
stringify!(#module_ident),
|
||||
stringify!(#name_ident),
|
||||
move |caller #(,#arg_decls)*|
|
||||
-> Box<dyn std::future::Future<Output = Result<#ret_ty, wasmtime::Trap>>> {
|
||||
Box::new(async move {
|
||||
let ctx = caller.store()
|
||||
.get::<std::rc::Rc<std::cell::RefCell<#ctx_type>>>()
|
||||
.ok_or_else(|| wasmtime::Trap::new("context is missing in the store"))?;
|
||||
let result = Self::#fn_ident(&caller, &mut ctx.borrow_mut() #(, #arg_names)*).await;
|
||||
result
|
||||
})
|
||||
}
|
||||
);
|
||||
});
|
||||
} else {
|
||||
host_funcs.push(quote! {
|
||||
config.wrap_host_func(
|
||||
stringify!(#module_ident),
|
||||
stringify!(#name_ident),
|
||||
move |caller: wasmtime::Caller #(, #arg_decls)*| -> Result<#ret_ty, wasmtime::Trap> {
|
||||
let ctx = caller
|
||||
.store()
|
||||
.get::<std::rc::Rc<std::cell::RefCell<#ctx_type>>>()
|
||||
.ok_or_else(|| wasmtime::Trap::new("context is missing in the store"))?;
|
||||
let result = Self::#fn_ident(&caller, &mut ctx.borrow_mut() #(, #arg_names)*);
|
||||
result
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,16 +42,8 @@ impl atoms::Atoms for Ctx {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_host_func() {
|
||||
let store = async_store();
|
||||
|
||||
let ctx = Rc::new(RefCell::new(Ctx));
|
||||
let atoms = Atoms::new(&store, ctx.clone());
|
||||
|
||||
let shim_mod = shim_module(&store);
|
||||
let mut linker = wasmtime::Linker::new(&store);
|
||||
atoms.add_to_linker(&mut linker).unwrap();
|
||||
fn run_sync_func(linker: &wasmtime::Linker) {
|
||||
let shim_mod = shim_module(linker.store());
|
||||
let shim_inst = run(linker.instantiate_async(&shim_mod)).unwrap();
|
||||
|
||||
let results = run(shim_inst
|
||||
@@ -68,16 +60,8 @@ fn test_sync_host_func() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_async_host_func() {
|
||||
let store = async_store();
|
||||
|
||||
let ctx = Rc::new(RefCell::new(Ctx));
|
||||
let atoms = Atoms::new(&store, ctx.clone());
|
||||
|
||||
let shim_mod = shim_module(&store);
|
||||
let mut linker = wasmtime::Linker::new(&store);
|
||||
atoms.add_to_linker(&mut linker).unwrap();
|
||||
fn run_async_func(linker: &wasmtime::Linker) {
|
||||
let shim_mod = shim_module(linker.store());
|
||||
let shim_inst = run(linker.instantiate_async(&shim_mod)).unwrap();
|
||||
|
||||
let input: i32 = 123;
|
||||
@@ -105,6 +89,62 @@ fn test_async_host_func() {
|
||||
assert_eq!((input * 2) as f32, result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_host_func() {
|
||||
let store = async_store();
|
||||
|
||||
let ctx = Rc::new(RefCell::new(Ctx));
|
||||
let atoms = Atoms::new(&store, ctx.clone());
|
||||
|
||||
let mut linker = wasmtime::Linker::new(&store);
|
||||
atoms.add_to_linker(&mut linker).unwrap();
|
||||
|
||||
run_sync_func(&linker);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_async_host_func() {
|
||||
let store = async_store();
|
||||
|
||||
let ctx = Rc::new(RefCell::new(Ctx));
|
||||
let atoms = Atoms::new(&store, ctx.clone());
|
||||
|
||||
let mut linker = wasmtime::Linker::new(&store);
|
||||
atoms.add_to_linker(&mut linker).unwrap();
|
||||
|
||||
run_async_func(&linker);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_config_host_func() {
|
||||
let mut config = wasmtime::Config::new();
|
||||
config.async_support(true);
|
||||
Atoms::add_to_config(&mut config);
|
||||
|
||||
let engine = wasmtime::Engine::new(&config).unwrap();
|
||||
let store = wasmtime::Store::new(&engine);
|
||||
|
||||
assert!(Atoms::set_context(&store, Ctx).is_ok());
|
||||
|
||||
let linker = wasmtime::Linker::new(&store);
|
||||
run_sync_func(&linker);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_async_config_host_func() {
|
||||
let mut config = wasmtime::Config::new();
|
||||
config.async_support(true);
|
||||
Atoms::add_to_config(&mut config);
|
||||
|
||||
let engine = wasmtime::Engine::new(&config).unwrap();
|
||||
let store = wasmtime::Store::new(&engine);
|
||||
|
||||
assert!(Atoms::set_context(&store, Ctx).is_ok());
|
||||
|
||||
let linker = wasmtime::Linker::new(&store);
|
||||
run_async_func(&linker);
|
||||
}
|
||||
|
||||
fn run<F: Future>(future: F) -> F::Output {
|
||||
let mut f = Pin::from(Box::new(future));
|
||||
let waker = dummy_waker();
|
||||
@@ -138,9 +178,11 @@ fn dummy_waker() -> Waker {
|
||||
assert_eq!(ptr as usize, 5);
|
||||
}
|
||||
}
|
||||
|
||||
fn async_store() -> wasmtime::Store {
|
||||
let engine = wasmtime::Engine::default();
|
||||
wasmtime::Store::new_async(&engine)
|
||||
wasmtime::Store::new(
|
||||
&wasmtime::Engine::new(wasmtime::Config::new().async_support(true)).unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
// Wiggle expects the caller to have an exported memory. Wasmtime can only
|
||||
|
||||
Reference in New Issue
Block a user