diff --git a/Cargo.toml b/Cargo.toml index 87c750ab8e2c..2c71e1ecf735 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -494,6 +494,7 @@ compile = ["cranelift"] run = [ "dep:wasmtime-wasi", "wasmtime/runtime", + "wasmtime/wave", "dep:listenfd", "dep:wasi-common", "dep:tokio", diff --git a/crates/wasmtime/src/runtime/component/component.rs b/crates/wasmtime/src/runtime/component/component.rs index 10af60dc94ec..eb0a9e2309c8 100644 --- a/crates/wasmtime/src/runtime/component/component.rs +++ b/crates/wasmtime/src/runtime/component/component.rs @@ -651,14 +651,13 @@ impl Component { /// skip string lookups at runtime and instead use a more efficient /// index-based lookup. /// - /// This method takes a few arguments: + /// This method takes two arguments: /// - /// * `engine` - the engine that was used to compile this component. /// * `instance` - an optional "parent instance" for the export being looked /// up. If this is `None` then the export is looked up on the root of the /// component itself, and otherwise the export is looked up on the /// `instance` specified. Note that `instance` must have come from a - /// previous invocation of this method. + /// previous invocation of this method, or from `Component::exports`. /// * `name` - the name of the export that's being looked up. /// /// If the export is located then two values are returned: a @@ -731,6 +730,207 @@ impl Component { )) } + /// Iterates over the exports of a component, yielding each exported + /// item's name, type, and export index. + /// + /// Returns `Some(impl Iterator...)` when the `instance` argument points + /// to a valid instance in the component, and `None` otherwise. + /// + /// The argument `instance` is an optional "parent instance" to iterate + /// over the exports of. If this is `None` then the exports iterated over + /// are from the root of the component itself, and otherwise the exports + /// iterated over are from the `instance` specified. Note that `instance` + /// must have come from a previous invocation of this method, or from + /// `Component::export_index`. + /// + /// # Examples + /// + /// ``` + /// use wasmtime::Engine; + /// use wasmtime::component::Component; + /// use wasmtime::component::types::ComponentItem; + /// + /// # fn main() -> wasmtime::Result<()> { + /// let engine = Engine::default(); + /// let component = Component::new( + /// &engine, + /// r#" + /// (component + /// (core module $m + /// (func (export "f")) + /// (func (export "g")) + /// ) + /// (core instance $i (instantiate $m)) + /// (func (export "f") + /// (canon lift (core func $i "f"))) + /// (func (export "g") + /// (canon lift (core func $i "g"))) + /// (component $c + /// (core module $m + /// (func (export "h")) + /// ) + /// (core instance $i (instantiate $m)) + /// (func (export "h") + /// (canon lift (core func $i "h"))) + /// ) + /// (instance (export "i") (instantiate $c)) + /// ) + /// "#, + /// )?; + /// + /// // Get all items exported by the component root: + /// let exports = component + /// .exports(None) + /// .expect("root") + /// .collect::>(); + /// assert_eq!(exports.len(), 3); + /// assert_eq!(exports[0].0, "f"); + /// assert!(matches!(exports[0].1, ComponentItem::ComponentFunc(_))); + /// assert_eq!(exports[1].0, "g"); + /// assert_eq!(exports[2].0, "i"); + /// assert!(matches!(exports[2].1, ComponentItem::ComponentInstance(_))); + /// let i = exports[2].2; + /// let i_exports = component + /// .exports(Some(&i)) + /// .expect("export instance `i` looked up above") + /// .collect::>(); + /// assert_eq!(i_exports.len(), 1); + /// assert_eq!(i_exports[0].0, "h"); + /// assert!(matches!(i_exports[0].1, ComponentItem::ComponentFunc(_))); + /// + /// # Ok(()) + /// # } + /// ``` + /// + pub fn exports<'a>( + &'a self, + instance: Option<&'_ ComponentExportIndex>, + ) -> Option + use<'a>> + { + let info = self.env_component(); + let exports = match instance { + Some(idx) => { + if idx.id != self.inner.id { + return None; + } + match &info.export_items[idx.index] { + Export::Instance { exports, .. } => exports, + _ => return None, + } + } + None => &info.exports, + }; + Some(exports.raw_iter().map(|(name, index)| { + let index = *index; + let ty = match info.export_items[index] { + Export::Instance { ty, .. } => TypeDef::ComponentInstance(ty), + Export::LiftedFunction { ty, .. } => TypeDef::ComponentFunc(ty), + Export::ModuleStatic { ty, .. } | Export::ModuleImport { ty, .. } => { + TypeDef::Module(ty) + } + Export::Type(ty) => ty, + }; + let item = self.with_uninstantiated_instance_type(|instance| { + types::ComponentItem::from(&self.inner.engine, &ty, instance) + }); + let export = ComponentExportIndex { + id: self.inner.id, + index, + }; + (name.as_str(), item, export) + })) + } + + /// Like `exports` but recursive: for each ComponentInstance yielded, so + /// will all of its contents. + /// + /// # Examples + /// + /// ``` + /// use wasmtime::Engine; + /// use wasmtime::component::Component; + /// use wasmtime::component::types::ComponentItem; + /// + /// # fn main() -> wasmtime::Result<()> { + /// let engine = Engine::default(); + /// let component = Component::new( + /// &engine, + /// r#" + /// (component + /// (core module $m + /// (func (export "f")) + /// (func (export "g")) + /// ) + /// (core instance $i (instantiate $m)) + /// (func (export "f") + /// (canon lift (core func $i "f"))) + /// (func (export "g") + /// (canon lift (core func $i "g"))) + /// (component $c + /// (core module $m + /// (func (export "h")) + /// ) + /// (core instance $i (instantiate $m)) + /// (func (export "h") + /// (canon lift (core func $i "h"))) + /// ) + /// (instance (export "i") (instantiate $c)) + /// ) + /// "#, + /// )?; + /// + /// // Get all items exported by the component root: + /// let exports = component + /// .exports_rec(None) + /// .expect("root") + /// .collect::>(); + /// assert_eq!(exports.len(), 4); + /// assert_eq!(exports[0].0, ["f"]); + /// assert!(matches!(exports[0].1, ComponentItem::ComponentFunc(_))); + /// assert_eq!(exports[1].0, ["g"]); + /// assert_eq!(exports[2].0, ["i"]); + /// assert!(matches!(exports[2].1, ComponentItem::ComponentInstance(_))); + /// assert_eq!(exports[3].0, ["i", "h"]); + /// assert!(matches!(exports[3].1, ComponentItem::ComponentFunc(_))); + /// + /// # Ok(()) + /// # } + /// ``` + pub fn exports_rec<'a>( + &'a self, + instance: Option<&'_ ComponentExportIndex>, + ) -> Option< + impl Iterator, types::ComponentItem, ComponentExportIndex)> + use<'a>, + > { + self.exports(instance).map(|i| { + i.flat_map(|(name, item, index)| { + let name = vec![name.to_owned()]; + let base = std::iter::once((name.clone(), item.clone(), index.clone())); + match item { + types::ComponentItem::ComponentInstance(_) => { + Box::new(base.chain(self.exports_rec(Some(&index)).unwrap().map( + move |(mut suffix, item, index)| { + let mut name = name.clone(); + name.append(&mut suffix); + (name, item, index) + }, + ))) + } + _ => Box::new(base) + as Box< + dyn Iterator< + Item = ( + Vec, + types::ComponentItem, + ComponentExportIndex, + ), + > + 'a, + >, + } + }) + }) + } + pub(crate) fn lookup_export_index( &self, instance: Option<&ComponentExportIndex>, diff --git a/src/commands/run.rs b/src/commands/run.rs index bd45e2daa5c2..4e33c4d44a4c 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -466,31 +466,31 @@ impl RunCommand { } #[cfg(feature = "component-model")] CliLinker::Component(linker) => { + let component = module.unwrap_component(); if self.invoke.is_some() { - bail!("using `--invoke` with components is not supported"); + self.invoke_component(&mut *store, component, linker).await + } else { + let command = wasmtime_wasi::bindings::Command::instantiate_async( + &mut *store, + component, + linker, + ) + .await?; + + let result = command + .wasi_cli_run() + .call_run(&mut *store) + .await + .context("failed to invoke `run` function") + .map_err(|e| self.handle_core_dump(&mut *store, e)); + + // Translate the `Result<(),()>` produced by wasm into a feigned + // explicit exit here with status 1 if `Err(())` is returned. + result.and_then(|wasm_result| match wasm_result { + Ok(()) => Ok(()), + Err(()) => Err(wasmtime_wasi::I32Exit(1).into()), + }) } - - let component = module.unwrap_component(); - - let command = wasmtime_wasi::bindings::Command::instantiate_async( - &mut *store, - component, - linker, - ) - .await?; - let result = command - .wasi_cli_run() - .call_run(&mut *store) - .await - .context("failed to invoke `run` function") - .map_err(|e| self.handle_core_dump(&mut *store, e)); - - // Translate the `Result<(),()>` produced by wasm into a feigned - // explicit exit here with status 1 if `Err(())` is returned. - result.and_then(|wasm_result| match wasm_result { - Ok(()) => Ok(()), - Err(()) => Err(wasmtime_wasi::I32Exit(1).into()), - }) } }; finish_epoch_handler(store); @@ -498,6 +498,63 @@ impl RunCommand { result } + #[cfg(feature = "component-model")] + async fn invoke_component( + &self, + store: &mut Store, + component: &wasmtime::component::Component, + linker: &mut wasmtime::component::Linker, + ) -> Result<()> { + use wasmtime::component::{ + types::ComponentItem, + wasm_wave::{ + untyped::UntypedFuncCall, + wasm::{DisplayFuncResults, WasmFunc}, + }, + Val, + }; + + let invoke = self.invoke.as_ref().unwrap(); + let untyped_call = UntypedFuncCall::parse(invoke) + .with_context(|| format!("parsing invoke \"{invoke}\""))?; + let name = untyped_call.name(); + let matches = component + .exports_rec(None) + .expect("at root") + .filter(|(names, _, _)| names.last().expect("always at least one name") == name) + .collect::>(); + match matches.len() { + 0 => bail!("No export named `{name}` in component."), + 1 => {} + _ => bail!("Multiple exports named `{name}`: {matches:?}. FIXME: support some way to disambiguate names"), + }; + let (params, result_len, export) = match &matches[0] { + (_names, ComponentItem::ComponentFunc(func), export) => { + let param_types = WasmFunc::params(func).collect::>(); + let params = untyped_call.to_wasm_params(¶m_types).with_context(|| { + format!("while interpreting parameters in invoke \"{invoke}\"") + })?; + (params, func.results().len(), export) + } + (names, ty, _) => { + bail!("Cannot invoke export {names:?}: expected ComponentFunc, got type {ty:?}"); + } + }; + + let instance = linker.instantiate_async(&mut *store, component).await?; + + let func = instance + .get_func(&mut *store, export) + .expect("found export index"); + + let mut results = vec![Val::Bool(false); result_len]; + func.call_async(&mut *store, ¶ms, &mut results).await?; + + println!("{}", DisplayFuncResults(&results)); + + Ok(()) + } + async fn invoke_func(&self, store: &mut Store, func: Func) -> Result<()> { let ty = func.ty(&store); if ty.params().len() > 0 { diff --git a/tests/all/cli_tests.rs b/tests/all/cli_tests.rs index 2c6f48f0c747..2da751c2e11e 100644 --- a/tests/all/cli_tests.rs +++ b/tests/all/cli_tests.rs @@ -1139,14 +1139,7 @@ mod test_programs { #[test] fn cli_hello_stdout() -> Result<()> { - run_wasmtime(&[ - "run", - "-Wcomponent-model", - CLI_HELLO_STDOUT_COMPONENT, - "gussie", - "sparky", - "willa", - ])?; + run_wasmtime(&["run", "-Wcomponent-model", CLI_HELLO_STDOUT_COMPONENT])?; Ok(()) } @@ -2070,6 +2063,26 @@ after empty ])?; Ok(()) } + + mod invoke { + use super::*; + + #[test] + fn cli_hello_stdout() -> Result<()> { + println!("{CLI_HELLO_STDOUT_COMPONENT}"); + let output = run_wasmtime(&[ + "run", + "-Wcomponent-model", + "--invoke", + "run()", + CLI_HELLO_STDOUT_COMPONENT, + ])?; + // First this component prints "hello, world", then the invoke + // result is printed as "ok". + assert_eq!(output, "hello, world\nok\n"); + Ok(()) + } + } } #[test]