* 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.
496 lines
14 KiB
Rust
496 lines
14 KiB
Rust
use std::cell::Cell;
|
|
use std::future::Future;
|
|
use std::pin::Pin;
|
|
use std::rc::Rc;
|
|
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
|
|
use wasmtime::*;
|
|
|
|
fn async_store() -> Store {
|
|
Store::new(&Engine::new(Config::new().async_support(true)).unwrap())
|
|
}
|
|
|
|
fn run_smoke_test(func: &Func) {
|
|
run(func.call_async(&[])).unwrap();
|
|
run(func.call_async(&[])).unwrap();
|
|
let future1 = func.call_async(&[]);
|
|
let future2 = func.call_async(&[]);
|
|
run(future2).unwrap();
|
|
run(future1).unwrap();
|
|
}
|
|
|
|
fn run_smoke_get0_test(func: &Func) {
|
|
let func = func.get0_async::<()>().unwrap();
|
|
run(func()).unwrap();
|
|
run(func()).unwrap();
|
|
let future1 = func();
|
|
let future2 = func();
|
|
run(future2).unwrap();
|
|
run(future1).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn smoke() {
|
|
let store = async_store();
|
|
let func = Func::new_async(
|
|
&store,
|
|
FuncType::new(None, None),
|
|
(),
|
|
move |_caller, _state, _params, _results| Box::new(async { Ok(()) }),
|
|
);
|
|
run_smoke_test(&func);
|
|
run_smoke_get0_test(&func);
|
|
|
|
let func = Func::wrap0_async(&store, (), move |_caller: Caller<'_>, _state| {
|
|
Box::new(async { Ok(()) })
|
|
});
|
|
run_smoke_test(&func);
|
|
run_smoke_get0_test(&func);
|
|
}
|
|
|
|
#[test]
|
|
fn smoke_host_func() {
|
|
let mut config = Config::new();
|
|
config.async_support(true);
|
|
config.define_host_func_async(
|
|
"",
|
|
"first",
|
|
FuncType::new(None, None),
|
|
move |_caller, _params, _results| Box::new(async { Ok(()) }),
|
|
);
|
|
config.wrap0_host_func_async("", "second", move |_caller: Caller<'_>| {
|
|
Box::new(async { Ok(()) })
|
|
});
|
|
|
|
let store = Store::new(&Engine::new(&config).unwrap());
|
|
|
|
let func = store
|
|
.get_host_func("", "first")
|
|
.expect("expected host function");
|
|
run_smoke_test(&func);
|
|
run_smoke_get0_test(&func);
|
|
|
|
let func = store
|
|
.get_host_func("", "second")
|
|
.expect("expected host function");
|
|
run_smoke_test(&func);
|
|
run_smoke_get0_test(&func);
|
|
}
|
|
|
|
#[test]
|
|
fn smoke_with_suspension() {
|
|
let store = async_store();
|
|
let func = Func::new_async(
|
|
&store,
|
|
FuncType::new(None, None),
|
|
(),
|
|
move |_caller, _state, _params, _results| {
|
|
Box::new(async {
|
|
PendingOnce::default().await;
|
|
Ok(())
|
|
})
|
|
},
|
|
);
|
|
run_smoke_test(&func);
|
|
run_smoke_get0_test(&func);
|
|
|
|
let func = Func::wrap0_async(&store, (), move |_caller: Caller<'_>, _state| {
|
|
Box::new(async {
|
|
PendingOnce::default().await;
|
|
Ok(())
|
|
})
|
|
});
|
|
run_smoke_test(&func);
|
|
run_smoke_get0_test(&func);
|
|
}
|
|
|
|
#[test]
|
|
fn smoke_host_func_with_suspension() {
|
|
let mut config = Config::new();
|
|
config.async_support(true);
|
|
config.define_host_func_async(
|
|
"",
|
|
"first",
|
|
FuncType::new(None, None),
|
|
move |_caller, _params, _results| {
|
|
Box::new(async {
|
|
PendingOnce::default().await;
|
|
Ok(())
|
|
})
|
|
},
|
|
);
|
|
config.wrap0_host_func_async("", "second", move |_caller: Caller<'_>| {
|
|
Box::new(async {
|
|
PendingOnce::default().await;
|
|
Ok(())
|
|
})
|
|
});
|
|
|
|
let store = Store::new(&Engine::new(&config).unwrap());
|
|
|
|
let func = store
|
|
.get_host_func("", "first")
|
|
.expect("expected host function");
|
|
run_smoke_test(&func);
|
|
run_smoke_get0_test(&func);
|
|
|
|
let func = store
|
|
.get_host_func("", "second")
|
|
.expect("expected host function");
|
|
run_smoke_test(&func);
|
|
run_smoke_get0_test(&func);
|
|
}
|
|
|
|
#[test]
|
|
fn recursive_call() {
|
|
let store = async_store();
|
|
let async_wasm_func = Rc::new(Func::new_async(
|
|
&store,
|
|
FuncType::new(None, None),
|
|
(),
|
|
|_caller, _state, _params, _results| {
|
|
Box::new(async {
|
|
PendingOnce::default().await;
|
|
Ok(())
|
|
})
|
|
},
|
|
));
|
|
let weak = Rc::downgrade(&async_wasm_func);
|
|
|
|
// Create an imported function which recursively invokes another wasm
|
|
// function asynchronously, although this one is just our own host function
|
|
// which suffices for this test.
|
|
let func2 = Func::new_async(
|
|
&store,
|
|
FuncType::new(None, None),
|
|
(),
|
|
move |_caller, _state, _params, _results| {
|
|
let async_wasm_func = weak.upgrade().unwrap();
|
|
Box::new(async move {
|
|
async_wasm_func.call_async(&[]).await?;
|
|
Ok(())
|
|
})
|
|
},
|
|
);
|
|
|
|
// Create an instance which calls an async import twice.
|
|
let module = Module::new(
|
|
store.engine(),
|
|
"
|
|
(module
|
|
(import \"\" \"\" (func))
|
|
(func (export \"\")
|
|
;; call imported function which recursively does an async
|
|
;; call
|
|
call 0
|
|
;; do it again, and our various pointers all better align
|
|
call 0))
|
|
",
|
|
)
|
|
.unwrap();
|
|
|
|
run(async {
|
|
let instance = Instance::new_async(&store, &module, &[func2.into()]).await?;
|
|
let func = instance.get_func("").unwrap();
|
|
func.call_async(&[]).await
|
|
})
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn suspend_while_suspending() {
|
|
let store = async_store();
|
|
|
|
// Create a synchronous function which calls our asynchronous function and
|
|
// runs it locally. This shouldn't generally happen but we know everything
|
|
// is synchronous in this test so it's fine for us to do this.
|
|
//
|
|
// The purpose of this test is intended to stress various cases in how
|
|
// we manage pointers in ways that are not necessarily common but are still
|
|
// possible in safe code.
|
|
let async_thunk = Rc::new(Func::new_async(
|
|
&store,
|
|
FuncType::new(None, None),
|
|
(),
|
|
|_caller, _state, _params, _results| Box::new(async { Ok(()) }),
|
|
));
|
|
let weak = Rc::downgrade(&async_thunk);
|
|
let sync_call_async_thunk = Func::new(
|
|
&store,
|
|
FuncType::new(None, None),
|
|
move |_caller, _params, _results| {
|
|
let async_thunk = weak.upgrade().unwrap();
|
|
run(async_thunk.call_async(&[]))?;
|
|
Ok(())
|
|
},
|
|
);
|
|
|
|
// A small async function that simply awaits once to pump the loops and
|
|
// then finishes.
|
|
let async_import = Func::new_async(
|
|
&store,
|
|
FuncType::new(None, None),
|
|
(),
|
|
move |_caller, _state, _params, _results| {
|
|
Box::new(async move {
|
|
PendingOnce::default().await;
|
|
Ok(())
|
|
})
|
|
},
|
|
);
|
|
|
|
let module = Module::new(
|
|
store.engine(),
|
|
"
|
|
(module
|
|
(import \"\" \"\" (func $sync_call_async_thunk))
|
|
(import \"\" \"\" (func $async_import))
|
|
(func (export \"\")
|
|
;; Set some store-local state and pointers
|
|
call $sync_call_async_thunk
|
|
;; .. and hopefully it's all still configured correctly
|
|
call $async_import))
|
|
",
|
|
)
|
|
.unwrap();
|
|
run(async {
|
|
let instance = Instance::new_async(
|
|
&store,
|
|
&module,
|
|
&[sync_call_async_thunk.into(), async_import.into()],
|
|
)
|
|
.await?;
|
|
let func = instance.get_func("").unwrap();
|
|
func.call_async(&[]).await
|
|
})
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn cancel_during_run() {
|
|
let store = async_store();
|
|
let state = Rc::new(Cell::new(0));
|
|
let state2 = state.clone();
|
|
|
|
let async_thunk = Func::new_async(
|
|
&store,
|
|
FuncType::new(None, None),
|
|
(),
|
|
move |_caller, _state, _params, _results| {
|
|
assert_eq!(state2.get(), 0);
|
|
state2.set(1);
|
|
let dtor = SetOnDrop(state2.clone());
|
|
Box::new(async move {
|
|
drop(&dtor);
|
|
PendingOnce::default().await;
|
|
Ok(())
|
|
})
|
|
},
|
|
);
|
|
// Shouldn't have called anything yet...
|
|
assert_eq!(state.get(), 0);
|
|
|
|
// Create our future, but as per async conventions this still doesn't
|
|
// actually do anything. No wasm or host function has been called yet.
|
|
let mut future = Pin::from(Box::new(async_thunk.call_async(&[])));
|
|
assert_eq!(state.get(), 0);
|
|
|
|
// Push the future forward one tick, which actually runs the host code in
|
|
// our async func. Our future is designed to be pending once, however.
|
|
let poll = future
|
|
.as_mut()
|
|
.poll(&mut Context::from_waker(&dummy_waker()));
|
|
assert!(poll.is_pending());
|
|
assert_eq!(state.get(), 1);
|
|
|
|
// Now that our future is running (on a separate, now-suspended fiber), drop
|
|
// the future and that should deallocate all the Rust bits as well.
|
|
drop(future);
|
|
assert_eq!(state.get(), 2);
|
|
|
|
struct SetOnDrop(Rc<Cell<u32>>);
|
|
|
|
impl Drop for SetOnDrop {
|
|
fn drop(&mut self) {
|
|
assert_eq!(self.0.get(), 1);
|
|
self.0.set(2);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct PendingOnce {
|
|
already_polled: bool,
|
|
}
|
|
|
|
impl Future for PendingOnce {
|
|
type Output = ();
|
|
|
|
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
|
|
if self.already_polled {
|
|
Poll::Ready(())
|
|
} else {
|
|
self.already_polled = true;
|
|
cx.waker().wake_by_ref();
|
|
Poll::Pending
|
|
}
|
|
}
|
|
}
|
|
|
|
fn run<F: Future>(future: F) -> F::Output {
|
|
let mut f = Pin::from(Box::new(future));
|
|
let waker = dummy_waker();
|
|
let mut cx = Context::from_waker(&waker);
|
|
loop {
|
|
match f.as_mut().poll(&mut cx) {
|
|
Poll::Ready(val) => break val,
|
|
Poll::Pending => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn dummy_waker() -> Waker {
|
|
return unsafe { Waker::from_raw(clone(5 as *const _)) };
|
|
|
|
unsafe fn clone(ptr: *const ()) -> RawWaker {
|
|
assert_eq!(ptr as usize, 5);
|
|
const VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);
|
|
RawWaker::new(ptr, &VTABLE)
|
|
}
|
|
|
|
unsafe fn wake(ptr: *const ()) {
|
|
assert_eq!(ptr as usize, 5);
|
|
}
|
|
|
|
unsafe fn wake_by_ref(ptr: *const ()) {
|
|
assert_eq!(ptr as usize, 5);
|
|
}
|
|
|
|
unsafe fn drop(ptr: *const ()) {
|
|
assert_eq!(ptr as usize, 5);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn iloop_with_fuel() {
|
|
let engine = Engine::new(Config::new().async_support(true).consume_fuel(true)).unwrap();
|
|
let store = Store::new(&engine);
|
|
store.out_of_fuel_async_yield(1_000, 10);
|
|
let module = Module::new(
|
|
&engine,
|
|
"
|
|
(module
|
|
(func (loop br 0))
|
|
(start 0)
|
|
)
|
|
",
|
|
)
|
|
.unwrap();
|
|
let instance = Instance::new_async(&store, &module, &[]);
|
|
let mut f = Pin::from(Box::new(instance));
|
|
let waker = dummy_waker();
|
|
let mut cx = Context::from_waker(&waker);
|
|
|
|
// This should yield a bunch of times...
|
|
for _ in 0..100 {
|
|
assert!(f.as_mut().poll(&mut cx).is_pending());
|
|
}
|
|
|
|
// ... but it should eventually also finish.
|
|
loop {
|
|
match f.as_mut().poll(&mut cx) {
|
|
Poll::Ready(_) => break,
|
|
Poll::Pending => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn fuel_eventually_finishes() {
|
|
let engine = Engine::new(Config::new().async_support(true).consume_fuel(true)).unwrap();
|
|
let store = Store::new(&engine);
|
|
store.out_of_fuel_async_yield(u32::max_value(), 10);
|
|
let module = Module::new(
|
|
&engine,
|
|
"
|
|
(module
|
|
(func
|
|
(local i32)
|
|
i32.const 100
|
|
local.set 0
|
|
(loop
|
|
local.get 0
|
|
i32.const -1
|
|
i32.add
|
|
local.tee 0
|
|
br_if 0)
|
|
)
|
|
(start 0)
|
|
)
|
|
",
|
|
)
|
|
.unwrap();
|
|
let instance = Instance::new_async(&store, &module, &[]);
|
|
run(instance).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn async_with_pooling_stacks() {
|
|
let mut config = Config::new();
|
|
config.async_support(true);
|
|
config.allocation_strategy(InstanceAllocationStrategy::Pooling {
|
|
strategy: PoolingAllocationStrategy::NextAvailable,
|
|
module_limits: ModuleLimits {
|
|
memory_pages: 1,
|
|
table_elements: 0,
|
|
..Default::default()
|
|
},
|
|
instance_limits: InstanceLimits {
|
|
count: 1,
|
|
memory_reservation_size: 1,
|
|
},
|
|
});
|
|
|
|
let engine = Engine::new(&config).unwrap();
|
|
let store = Store::new(&engine);
|
|
let func = Func::new_async(
|
|
&store,
|
|
FuncType::new(None, None),
|
|
(),
|
|
move |_caller, _state, _params, _results| Box::new(async { Ok(()) }),
|
|
);
|
|
|
|
run_smoke_test(&func);
|
|
run_smoke_get0_test(&func);
|
|
}
|
|
|
|
#[test]
|
|
fn async_host_func_with_pooling_stacks() {
|
|
let mut config = Config::new();
|
|
config.async_support(true);
|
|
config.allocation_strategy(InstanceAllocationStrategy::Pooling {
|
|
strategy: PoolingAllocationStrategy::NextAvailable,
|
|
module_limits: ModuleLimits {
|
|
memory_pages: 1,
|
|
table_elements: 0,
|
|
..Default::default()
|
|
},
|
|
instance_limits: InstanceLimits {
|
|
count: 1,
|
|
memory_reservation_size: 1,
|
|
},
|
|
});
|
|
|
|
config.define_host_func_async(
|
|
"",
|
|
"",
|
|
FuncType::new(None, None),
|
|
move |_caller, _params, _results| Box::new(async { Ok(()) }),
|
|
);
|
|
|
|
let store = Store::new(&Engine::new(&config).unwrap());
|
|
let func = store.get_host_func("", "").expect("expected host function");
|
|
|
|
run_smoke_test(&func);
|
|
run_smoke_get0_test(&func);
|
|
}
|