Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wasmtime-cli: support run --invoke for components using wave #10054

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,7 @@ compile = ["cranelift"]
run = [
"dep:wasmtime-wasi",
"wasmtime/runtime",
"wasmtime/wave",
"dep:listenfd",
"dep:wasi-common",
"dep:tokio",
Expand Down
206 changes: 203 additions & 3 deletions crates/wasmtime/src/runtime/component/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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::<Vec<_>>();
/// 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::<Vec<_>>();
/// 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<impl Iterator<Item = (&'a str, types::ComponentItem, ComponentExportIndex)> + 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::<Vec<_>>();
/// 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<Item = (Vec<String>, 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<String>,
types::ComponentItem,
ComponentExportIndex,
),
> + 'a,
>,
}
})
})
}

pub(crate) fn lookup_export_index(
&self,
instance: Option<&ComponentExportIndex>,
Expand Down
103 changes: 80 additions & 23 deletions src/commands/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -466,38 +466,95 @@ 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);

result
}

#[cfg(feature = "component-model")]
async fn invoke_component(
&self,
store: &mut Store<Host>,
component: &wasmtime::component::Component,
linker: &mut wasmtime::component::Linker<Host>,
) -> 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::<Vec<_>>();
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::<Vec<_>>();
let params = untyped_call.to_wasm_params(&param_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, &params, &mut results).await?;

println!("{}", DisplayFuncResults(&results));

Ok(())
}

async fn invoke_func(&self, store: &mut Store<Host>, func: Func) -> Result<()> {
let ty = func.ty(&store);
if ty.params().len() > 0 {
Expand Down
29 changes: 21 additions & 8 deletions tests/all/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

Expand Down Expand Up @@ -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]
Expand Down
Loading