From 6a710b92d843bdb185a7514f541b5888c573cd4f Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Fri, 8 Mar 2024 16:45:56 -0600 Subject: [PATCH] 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 * Fix doc links * Fix component call benchmarks --------- Co-authored-by: Joel Dice --- benches/call.rs | 37 ++--- crates/fuzzing/src/oracles.rs | 2 +- .../src/runtime/component/func/host.rs | 27 +-- .../wasmtime/src/runtime/component/linker.rs | 154 ++++++++++++------ tests/all/component_model/import.rs | 91 +++++------ 5 files changed, 168 insertions(+), 143 deletions(-) diff --git a/benches/call.rs b/benches/call.rs index 601b81bbfe..aef4421b07 100644 --- a/benches/call.rs +++ b/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() diff --git a/crates/fuzzing/src/oracles.rs b/crates/fuzzing/src/oracles.rs index e5543e3ef2..d10f439ded 100644 --- a/crates/fuzzing/src/oracles.rs +++ b/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, Option>)>, params: &[Val], results: &mut [Val]| diff --git a/crates/wasmtime/src/runtime/component/func/host.rs b/crates/wasmtime/src/runtime/component/func/host.rs index 22946854f7..ef352657ba 100644 --- a/crates/wasmtime/src/runtime/component/func/host.rs +++ b/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( - func: F, - index: TypeFuncIndex, - types: &Arc, - ) -> Arc + pub(crate) fn new_dynamic(func: F) -> Arc where F: Fn(StoreContextMut<'_, T>, &[Val], &mut [Val]) -> Result<()> + Send + Sync + 'static, { Arc::new(HostFunc { entrypoint: dynamic_entrypoint::, - 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), }) } diff --git a/crates/wasmtime/src/runtime/component/linker.rs b/crates/wasmtime/src/runtime/component/linker.rs index 4b232cb113..00bcb0a524 100644 --- a/crates/wasmtime/src/runtime/component/linker.rs +++ b/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 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::>(); - - 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 LinkerInstance<'_, T> { /// host function. #[cfg(feature = "async")] #[cfg_attr(docsrs, doc(cfg(feature = "async")))] - pub fn func_new_async(&mut self, component: &Component, name: &str, f: F) -> Result<()> + pub fn func_new_async(&mut self, name: &str, f: F) -> Result<()> where F: for<'a> Fn( StoreContextMut<'a, T>, @@ -488,7 +544,7 @@ impl 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. diff --git a/tests/all/component_model/import.rs b/tests/all/component_model/import.rs index 3b453457f3..105e9b8c7d 100644 --- a/tests/all/component_model/import.rs +++ b/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>, 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>, 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();