Skip to content

Commit 9446e57

Browse files
committed
Require Wasmtime options come before wasm modules
This commit implements a new behavior for the CLI of the `wasmtime` executable which will require that options for Wasmtime itself come before the wasm module being run. Currently they're allowed to come afterwards, but instead all arguments and flags coming after a module will be interpreted as arguments for the module itself. This feature has a bit of a storied history at this point, and the breadcrumbs are: * Originally landed in bytecodealliance#6737 * Reverted for 12.0.0 in bytecodealliance#6830 * Reverted for 13.0.0 in bytecodealliance#6944 This PR is intended to be landed as a sibling of bytecodealliance#6925, another independent overhaul of Wasmtime's own options on the CLI, for the Wasmtime 14.0.0 release. More information about the motivation for this change, as well as consequences of the fallout, can be found on bytecodealliance#6737.
1 parent 6875e26 commit 9446e57

File tree

3 files changed

+154
-80
lines changed

3 files changed

+154
-80
lines changed

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: 117 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@
66
)]
77

88
use anyhow::{anyhow, bail, Context as _, Error, Result};
9-
use clap::builder::{OsStringValueParser, TypedValueParser};
109
use clap::Parser;
11-
use std::ffi::OsStr;
12-
use std::ffi::OsString;
1310
use std::fs::File;
1411
use std::io::Write;
1512
use std::path::{Path, PathBuf};
@@ -38,18 +35,6 @@ use wasmtime_wasi_threads::WasiThreadsCtx;
3835
#[cfg(feature = "wasi-http")]
3936
use wasmtime_wasi_http::WasiHttpCtx;
4037

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

10186
/// Runs a WebAssembly module
10287
#[derive(Parser)]
88+
<<<<<<< HEAD
10389
#[structopt(name = "run", trailing_var_arg = true)]
90+
=======
91+
#[structopt(name = "run", after_help = AFTER_HELP.as_str())]
92+
>>>>>>> 562330d0e2 (Require Wasmtime options come before wasm modules)
10493
pub struct RunCommand {
10594
#[clap(flatten)]
10695
common: CommonOptions,
@@ -137,13 +126,30 @@ pub struct RunCommand {
137126
#[clap(long, value_name = "FUNCTION")]
138127
invoke: Option<String>,
139128

129+
<<<<<<< HEAD
140130
/// The path of the WebAssembly module to run
141131
#[clap(
142132
required = true,
143133
value_name = "MODULE",
144134
value_parser = OsStringValueParser::new().try_map(parse_module),
145135
)]
146136
module: PathBuf,
137+
=======
138+
/// Grant access to a guest directory mapped as a host directory
139+
#[clap(long = "mapdir", number_of_values = 1, value_name = "GUEST_DIR::HOST_DIR", value_parser = parse_map_dirs)]
140+
map_dirs: Vec<(String, String)>,
141+
142+
/// Pre-load machine learning graphs (i.e., models) for use by wasi-nn.
143+
///
144+
/// Each use of the flag will preload a ML model from the host directory
145+
/// using the given model encoding. The model will be mapped to the
146+
/// directory name: e.g., `--wasi-nn-graph openvino:/foo/bar` will preload
147+
/// an OpenVINO model named `bar`. Note that which model encodings are
148+
/// available is dependent on the backends implemented in the
149+
/// `wasmtime_wasi_nn` crate.
150+
#[clap(long = "wasi-nn-graph", value_name = "FORMAT::HOST_DIR", value_parser = parse_graphs)]
151+
graphs: Vec<(String, String)>,
152+
>>>>>>> 562330d0e2 (Require Wasmtime options come before wasm modules)
147153

148154
/// Load the given WebAssembly module before the main module
149155
#[clap(
@@ -176,10 +182,70 @@ pub struct RunCommand {
176182
)]
177183
profile: Option<Profile>,
178184

185+
<<<<<<< HEAD
179186
// NOTE: this must come last for trailing varargs
180187
/// The arguments to pass to the module
181188
#[clap(value_name = "ARGS")]
182189
module_args: Vec<String>,
190+
=======
191+
/// Enable coredump generation after a WebAssembly trap.
192+
#[clap(long = "coredump-on-trap", value_name = "PATH")]
193+
coredump_on_trap: Option<String>,
194+
195+
/// Maximum size, in bytes, that a linear memory is allowed to reach.
196+
///
197+
/// Growth beyond this limit will cause `memory.grow` instructions in
198+
/// WebAssembly modules to return -1 and fail.
199+
#[clap(long, value_name = "BYTES")]
200+
max_memory_size: Option<usize>,
201+
202+
/// Maximum size, in table elements, that a table is allowed to reach.
203+
#[clap(long)]
204+
max_table_elements: Option<u32>,
205+
206+
/// Maximum number of WebAssembly instances allowed to be created.
207+
#[clap(long)]
208+
max_instances: Option<usize>,
209+
210+
/// Maximum number of WebAssembly tables allowed to be created.
211+
#[clap(long)]
212+
max_tables: Option<usize>,
213+
214+
/// Maximum number of WebAssembly linear memories allowed to be created.
215+
#[clap(long)]
216+
max_memories: Option<usize>,
217+
218+
/// Force a trap to be raised on `memory.grow` and `table.grow` failure
219+
/// instead of returning -1 from these instructions.
220+
///
221+
/// This is not necessarily a spec-compliant option to enable but can be
222+
/// useful for tracking down a backtrace of what is requesting so much
223+
/// memory, for example.
224+
#[clap(long)]
225+
trap_on_grow_failure: bool,
226+
227+
/// Enables memory error checking.
228+
///
229+
/// See wmemcheck.md for documentation on how to use.
230+
#[clap(long)]
231+
wmemcheck: bool,
232+
233+
/// The WebAssembly module to run and arguments to pass to it.
234+
///
235+
/// Arguments passed to the wasm module will be configured as WASI CLI
236+
/// arguments unless the `--invoke` CLI argument is passed in which case
237+
/// arguments will be interpreted as arguments to the function specified.
238+
#[clap(value_name = "WASM", trailing_var_arg = true, required = true)]
239+
module_and_args: Vec<PathBuf>,
240+
241+
/// Indicates that the implementation of WASI preview1 should be backed by
242+
/// the preview2 implementation for components.
243+
///
244+
/// This will become the default in the future and this option will be
245+
/// removed. For now this is primarily here for testing.
246+
#[clap(long)]
247+
preview2: bool,
248+
>>>>>>> 562330d0e2 (Require Wasmtime options come before wasm modules)
183249
}
184250

185251
#[derive(Clone)]
@@ -242,7 +308,7 @@ impl RunCommand {
242308
let engine = Engine::new(&config)?;
243309

244310
// Read the wasm module binary either as `*.wat` or a raw binary.
245-
let main = self.load_module(&engine, &self.module)?;
311+
let main = self.load_module(&engine, &self.module_and_args[0])?;
246312

247313
// Validate coredump-on-trap argument
248314
if let Some(coredump_path) = &self.common.debug.coredump {
@@ -335,8 +401,12 @@ impl RunCommand {
335401
// Load the main wasm module.
336402
match self
337403
.load_main_module(&mut store, &mut linker, &main, modules)
338-
.with_context(|| format!("failed to run main module `{}`", self.module.display()))
339-
{
404+
.with_context(|| {
405+
format!(
406+
"failed to run main module `{}`",
407+
self.module_and_args[0].display()
408+
)
409+
}) {
340410
Ok(()) => (),
341411
Err(e) => {
342412
// Exit the process if Wasmtime understands the error;
@@ -377,27 +447,25 @@ impl RunCommand {
377447
Ok(listeners)
378448
}
379449

380-
fn compute_argv(&self) -> Vec<String> {
450+
fn compute_argv(&self) -> Result<Vec<String>> {
381451
let mut result = Vec::new();
382452

383-
// Add argv[0], which is the program name. Only include the base name of the
384-
// main wasm module, to avoid leaking path information.
385-
result.push(
386-
self.module
387-
.components()
388-
.next_back()
389-
.map(|c| c.as_os_str())
390-
.and_then(OsStr::to_str)
391-
.unwrap_or("")
392-
.to_owned(),
393-
);
394-
395-
// Add the remaining arguments.
396-
for arg in self.module_args.iter() {
397-
result.push(arg.clone());
453+
for (i, arg) in self.module_and_args.iter().enumerate() {
454+
// For argv[0], which is the program name. Only include the base
455+
// name of the main wasm module, to avoid leaking path information.
456+
let arg = if i == 0 {
457+
arg.components().next_back().unwrap().as_os_str()
458+
} else {
459+
arg.as_ref()
460+
};
461+
result.push(
462+
arg.to_str()
463+
.ok_or_else(|| anyhow!("failed to convert {arg:?} to utf-8"))?
464+
.to_string(),
465+
);
398466
}
399467

400-
result
468+
Ok(result)
401469
}
402470

403471
fn setup_epoch_handler(
@@ -406,7 +474,7 @@ impl RunCommand {
406474
modules: Vec<(String, Module)>,
407475
) -> Box<dyn FnOnce(&mut Store<Host>)> {
408476
if let Some(Profile::Guest { path, interval }) = &self.profile {
409-
let module_name = self.module.to_str().unwrap_or("<main module>");
477+
let module_name = self.module_and_args[0].to_str().unwrap_or("<main module>");
410478
let interval = *interval;
411479
store.data_mut().guest_profiler =
412480
Some(Arc::new(GuestProfiler::new(module_name, interval, modules)));
@@ -512,9 +580,10 @@ impl RunCommand {
512580
CliLinker::Core(linker) => {
513581
// Use "" as a default module name.
514582
let module = module.unwrap_core();
515-
linker
516-
.module(&mut *store, "", &module)
517-
.context(format!("failed to instantiate {:?}", self.module))?;
583+
linker.module(&mut *store, "", &module).context(format!(
584+
"failed to instantiate {:?}",
585+
self.module_and_args[0]
586+
))?;
518587

519588
// If a function to invoke was given, invoke it.
520589
let func = if let Some(name) = &self.invoke {
@@ -569,7 +638,7 @@ impl RunCommand {
569638
is experimental and may break in the future"
570639
);
571640
}
572-
let mut args = self.module_args.iter();
641+
let mut args = self.module_and_args.iter().skip(1);
573642
let mut values = Vec::new();
574643
for ty in ty.params() {
575644
let val = match args.next() {
@@ -582,6 +651,9 @@ impl RunCommand {
582651
}
583652
}
584653
};
654+
let val = val
655+
.to_str()
656+
.ok_or_else(|| anyhow!("argument is not valid utf-8: {val:?}"))?;
585657
values.push(match ty {
586658
// TODO: integer parsing here should handle hexadecimal notation
587659
// like `0x0...`, but the Rust standard library currently only
@@ -639,7 +711,9 @@ impl RunCommand {
639711
if !err.is::<wasmtime::Trap>() {
640712
return err;
641713
}
642-
let source_name = self.module.to_str().unwrap_or_else(|| "unknown");
714+
let source_name = self.module_and_args[0]
715+
.to_str()
716+
.unwrap_or_else(|| "unknown");
643717

644718
if let Err(coredump_err) = generate_coredump(&err, &source_name, coredump_path) {
645719
eprintln!("warning: coredump failed to generate: {}", coredump_err);
@@ -884,7 +958,7 @@ impl RunCommand {
884958

885959
fn set_preview1_ctx(&self, store: &mut Store<Host>) -> Result<()> {
886960
let mut builder = WasiCtxBuilder::new();
887-
builder.inherit_stdio().args(&self.compute_argv())?;
961+
builder.inherit_stdio().args(&self.compute_argv()?)?;
888962

889963
for (key, value) in self.vars.iter() {
890964
let value = match value {
@@ -916,7 +990,7 @@ impl RunCommand {
916990

917991
fn set_preview2_ctx(&self, store: &mut Store<Host>) -> Result<()> {
918992
let mut builder = preview2::WasiCtxBuilder::new();
919-
builder.inherit_stdio().args(&self.compute_argv());
993+
builder.inherit_stdio().args(&self.compute_argv()?);
920994

921995
for (key, value) in self.vars.iter() {
922996
let value = match value {

0 commit comments

Comments
 (0)