Browse Source

Remove type information from dynamic component funcs (#8070)

* Remove type information from dynamic component funcs

This commit removes the `&Component` argument from the
`component::Linker::func_new` API. This is inspired by #8062 where `Val`
holds less type information as well in addition to the realization that
type-checking happens at runtime rather than instantiation time.

This argument was originally added to mirror
`wasmtime::Linker::func_new` which takes a type argument of the core
wasm function that's being defined. Unlike core wasm, though, component
functions already have to carry along their type information as part of
function calls to handle resources correctly. This means that when a
host function is invoked the type is already known of all the parameters
and results. Additionally values are already required to be type-checked
going back into wasm, so there's less of a need to perform an additional
type-check up front.

The main consequence of this commit is that it's a bit more difficult
for embeddings to know what the expected types of results are. No type
information is provided when a host function is defined, not even
function arity. This means that when the host function is invoked it may
not know how many results are expected to be produced and of what type.
Typically though a bindings generator is used somewhere along the way so
that's expected to alleviate this issue.

Finally my hope is to enhance this "dynamic" API in the future with a
bit more information so the type information is more readily accessible
at runtime. For now though hosts will have to "simply know what to do".

* Update crates/wasmtime/src/runtime/component/linker.rs

Co-authored-by: Joel Dice <joel.dice@fermyon.com>

* Fix doc links

* Fix component call benchmarks

---------

Co-authored-by: Joel Dice <joel.dice@fermyon.com>
pull/8072/head
Alex Crichton 8 months ago
committed by GitHub
parent
commit
6a710b92d8
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 37
      benches/call.rs
  2. 2
      crates/fuzzing/src/oracles.rs
  3. 27
      crates/wasmtime/src/runtime/component/func/host.rs
  4. 154
      crates/wasmtime/src/runtime/component/linker.rs
  5. 91
      tests/all/component_model/import.rs

37
benches/call.rs

@ -798,30 +798,23 @@ mod component {
bench_instance(group, store, &instance, "typed", is_async);
let mut untyped = component::Linker::new(&engine);
untyped.root().func_new("nop", |_, _, _| Ok(())).unwrap();
untyped
.root()
.func_new(&component, "nop", |_, _, _| Ok(()))
.unwrap();
untyped
.root()
.func_new(
&component,
"nop-params-and-results",
|_caller, params, results| {
assert_eq!(params.len(), 2);
match params[0] {
component::Val::U32(0) => {}
_ => unreachable!(),
}
match params[1] {
component::Val::U64(0) => {}
_ => unreachable!(),
}
assert_eq!(results.len(), 1);
results[0] = component::Val::Float32(0.0);
Ok(())
},
)
.func_new("nop-params-and-results", |_caller, params, results| {
assert_eq!(params.len(), 2);
match params[0] {
component::Val::U32(0) => {}
_ => unreachable!(),
}
match params[1] {
component::Val::U64(0) => {}
_ => unreachable!(),
}
assert_eq!(results.len(), 1);
results[0] = component::Val::Float32(0.0);
Ok(())
})
.unwrap();
let instance = if is_async.use_async() {
run_await(untyped.instantiate_async(&mut *store, &component)).unwrap()

2
crates/fuzzing/src/oracles.rs

@ -883,7 +883,7 @@ pub fn dynamic_component_api_target(input: &mut arbitrary::Unstructured) -> arbi
linker
.root()
.func_new(&component, IMPORT_FUNCTION, {
.func_new(IMPORT_FUNCTION, {
move |mut cx: StoreContextMut<'_, (Vec<Val>, Option<Vec<Val>>)>,
params: &[Val],
results: &mut [Val]|

27
crates/wasmtime/src/runtime/component/func/host.rs

@ -3,15 +3,15 @@ use crate::component::matching::InstanceType;
use crate::component::storage::slice_to_storage_mut;
use crate::component::{ComponentNamedList, ComponentType, Lift, Lower, Val};
use crate::{AsContextMut, StoreContextMut, ValRaw};
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{bail, Context, Result};
use std::any::Any;
use std::mem::{self, MaybeUninit};
use std::panic::{self, AssertUnwindSafe};
use std::ptr::NonNull;
use std::sync::Arc;
use wasmtime_environ::component::{
CanonicalAbiInfo, ComponentTypes, InterfaceType, StringEncoding, TypeFuncIndex,
MAX_FLAT_PARAMS, MAX_FLAT_RESULTS,
CanonicalAbiInfo, InterfaceType, StringEncoding, TypeFuncIndex, MAX_FLAT_PARAMS,
MAX_FLAT_RESULTS,
};
use wasmtime_runtime::component::{
InstanceFlags, VMComponentContext, VMLowering, VMLoweringCallee,
@ -71,27 +71,16 @@ impl HostFunc {
}
}
pub(crate) fn new_dynamic<T, F>(
func: F,
index: TypeFuncIndex,
types: &Arc<ComponentTypes>,
) -> Arc<HostFunc>
pub(crate) fn new_dynamic<T, F>(func: F) -> Arc<HostFunc>
where
F: Fn(StoreContextMut<'_, T>, &[Val], &mut [Val]) -> Result<()> + Send + Sync + 'static,
{
Arc::new(HostFunc {
entrypoint: dynamic_entrypoint::<T, F>,
typecheck: Box::new({
let types = types.clone();
move |expected_index, expected_types| {
if index == expected_index && std::ptr::eq(&*types, &**expected_types.types) {
Ok(())
} else {
Err(anyhow!("function type mismatch"))
}
}
}),
// This function performs dynamic type checks and subsequently does
// not need to perform up-front type checks. Instead everything is
// dynamically managed at runtime.
typecheck: Box::new(move |_expected_index, _expected_types| Ok(())),
func: Box::new(func),
})
}

154
crates/wasmtime/src/runtime/component/linker.rs

@ -6,16 +6,13 @@ use crate::component::{
Component, ComponentNamedList, Instance, InstancePre, Lift, Lower, ResourceType, Val,
};
use crate::{AsContextMut, Engine, Module, StoreContextMut};
use anyhow::{anyhow, bail, Context, Result};
use indexmap::IndexMap;
use anyhow::{bail, Context, Result};
use semver::Version;
use std::collections::hash_map::{Entry, HashMap};
use std::future::Future;
use std::marker;
use std::ops::Deref;
use std::pin::Pin;
use std::sync::Arc;
use wasmtime_environ::component::TypeDef;
use wasmtime_environ::PrimaryMap;
/// A type used to instantiate [`Component`]s.
@ -413,53 +410,112 @@ impl<T> LinkerInstance<'_, T> {
self.func_wrap(name, ff)
}
/// Define a new host-provided function using dynamic types.
/// Define a new host-provided function using dynamically typed values.
///
/// `name` must refer to a function type import in `component`. If and when
/// that import is invoked by the component, the specified `func` will be
/// called, which must return a `Val` which is an instance of the result
/// type of the import.
pub fn func_new<
F: Fn(StoreContextMut<'_, T>, &[Val], &mut [Val]) -> Result<()> + Send + Sync + 'static,
>(
/// The `name` provided is the name of the function to define and the
/// `func` provided is the host-defined closure to invoke when this
/// function is called.
///
/// This function is the "dynamic" version of defining a host function as
/// compared to [`LinkerInstance::func_wrap`]. With
/// [`LinkerInstance::func_wrap`] a function's type is statically known but
/// with this method the `func` argument's type isn't known ahead of time.
/// That means that `func` can be by imported component so long as it's
/// imported as a matching name.
///
/// Type information will be available at execution time, however. For
/// example when `func` is invoked the second argument, a `&[Val]` list,
/// contains [`Val`] entries that say what type they are. Additionally the
/// third argument, `&mut [Val]`, is the expected number of results. Note
/// that the expected types of the results cannot be learned during the
/// execution of `func`. Learning that would require runtime introspection
/// of a component.
///
/// Return values, stored in the third argument of `&mut [Val]`, are
/// type-checked at runtime to ensure that they have the appropriate type.
/// A trap will be raised if they do not have the right type.
///
/// # Examples
///
/// ```
/// use wasmtime::{Store, Engine};
/// use wasmtime::component::{Component, Linker, Val};
///
/// # fn main() -> wasmtime::Result<()> {
/// let engine = Engine::default();
/// let component = Component::new(
/// &engine,
/// r#"
/// (component
/// (import "thunk" (func $thunk))
/// (import "is-even" (func $is-even (param "x" u32) (result bool)))
///
/// (core module $m
/// (import "" "thunk" (func $thunk))
/// (import "" "is-even" (func $is-even (param i32) (result i32)))
///
/// (func (export "run")
/// call $thunk
///
/// (call $is-even (i32.const 1))
/// if unreachable end
///
/// (call $is-even (i32.const 2))
/// i32.eqz
/// if unreachable end
/// )
/// )
/// (core func $thunk (canon lower (func $thunk)))
/// (core func $is-even (canon lower (func $is-even)))
/// (core instance $i (instantiate $m
/// (with "" (instance
/// (export "thunk" (func $thunk))
/// (export "is-even" (func $is-even))
/// ))
/// ))
///
/// (func (export "run") (canon lift (core func $i "run")))
/// )
/// "#,
/// )?;
///
/// let mut linker = Linker::<()>::new(&engine);
///
/// // Sample function that takes no arguments.
/// linker.root().func_new("thunk", |_store, params, results| {
/// assert!(params.is_empty());
/// assert!(results.is_empty());
/// println!("Look ma, host hands!");
/// Ok(())
/// })?;
///
/// // This function takes one argument and returns one result.
/// linker.root().func_new("is-even", |_store, params, results| {
/// assert_eq!(params.len(), 1);
/// let param = match params[0] {
/// Val::U32(n) => n,
/// _ => panic!("unexpected type"),
/// };
///
/// assert_eq!(results.len(), 1);
/// results[0] = Val::Bool(param % 2 == 0);
/// Ok(())
/// })?;
///
/// let mut store = Store::new(&engine, ());
/// let instance = linker.instantiate(&mut store, &component)?;
/// let run = instance.get_typed_func::<(), ()>(&mut store, "run")?;
/// run.call(&mut store, ())?;
/// # Ok(())
/// # }
/// ```
pub fn func_new(
&mut self,
component: &Component,
name: &str,
func: F,
func: impl Fn(StoreContextMut<'_, T>, &[Val], &mut [Val]) -> Result<()> + Send + Sync + 'static,
) -> Result<()> {
let mut map = &component
.env_component()
.import_types
.values()
.map(|(k, v)| (k.clone(), *v))
.collect::<IndexMap<_, _>>();
for name in self.path.iter().copied().take(self.path_len) {
let name = self.strings.strings[name].deref();
if let Some(ty) = map.get(name) {
if let TypeDef::ComponentInstance(index) = ty {
map = &component.types()[*index].exports;
} else {
bail!("import `{name}` has the wrong type (expected a component instance)");
}
} else {
bail!("import `{name}` not found");
}
}
if let Some(ty) = map.get(name) {
if let TypeDef::ComponentFunc(index) = ty {
self.insert(
name,
Definition::Func(HostFunc::new_dynamic(func, *index, component.types())),
)?;
Ok(())
} else {
bail!("import `{name}` has the wrong type (expected a function)");
}
} else {
Err(anyhow!("import `{name}` not found"))
}
self.insert(name, Definition::Func(HostFunc::new_dynamic(func)))?;
Ok(())
}
/// Define a new host-provided async function using dynamic types.
@ -468,7 +524,7 @@ impl<T> LinkerInstance<'_, T> {
/// host function.
#[cfg(feature = "async")]
#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
pub fn func_new_async<F>(&mut self, component: &Component, name: &str, f: F) -> Result<()>
pub fn func_new_async<F>(&mut self, name: &str, f: F) -> Result<()>
where
F: for<'a> Fn(
StoreContextMut<'a, T>,
@ -488,7 +544,7 @@ impl<T> LinkerInstance<'_, T> {
let mut future = Pin::from(f(store.as_context_mut(), params, results));
unsafe { async_cx.block_on(future.as_mut()) }?
};
self.func_new(component, name, ff)
self.func_new(name, ff)
}
/// Defines a [`Module`] within this instance.

91
tests/all/component_model/import.rs

@ -147,7 +147,6 @@ fn simple() -> Result<()> {
*store.data_mut() = None;
let mut linker = Linker::new(&engine);
linker.root().func_new(
&component,
"a",
|mut store: StoreContextMut<'_, Option<String>>, args, _results| {
if let Val::String(s) = &args[0] {
@ -249,7 +248,6 @@ fn functions_in_instances() -> Result<()> {
*store.data_mut() = None;
let mut linker = Linker::new(&engine);
linker.instance("test:test/foo")?.func_new(
&component,
"a",
|mut store: StoreContextMut<'_, Option<String>>, args, _results| {
if let Val::String(s) = &args[0] {
@ -468,7 +466,6 @@ fn attempt_to_reenter_during_host() -> Result<()> {
let mut store = Store::new(&engine, DynamicState { func: None });
let mut linker = Linker::new(&engine);
linker.root().func_new(
&component,
"thunk",
|mut store: StoreContextMut<'_, DynamicState>, _, _| {
let func = store.data_mut().func.take().unwrap();
@ -691,58 +688,50 @@ fn stack_and_heap_args_and_rets() -> Result<()> {
// Next, test the dynamic API
let mut linker = Linker::new(&engine);
linker
.root()
.func_new(&component, "f1", |_, args, results| {
if let Val::U32(x) = &args[0] {
assert_eq!(*x, 1);
results[0] = Val::U32(2);
linker.root().func_new("f1", |_, args, results| {
if let Val::U32(x) = &args[0] {
assert_eq!(*x, 1);
results[0] = Val::U32(2);
Ok(())
} else {
panic!()
}
})?;
linker.root().func_new("f2", |_, args, results| {
if let Val::Tuple(tuple) = &args[0] {
if let Val::String(s) = &tuple[0] {
assert_eq!(s.deref(), "abc");
results[0] = Val::U32(3);
Ok(())
} else {
panic!()
}
})?;
linker
.root()
.func_new(&component, "f2", |_, args, results| {
if let Val::Tuple(tuple) = &args[0] {
if let Val::String(s) = &tuple[0] {
assert_eq!(s.deref(), "abc");
results[0] = Val::U32(3);
Ok(())
} else {
panic!()
}
} else {
panic!()
}
})?;
linker
.root()
.func_new(&component, "f3", |_, args, results| {
if let Val::U32(x) = &args[0] {
assert_eq!(*x, 8);
} else {
panic!()
}
})?;
linker.root().func_new("f3", |_, args, results| {
if let Val::U32(x) = &args[0] {
assert_eq!(*x, 8);
results[0] = Val::String("xyz".into());
Ok(())
} else {
panic!();
}
})?;
linker.root().func_new("f4", |_, args, results| {
if let Val::Tuple(tuple) = &args[0] {
if let Val::String(s) = &tuple[0] {
assert_eq!(s.deref(), "abc");
results[0] = Val::String("xyz".into());
Ok(())
} else {
panic!();
}
})?;
linker
.root()
.func_new(&component, "f4", |_, args, results| {
if let Val::Tuple(tuple) = &args[0] {
if let Val::String(s) = &tuple[0] {
assert_eq!(s.deref(), "abc");
results[0] = Val::String("xyz".into());
Ok(())
} else {
panic!()
}
} else {
panic!()
}
})?;
} else {
panic!()
}
})?;
let instance = linker.instantiate(&mut store, &component)?;
instance
.get_func(&mut store, "run")
@ -906,14 +895,12 @@ fn no_actual_wasm_code() -> Result<()> {
*store.data_mut() = 0;
let mut linker = Linker::new(&engine);
linker.root().func_new(
&component,
"f",
|mut store: StoreContextMut<'_, u32>, _, _| {
linker
.root()
.func_new("f", |mut store: StoreContextMut<'_, u32>, _, _| {
*store.data_mut() += 1;
Ok(())
},
)?;
})?;
let instance = linker.instantiate(&mut store, &component)?;
let thunk = instance.get_func(&mut store, "thunk").unwrap();

Loading…
Cancel
Save