Skip to content

Commit

Permalink
Implement defining host functions at the Config level.
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
peterhuene committed Mar 9, 2021
1 parent f8cc824 commit 34cbd8b
Show file tree
Hide file tree
Showing 22 changed files with 1,617 additions and 456 deletions.
6 changes: 3 additions & 3 deletions crates/c-api/src/func.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ pub struct wasm_func_t {
wasmtime_c_api_macros::declare_ref!(wasm_func_t);

#[repr(C)]
pub struct wasmtime_caller_t<'a> {
caller: Caller<'a>,
pub struct wasmtime_caller_t {
caller: Caller,
}

pub type wasm_func_callback_t = extern "C" fn(
Expand Down Expand Up @@ -85,7 +85,7 @@ impl From<Func> for wasm_func_t {
fn create_function(
store: &wasm_store_t,
ty: &wasm_functype_t,
func: impl Fn(Caller<'_>, *const wasm_val_vec_t, *mut wasm_val_vec_t) -> Option<Box<wasm_trap_t>>
func: impl Fn(Caller, *const wasm_val_vec_t, *mut wasm_val_vec_t) -> Option<Box<wasm_trap_t>>
+ 'static,
) -> Box<wasm_func_t> {
let store = &store.store;
Expand Down
3 changes: 0 additions & 3 deletions crates/runtime/src/instance/allocator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -581,9 +581,6 @@ unsafe impl InstanceAllocator for OnDemandInstanceAllocator {
&self,
mut req: InstanceAllocationRequest,
) -> Result<InstanceHandle, InstantiationError> {
debug_assert!(!req.externref_activations_table.is_null());
debug_assert!(!req.stack_map_registry.is_null());

let memories = self.create_memories(&req.module)?;
let tables = Self::create_tables(&req.module);

Expand Down
26 changes: 11 additions & 15 deletions crates/runtime/src/traphandlers.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! WebAssembly trap handling, which is built on top of the lower-level
//! signalhandling mechanisms.
use crate::VMContext;
use crate::VMInterrupts;
use backtrace::Backtrace;
use std::any::Any;
use std::cell::Cell;
Expand Down Expand Up @@ -445,19 +445,15 @@ impl Trap {
/// returning them as a `Result`.
///
/// Highly unsafe since `closure` won't have any dtors run.
pub unsafe fn catch_traps<F>(
vmctx: *mut VMContext,
trap_info: &impl TrapInfo,
mut closure: F,
) -> Result<(), Trap>
pub unsafe fn catch_traps<F>(trap_info: &impl TrapInfo, mut closure: F) -> Result<(), Trap>
where
F: FnMut(),
{
// Ensure that we have our sigaltstack installed.
#[cfg(unix)]
setup_unix_sigaltstack()?;

return CallThreadState::new(vmctx, trap_info).with(|cx| {
return CallThreadState::new(trap_info).with(|cx| {
RegisterSetjmp(
cx.jmp_buf.as_ptr(),
call_closure::<F>,
Expand Down Expand Up @@ -493,7 +489,6 @@ pub fn out_of_gas() {
pub struct CallThreadState<'a> {
unwind: Cell<UnwindReason>,
jmp_buf: Cell<*const u8>,
vmctx: *mut VMContext,
handling_trap: Cell<bool>,
trap_info: &'a (dyn TrapInfo + 'a),
}
Expand Down Expand Up @@ -526,6 +521,9 @@ pub unsafe trait TrapInfo {
///
/// This function may return, and it may also `raise_lib_trap`.
fn out_of_gas(&self);

/// Returns the VM interrupts to use for interrupting Wasm code.
fn interrupts(&self) -> &VMInterrupts;
}

enum UnwindReason {
Expand All @@ -537,10 +535,9 @@ enum UnwindReason {
}

impl<'a> CallThreadState<'a> {
fn new(vmctx: *mut VMContext, trap_info: &'a (dyn TrapInfo + 'a)) -> CallThreadState<'a> {
fn new(trap_info: &'a (dyn TrapInfo + 'a)) -> CallThreadState<'a> {
CallThreadState {
unwind: Cell::new(UnwindReason::None),
vmctx,
jmp_buf: Cell::new(ptr::null()),
handling_trap: Cell::new(false),
trap_info,
Expand All @@ -562,10 +559,9 @@ impl<'a> CallThreadState<'a> {
UnwindReason::LibTrap(trap) => Err(trap),
UnwindReason::JitTrap { backtrace, pc } => {
debug_assert_eq!(ret, 0);
let maybe_interrupted = unsafe {
let interrupts = (*self.vmctx).instance().interrupts();
(**interrupts).stack_limit.load(SeqCst) == wasmtime_environ::INTERRUPTED
};
let interrupts = self.trap_info.interrupts();
let maybe_interrupted =
interrupts.stack_limit.load(SeqCst) == wasmtime_environ::INTERRUPTED;
Err(Trap::Jit {
pc,
backtrace,
Expand Down Expand Up @@ -622,7 +618,7 @@ impl<'a> CallThreadState<'a> {
// (a million bytes) the slop shouldn't matter too much.
let wasm_stack_limit = psm::stack_pointer() as usize - self.trap_info.max_wasm_stack();

let interrupts = unsafe { &**(&*self.vmctx).instance().interrupts() };
let interrupts = self.trap_info.interrupts();
let reset_stack_limit = match interrupts.stack_limit.compare_exchange(
usize::max_value(),
wasm_stack_limit,
Expand Down
11 changes: 10 additions & 1 deletion crates/wasi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
use std::cell::RefCell;
use std::rc::Rc;
pub use wasi_common::{Error, WasiCtx, WasiCtxBuilder, WasiDir, WasiFile};
use wasmtime::{Linker, Store};
use wasmtime::{Config, Linker, Store};

/// An instantiated instance of all available wasi exports. Presently includes
/// both the "preview1" snapshot and the "unstable" (preview0) snapshot.
Expand All @@ -34,6 +34,15 @@ impl Wasi {
self.preview_0.add_to_linker(linker)?;
Ok(())
}
pub fn add_to_config(config: &mut Config) {
snapshots::preview_1::Wasi::add_to_config(config);
snapshots::preview_0::Wasi::add_to_config(config);
}
pub fn set_context(store: &Store, context: WasiCtx) -> Result<(), WasiCtx> {
// It doesn't matter which underlying `Wasi` type this gets called on as the
// implementations are identical
snapshots::preview_1::Wasi::set_context(store, context)
}
}

pub mod snapshots {
Expand Down
141 changes: 141 additions & 0 deletions crates/wasmtime/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
use crate::memory::MemoryCreator;
use crate::trampoline::MemoryCreatorProxy;
use crate::{func::HostFunc, Caller, FuncType, IntoFunc, Trap, Val, WasmRet, WasmTy};
use anyhow::{bail, Result};
use std::cmp;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::fmt;
use std::future::Future;
#[cfg(feature = "cache")]
use std::path::Path;
use std::pin::Pin;
use std::sync::Arc;
use wasmparser::WasmFeatures;
#[cfg(feature = "cache")]
Expand Down Expand Up @@ -266,6 +270,88 @@ impl Default for InstanceAllocationStrategy {
}
}

/// This type is used for storing host functions in a `Config`.
///
/// The module and function names are interned for more compact storage.
#[derive(Clone)]
struct HostFuncMap {
index_map: HashMap<Arc<str>, usize>,
strings: Vec<Arc<str>>,
funcs: HashMap<(usize, usize), Arc<HostFunc>>,
}

impl HostFuncMap {
fn new() -> Self {
Self {
index_map: HashMap::new(),
strings: Vec::new(),
funcs: HashMap::new(),
}
}

fn insert(&mut self, module: &str, name: &str, func: HostFunc) {
let key = (self.intern_str(module), self.intern_str(name));
self.funcs.insert(key, Arc::new(func));
}

fn get(&self, module: &str, name: &str) -> Option<&HostFunc> {
let key = (
self.index_map.get(module).cloned()?,
self.index_map.get(name).cloned()?,
);
self.funcs.get(&key).map(AsRef::as_ref)
}

fn intern_str(&mut self, string: &str) -> usize {
if let Some(idx) = self.index_map.get(string) {
return *idx;
}
let string: Arc<str> = string.into();
let idx = self.strings.len();
self.strings.push(string.clone());
self.index_map.insert(string, idx);
idx
}
}

macro_rules! generate_wrap_async_host_func {
($num:tt $($args:ident)*) => (paste::paste!{
/// Same as [`Config::wrap_host_func`], except the closure asynchronously produces
/// its result. For more information see the [`Func`](crate::Func) documentation.
///
/// # Panics
///
/// A panic will occur if the store associated with the instance that calls this host
/// function is not asynchronous (see [`Store::new_async`](crate::Store::new_async)).
#[allow(non_snake_case)]
#[cfg(feature = "async")]
#[cfg_attr(nightlydoc, doc(cfg(feature = "async")))]
pub fn [<wrap_host_func $num _async>]<$($args,)* R>(
&mut self,
module: &str,
name: &str,
func: impl Fn(Caller, $($args),*) -> Box<dyn Future<Output = R>> + Send + Sync + 'static,
)
where
$($args: WasmTy,)*
R: WasmRet,
{
self.host_funcs.insert(
module,
name,
HostFunc::wrap(&self.default_instance_allocator, move |caller: Caller, $($args: $args),*| {
let store = caller.store().clone();
let mut future = Pin::from(func(caller, $($args),*));
match store.block_on(future.as_mut()) {
Ok(ret) => ret.into_result(),
Err(e) => Err(e),
}
})
);
}
})
}

/// Global configuration options used to create an [`Engine`](crate::Engine)
/// and customize its behavior.
///
Expand All @@ -292,6 +378,7 @@ pub struct Config {
pub(crate) max_memories: usize,
#[cfg(feature = "async")]
pub(crate) async_stack_size: usize,
host_funcs: HostFuncMap,
}

impl Config {
Expand Down Expand Up @@ -344,6 +431,7 @@ impl Config {
max_memories: 10_000,
#[cfg(feature = "async")]
async_stack_size: 2 << 20,
host_funcs: HostFuncMap::new(),
};
ret.wasm_backtrace_details(WasmBacktraceDetails::Environment);
return ret;
Expand Down Expand Up @@ -1062,6 +1150,59 @@ impl Config {
self
}

/// Defines a host function for the [`Config`] for the given callback.
///
/// Use [`Store::get_host_func`](crate::Store::get_host_func) to get a [`Func`](crate::Func) representing the function.
///
/// Note that the implementation of `func` must adhere to the `ty`
/// signature given, error or traps may occur if it does not respect the
/// `ty` signature.
///
/// Additionally note that this is quite a dynamic function since signatures
/// are not statically known. For performance reasons, it's recommended
/// to use [`Config::wrap_host_func`] if you can because with statically known
/// signatures the engine can optimize the implementation much more.
///
/// The callback must be `Send` and `Sync` as it is shared between all engines created
/// from the `Config`. For more relaxed bounds, use [`Func::new`](crate::Func::new) to define the function.
pub fn define_host_func(
&mut self,
module: &str,
name: &str,
ty: FuncType,
func: impl Fn(Caller, &[Val], &mut [Val]) -> Result<(), Trap> + Send + Sync + 'static,
) {
self.host_funcs
.insert(module, name, HostFunc::new(self, ty, func));
}

/// Defines a host function for the [`Config`] from the given Rust closure.
///
/// Use [`Store::get_host_func`](crate::Store::get_host_func) to get a [`Func`](crate::Func) representing the function.
///
/// See [`Func::wrap`](crate::Func::wrap) for information about accepted parameter and result types for the closure.
///
/// The closure must be `Send` and `Sync` as it is shared between all engines created
/// from the `Config`. For more relaxed bounds, use [`Func::wrap`](crate::Func::wrap) to wrap the closure.
pub fn wrap_host_func<Params, Results>(
&mut self,
module: &str,
name: &str,
func: impl IntoFunc<Params, Results> + Send + Sync,
) {
self.host_funcs.insert(
module,
name,
HostFunc::wrap(&self.default_instance_allocator, func),
);
}

for_each_function_signature!(generate_wrap_async_host_func);

pub(crate) fn get_host_func(&self, module: &str, name: &str) -> Option<&HostFunc> {
self.host_funcs.get(module, name)
}

pub(crate) fn target_isa(&self) -> Box<dyn TargetIsa> {
self.isa_flags
.clone()
Expand Down
Loading

0 comments on commit 34cbd8b

Please sign in to comment.