Skip to content

Commit 8091556

Browse files
authored
Require wasmtime options are first when running modules (#6737)
* Require wasmtime options are first when running modules Currently the way we've configured argument parsing it's valid to execute a command such as: wasmtime run foo.wasm -O which is the same as: wasmtime run -O foo.wasm or otherwise all flags are attempted to be parsed as Wasmtime flags and an error is generated when they're not wasmtime flags. I've personally found this a bit confusing in the past and I find myself frequently executing: wasmtime run -- foo.wasm -other -arguments While this works my general impression is that many other "wrapper commands" don't behave this way and typically don't require `--` to pass flags to the target executable. This commit reconfigures argument parsing to consider any argument after the WebAssembly module itself to be an argument to the wasm program rather than an argument to Wasmtime. This means that all Wasmtime options must come between the `run` command and the `foo.wasm` WebAssembly argument. * Update wasi testsuite runner * Support `wasmtime -- run` Additionally use more clap features to avoid manually checking against subcommands. * Remove stale comment * Reorder wasi-nn arguments * Reorder more flags * Fix unused import on Windows * Don't run a stdio test on Windows * Update gdb/lldb tests * Don't assert that the write succeeds prtest:full
1 parent 329a2ba commit 8091556

10 files changed

Lines changed: 339 additions & 138 deletions

File tree

ci/run-wasi-crypto-example.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ pushd "$RUST_BINDINGS"
77
cargo build --release --target=wasm32-wasi
88
popd
99

10-
cargo run --features wasi-crypto -- run "$RUST_BINDINGS/target/wasm32-wasi/release/wasi-crypto-guest.wasm" --wasi-modules=experimental-wasi-crypto
10+
cargo run --features wasi-crypto -- run --wasi-modules=experimental-wasi-crypto "$RUST_BINDINGS/target/wasm32-wasi/release/wasi-crypto-guest.wasm"

ci/run-wasi-nn-example.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ cp target/wasm32-wasi/release/wasi-nn-example.wasm $TMP_DIR
3333
popd
3434

3535
# Run the example in Wasmtime (note that the example uses `fixture` as the expected location of the model/tensor files).
36-
cargo run -- run --mapdir fixture::$TMP_DIR $TMP_DIR/wasi-nn-example.wasm --wasi-modules=experimental-wasi-nn
36+
cargo run -- run --mapdir fixture::$TMP_DIR --wasi-modules=experimental-wasi-nn $TMP_DIR/wasi-nn-example.wasm
3737

3838
# Clean up the temporary directory only if it was not specified (users may want to keep the directory around).
3939
if [[ $REMOVE_TMP_DIR -eq 1 ]]; then

src/bin/wasmtime.rs

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//! See `wasmtime --help` for usage.
55
66
use anyhow::Result;
7-
use clap::{error::ErrorKind, Parser};
7+
use clap::Parser;
88
use wasmtime_cli::commands::{
99
CompileCommand, ConfigCommand, ExploreCommand, RunCommand, SettingsCommand, WastCommand,
1010
};
@@ -27,10 +27,24 @@ use wasmtime_cli::commands::{
2727
\n\
2828
Invoking a specific function (e.g. `add`) in a WebAssembly module:\n\
2929
\n \
30-
wasmtime example.wasm --invoke add 1 2\n"
30+
wasmtime example.wasm --invoke add 1 2\n",
31+
32+
// This option enables the pattern below where we ask clap to parse twice
33+
// sorta: once where it's trying to find a subcommand and once assuming
34+
// a subcommand doesn't get passed. Clap should then, apparently,
35+
// fill in the `subcommand` if found and otherwise fill in the
36+
// `RunCommand`.
37+
args_conflicts_with_subcommands = true
3138
)]
32-
enum Wasmtime {
33-
// !!! IMPORTANT: if subcommands are added or removed, update `parse_module` in `src/commands/run.rs`. !!!
39+
struct Wasmtime {
40+
#[clap(subcommand)]
41+
subcommand: Option<Subcommand>,
42+
#[clap(flatten)]
43+
run: RunCommand,
44+
}
45+
46+
#[derive(Parser)]
47+
enum Subcommand {
3448
/// Controls Wasmtime configuration settings
3549
Config(ConfigCommand),
3650
/// Compiles a WebAssembly module.
@@ -48,26 +62,20 @@ enum Wasmtime {
4862
impl Wasmtime {
4963
/// Executes the command.
5064
pub fn execute(self) -> Result<()> {
51-
match self {
52-
Self::Config(c) => c.execute(),
53-
Self::Compile(c) => c.execute(),
54-
Self::Explore(c) => c.execute(),
55-
Self::Run(c) => c.execute(),
56-
Self::Settings(c) => c.execute(),
57-
Self::Wast(c) => c.execute(),
65+
let subcommand = self.subcommand.unwrap_or(Subcommand::Run(self.run));
66+
match subcommand {
67+
Subcommand::Config(c) => c.execute(),
68+
Subcommand::Compile(c) => c.execute(),
69+
Subcommand::Explore(c) => c.execute(),
70+
Subcommand::Run(c) => c.execute(),
71+
Subcommand::Settings(c) => c.execute(),
72+
Subcommand::Wast(c) => c.execute(),
5873
}
5974
}
6075
}
6176

6277
fn main() -> Result<()> {
63-
Wasmtime::try_parse()
64-
.unwrap_or_else(|e| match e.kind() {
65-
ErrorKind::InvalidSubcommand | ErrorKind::UnknownArgument => {
66-
Wasmtime::Run(RunCommand::parse())
67-
}
68-
_ => e.exit(),
69-
})
70-
.execute()
78+
Wasmtime::parse().execute()
7179
}
7280

7381
#[test]

src/commands/run.rs

Lines changed: 50 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
//! The module that implements the `wasmtime run` command.
22
33
use anyhow::{anyhow, bail, Context as _, Result};
4-
use clap::builder::{OsStringValueParser, TypedValueParser};
54
use clap::Parser;
65
use once_cell::sync::Lazy;
7-
use std::ffi::OsStr;
8-
use std::ffi::OsString;
96
use std::fs::File;
107
use std::io::Write;
11-
use std::path::{Component, Path, PathBuf};
8+
use std::path::{Path, PathBuf};
129
use std::thread;
1310
use std::time::Duration;
1411
use wasmtime::{
@@ -39,18 +36,6 @@ use wasmtime_wasi_threads::WasiThreadsCtx;
3936
#[cfg(feature = "wasi-http")]
4037
use wasmtime_wasi_http::WasiHttp;
4138

42-
fn parse_module(s: OsString) -> anyhow::Result<PathBuf> {
43-
// Do not accept wasmtime subcommand names as the module name
44-
match s.to_str() {
45-
Some("help") | Some("config") | Some("run") | Some("wast") | Some("compile") => {
46-
bail!("module name cannot be the same as a subcommand")
47-
}
48-
#[cfg(unix)]
49-
Some("-") => Ok(PathBuf::from("/dev/stdin")),
50-
_ => Ok(s.into()),
51-
}
52-
}
53-
5439
fn parse_env_var(s: &str) -> Result<(String, Option<String>)> {
5540
let mut parts = s.splitn(2, '=');
5641
Ok((
@@ -111,7 +96,7 @@ static AFTER_HELP: Lazy<String> = Lazy::new(|| crate::FLAG_EXPLANATIONS.to_strin
11196

11297
/// Runs a WebAssembly module
11398
#[derive(Parser)]
114-
#[structopt(name = "run", trailing_var_arg = true, after_help = AFTER_HELP.as_str())]
99+
#[structopt(name = "run", after_help = AFTER_HELP.as_str())]
115100
pub struct RunCommand {
116101
#[clap(flatten)]
117102
common: CommonOptions,
@@ -174,14 +159,6 @@ pub struct RunCommand {
174159
#[clap(long = "mapdir", number_of_values = 1, value_name = "GUEST_DIR::HOST_DIR", value_parser = parse_map_dirs)]
175160
map_dirs: Vec<(String, String)>,
176161

177-
/// The path of the WebAssembly module to run
178-
#[clap(
179-
required = true,
180-
value_name = "MODULE",
181-
value_parser = OsStringValueParser::new().try_map(parse_module),
182-
)]
183-
module: PathBuf,
184-
185162
/// Load the given WebAssembly module before the main module
186163
#[clap(
187164
long = "preload",
@@ -225,11 +202,6 @@ pub struct RunCommand {
225202
#[clap(long = "coredump-on-trap", value_name = "PATH")]
226203
coredump_on_trap: Option<String>,
227204

228-
// NOTE: this must come last for trailing varargs
229-
/// The arguments to pass to the module
230-
#[clap(value_name = "ARGS")]
231-
module_args: Vec<String>,
232-
233205
/// Maximum size, in bytes, that a linear memory is allowed to reach.
234206
///
235207
/// Growth beyond this limit will cause `memory.grow` instructions in
@@ -261,6 +233,14 @@ pub struct RunCommand {
261233
/// memory, for example.
262234
#[clap(long)]
263235
trap_on_grow_failure: bool,
236+
237+
/// The WebAssembly module to run and arguments to pass to it.
238+
///
239+
/// Arguments passed to the wasm module will be configured as WASI CLI
240+
/// arguments unless the `--invoke` CLI argument is passed in which case
241+
/// arguments will be interpreted as arguments to the function specified.
242+
#[clap(value_name = "WASM", trailing_var_arg = true, required = true)]
243+
module_and_args: Vec<PathBuf>,
264244
}
265245

266246
#[derive(Clone)]
@@ -303,13 +283,13 @@ impl RunCommand {
303283

304284
// Make wasi available by default.
305285
let preopen_dirs = self.compute_preopen_dirs()?;
306-
let argv = self.compute_argv();
286+
let argv = self.compute_argv()?;
307287

308288
let mut linker = Linker::new(&engine);
309289
linker.allow_unknown_exports(self.allow_unknown_exports);
310290

311291
// Read the wasm module binary either as `*.wat` or a raw binary.
312-
let module = self.load_module(linker.engine(), &self.module)?;
292+
let module = self.load_module(linker.engine(), &self.module_and_args[0])?;
313293
let mut modules = vec![(String::new(), module.clone())];
314294

315295
let host = Host::default();
@@ -370,8 +350,12 @@ impl RunCommand {
370350
// Load the main wasm module.
371351
match self
372352
.load_main_module(&mut store, &mut linker, module, modules, &argv[0])
373-
.with_context(|| format!("failed to run main module `{}`", self.module.display()))
374-
{
353+
.with_context(|| {
354+
format!(
355+
"failed to run main module `{}`",
356+
self.module_and_args[0].display()
357+
)
358+
}) {
375359
Ok(()) => (),
376360
Err(e) => {
377361
// Exit the process if Wasmtime understands the error;
@@ -420,27 +404,25 @@ impl RunCommand {
420404
Ok(listeners)
421405
}
422406

423-
fn compute_argv(&self) -> Vec<String> {
407+
fn compute_argv(&self) -> Result<Vec<String>> {
424408
let mut result = Vec::new();
425409

426-
// Add argv[0], which is the program name. Only include the base name of the
427-
// main wasm module, to avoid leaking path information.
428-
result.push(
429-
self.module
430-
.components()
431-
.next_back()
432-
.map(Component::as_os_str)
433-
.and_then(OsStr::to_str)
434-
.unwrap_or("")
435-
.to_owned(),
436-
);
437-
438-
// Add the remaining arguments.
439-
for arg in self.module_args.iter() {
440-
result.push(arg.clone());
410+
for (i, arg) in self.module_and_args.iter().enumerate() {
411+
// For argv[0], which is the program name. Only include the base
412+
// name of the main wasm module, to avoid leaking path information.
413+
let arg = if i == 0 {
414+
arg.components().next_back().unwrap().as_os_str()
415+
} else {
416+
arg.as_ref()
417+
};
418+
result.push(
419+
arg.to_str()
420+
.ok_or_else(|| anyhow!("failed to convert {arg:?} to utf-8"))?
421+
.to_string(),
422+
);
441423
}
442424

443-
result
425+
Ok(result)
444426
}
445427

446428
fn setup_epoch_handler(
@@ -541,9 +523,10 @@ impl RunCommand {
541523
}
542524

543525
// Use "" as a default module name.
544-
linker
545-
.module(&mut *store, "", &module)
546-
.context(format!("failed to instantiate {:?}", self.module))?;
526+
linker.module(&mut *store, "", &module).context(format!(
527+
"failed to instantiate {:?}",
528+
self.module_and_args[0]
529+
))?;
547530

548531
// If a function to invoke was given, invoke it.
549532
let func = if let Some(name) = &self.invoke {
@@ -584,7 +567,7 @@ impl RunCommand {
584567
is experimental and may break in the future"
585568
);
586569
}
587-
let mut args = self.module_args.iter();
570+
let mut args = self.module_and_args.iter().skip(1);
588571
let mut values = Vec::new();
589572
for ty in ty.params() {
590573
let val = match args.next() {
@@ -597,6 +580,9 @@ impl RunCommand {
597580
}
598581
}
599582
};
583+
let val = val
584+
.to_str()
585+
.ok_or_else(|| anyhow!("argument is not valid utf-8: {val:?}"))?;
600586
values.push(match ty {
601587
// TODO: integer parsing here should handle hexadecimal notation
602588
// like `0x0...`, but the Rust standard library currently only
@@ -623,7 +609,9 @@ impl RunCommand {
623609
if let Err(err) = invoke_res {
624610
let err = if err.is::<wasmtime::Trap>() {
625611
if let Some(coredump_path) = self.coredump_on_trap.as_ref() {
626-
let source_name = self.module.to_str().unwrap_or_else(|| "unknown");
612+
let source_name = self.module_and_args[0]
613+
.to_str()
614+
.unwrap_or_else(|| "unknown");
627615

628616
if let Err(coredump_err) = generate_coredump(&err, &source_name, coredump_path)
629617
{
@@ -664,6 +652,12 @@ impl RunCommand {
664652
}
665653

666654
fn load_module(&self, engine: &Engine, path: &Path) -> Result<Module> {
655+
let path = match path.to_str() {
656+
#[cfg(unix)]
657+
Some("-") => "/dev/stdin".as_ref(),
658+
_ => path,
659+
};
660+
667661
if self.allow_precompiled {
668662
unsafe { Module::from_trusted_file(engine, path) }
669663
} else {

0 commit comments

Comments
 (0)