From 59258730c2e148e65c629a6b0f308a229f8be995 Mon Sep 17 00:00:00 2001 From: Peter Huene Date: Tue, 17 Dec 2019 15:38:00 -0500 Subject: [PATCH] Use structopt instead of docopt. This commit refactors the Wasmtime CLI tools to use `structopt` instead of `docopt`. The `wasmtime` tool now has the following subcommands: * `config new` - creates a new Wasmtime configuration file. * `run` - runs a WebAssembly module. * `wasm2obj` - translates a Wasm module to native object file. * `wast` - runs a test script file. If no subcommand is specified, the `run` subcommand is used. Thus, `wasmtime foo.wasm` should continue to function as expected. The `wasm2obj` and `wast` tools still exist, but delegate to the same implementation as the `wasmtime` subcommands. The standalone `wasm2obj` and `wast` tools may be removed in the future in favor of simply using `wasmtime`. Included in this commit is a breaking change to the default Wasmtime configuration file: it has been renamed from `wasmtime-cache-config.toml` to simply `config.toml`. The new name is less specific which will allow for additional (non-cache-related) settings in the future. There are some breaking changes to improve command line UX: * The `--cache-config` option has been renamed to `--config`. * The `--create-config-file` option has moved to the `config new` subcommand. As a result, the `wasm2obj` and `wast` tools cannot be used to create a new config file. * The short form of the `--optimize` option has changed from `-o` to `-O` for consistency. * The `wasm2obj` command takes the output object file as a required positional argument rather than the former required output *option* (e.g. `wasmtime wasm2obj foo.wasm foo.obj`). --- Cargo.lock | 53 ++-- Cargo.toml | 3 +- crates/api/Cargo.toml | 2 - crates/environ/src/cache/config.rs | 4 +- docs/cli-cache.md | 4 +- src/bin/wasm2obj.rs | 295 +---------------- src/bin/wasmtime.rs | 491 ++++------------------------- src/bin/wast.rs | 152 +-------- src/commands.rs | 8 + src/commands/config.rs | 49 +++ src/commands/run.rs | 372 ++++++++++++++++++++++ src/commands/wasm2obj.rs | 205 ++++++++++++ src/commands/wast.rs | 83 +++++ src/lib.rs | 70 +++- 14 files changed, 900 insertions(+), 891 deletions(-) create mode 100644 src/commands.rs create mode 100644 src/commands/config.rs create mode 100644 src/commands/run.rs create mode 100644 src/commands/wasm2obj.rs create mode 100644 src/commands/wast.rs diff --git a/Cargo.lock b/Cargo.lock index e0a39439b7..2e9e8c7f56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -316,7 +316,7 @@ dependencies = [ "ansi_term", "atty", "bitflags", - "strsim 0.8.0", + "strsim", "textwrap", "unicode-width", "vec_map", @@ -550,18 +550,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "docopt" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f525a586d310c87df72ebcd98009e57f1cc030c8c268305287a476beb653969" -dependencies = [ - "lazy_static", - "regex", - "serde", - "strsim 0.9.2", -] - [[package]] name = "dynasm" version = "0.5.2" @@ -1185,6 +1173,17 @@ dependencies = [ "log", ] +[[package]] +name = "proc-macro-error" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeccfe4d5d8ea175d5f0e4a2ad0637e0f4121d63bd99d356fb1f39ab2e7c6097" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "proc-macro-hack" version = "0.5.11" @@ -1622,10 +1621,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] -name = "strsim" -version = "0.9.2" +name = "structopt" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "032c03039aae92b350aad2e3779c352e104d919cb192ba2fabbd7b831ce4f0f6" +checksum = "30b3a3e93f5ad553c38b3301c8a0a0cec829a36783f6a0c467fc4bf553a5f5bf" +dependencies = [ + "clap", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea692d40005b3ceba90a9fe7a78fa8d4b82b0ce627eebbffc329aab850f3410e" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] [[package]] name = "syn" @@ -1950,12 +1966,10 @@ name = "wasmtime" version = "0.7.0" dependencies = [ "anyhow", - "docopt", "file-per-thread-logger", "pretty_env_logger", "rayon", "region", - "serde", "target-lexicon", "thiserror", "wasi-common", @@ -1973,14 +1987,13 @@ name = "wasmtime-cli" version = "0.7.0" dependencies = [ "anyhow", - "docopt", "faerie", "file-per-thread-logger", "libc", "more-asserts", "pretty_env_logger", "rayon", - "serde", + "structopt", "target-lexicon", "test-programs", "wasi-common", diff --git a/Cargo.toml b/Cargo.toml index 323ad14482..4298134555 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,8 +25,7 @@ wasmtime-wast = { path = "crates/wast" } wasmtime-wasi = { path = "crates/wasi" } wasmtime-wasi-c = { path = "crates/wasi-c", optional = true } wasi-common = { path = "crates/wasi-common" } -docopt = "1.0.1" -serde = { "version" = "1.0.94", features = ["derive"] } +structopt = { version = "0.3.5", features = ["color", "suggestions"] } faerie = "0.13.0" anyhow = "1.0.19" target-lexicon = { version = "0.9.0", default-features = false } diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index 3a71adc41e..bb00360fde 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -25,8 +25,6 @@ region = "2.0.0" [dev-dependencies] # for wasmtime.rs wasi-common = { path = "../wasi-common" } -docopt = "1.0.1" -serde = { "version" = "1.0.94", features = ["derive"] } pretty_env_logger = "0.3.0" wasmtime-wast = { path = "../wast" } wasmtime-wasi = { path = "../wasi" } diff --git a/crates/environ/src/cache/config.rs b/crates/environ/src/cache/config.rs index 73831aa7bf..1db0c6833a 100644 --- a/crates/environ/src/cache/config.rs +++ b/crates/environ/src/cache/config.rs @@ -151,7 +151,7 @@ pub fn create_new_config + Debug>(config_file: Option

) -> Resu if config_file.exists() { bail!( - "Specified config file already exists! Path: {}", + "Configuration file '{}' already exists.", config_file.display() ); } @@ -193,7 +193,7 @@ lazy_static! { ProjectDirs::from("", "BytecodeAlliance", "wasmtime"); static ref DEFAULT_CONFIG_PATH: Result = PROJECT_DIRS .as_ref() - .map(|proj_dirs| proj_dirs.config_dir().join("wasmtime-cache-config.toml")) + .map(|proj_dirs| proj_dirs.config_dir().join("config.toml")) .ok_or_else(|| "Config file not specified and failed to get the default".to_string()); } diff --git a/docs/cli-cache.md b/docs/cli-cache.md index ce7eca8de8..c8c9f1680e 100644 --- a/docs/cli-cache.md +++ b/docs/cli-cache.md @@ -1,9 +1,9 @@ # Cache Configuration of `wasmtime` -The cache configuration file uses the [toml] format. +The configuration file uses the [toml] format. You can create a configuration file at the default location with: ``` -$ wasmtime --create-cache-config +$ wasmtime config new ``` It will print the location regardless of the success. Please refer to the `--help` message for using a custom location. diff --git a/src/bin/wasm2obj.rs b/src/bin/wasm2obj.rs index 5be8084c0b..12e02b3e20 100644 --- a/src/bin/wasm2obj.rs +++ b/src/bin/wasm2obj.rs @@ -1,291 +1,12 @@ -//! Translation from wasm to native object files. +//! The `wasm2obj` command line tool. //! -//! Reads a Wasm binary file, translates the functions' code to Cranelift -//! IL, then translates it to native code, and writes it out to a native -//! object file with relocations. +//! Translates WebAssembly modules to object files. +//! See `wasm2obj --help` for usage. -#![deny( - missing_docs, - trivial_numeric_casts, - unused_extern_crates, - unstable_features -)] -#![warn(unused_import_braces)] -#![cfg_attr(feature = "clippy", plugin(clippy(conf_file = "../clippy.toml")))] -#![cfg_attr(feature = "cargo-clippy", allow(clippy::new_without_default))] -#![cfg_attr( - feature = "cargo-clippy", - warn( - clippy::float_arithmetic, - clippy::mut_mut, - clippy::nonminimal_bool, - clippy::option_map_unwrap_or, - clippy::option_map_unwrap_or_else, - clippy::unicode_not_nfc, - clippy::use_self - ) -)] +use anyhow::Result; +use structopt::StructOpt; +use wasmtime_cli::commands::WasmToObjCommand; -use anyhow::{anyhow, bail, Result}; -use docopt::Docopt; -use faerie::Artifact; -use serde::Deserialize; -use std::path::{Path, PathBuf}; -use std::str::FromStr; -use std::{process, str}; -use target_lexicon::Triple; -use wasmtime::Strategy; -use wasmtime_cli::pick_compilation_strategy; -use wasmtime_debug::{emit_debugsections, read_debuginfo}; -use wasmtime_environ::entity::EntityRef; -use wasmtime_environ::settings; -use wasmtime_environ::settings::Configurable; -use wasmtime_environ::wasm::DefinedMemoryIndex; -#[cfg(feature = "lightbeam")] -use wasmtime_environ::Lightbeam; -use wasmtime_environ::{ - cache_create_new_config, cache_init, Compiler, Cranelift, ModuleEnvironment, ModuleVmctxInfo, - Tunables, VMOffsets, -}; -use wasmtime_jit::native; -use wasmtime_obj::emit_module; - -const USAGE: &str = " -Wasm to native object translation utility. -Takes a binary WebAssembly module into a native object file. -The translation is dependent on the environment chosen. -The default is a dummy environment that produces placeholder values. - -Usage: - wasm2obj [--target TARGET] [-Odg] [--disable-cache | --cache-config=] \ - [--enable-simd] [--lightbeam | --cranelift] -o - wasm2obj --create-cache-config [--cache-config=] - wasm2obj --help | --version - -Options: - -v, --verbose displays the module and translated functions - -h, --help print this help message - --target build for the target triple; default is the host machine - -g generate debug information - --disable-cache disables cache system - --cache-config= - use specified cache configuration; - can be used with --create-cache-config to specify custom file - --create-cache-config - creates default configuration and writes it to the disk, - use with --cache-config to specify custom config file - instead of default one - --lightbeam use Lightbeam for all compilation - --cranelift use Cranelift for all compilation - --enable-simd enable proposed SIMD instructions - -O, --optimize runs optimization passes on the translated functions - --version print the Cranelift version - -d, --debug enable debug output on stderr/stdout -"; - -#[derive(Deserialize, Debug, Clone)] -struct Args { - arg_file: String, - arg_output: String, - arg_target: Option, - flag_g: bool, - flag_debug: bool, - flag_disable_cache: bool, - flag_cache_config: Option, - flag_create_cache_config: bool, - flag_enable_simd: bool, - flag_lightbeam: bool, - flag_cranelift: bool, - flag_optimize: bool, -} - -fn main() { - let version = env!("CARGO_PKG_VERSION"); - let args: Args = Docopt::new(USAGE) - .and_then(|d| { - d.help(true) - .version(Some(String::from(version))) - .deserialize() - }) - .unwrap_or_else(|e| e.exit()); - - let log_config = if args.flag_debug { - pretty_env_logger::init(); - None - } else { - let prefix = "wasm2obj.dbg."; - wasmtime_cli::init_file_per_thread_logger(prefix); - Some(prefix) - }; - - if args.flag_create_cache_config { - match cache_create_new_config(args.flag_cache_config) { - Ok(path) => { - println!( - "Successfully created new configuation file at {}", - path.display() - ); - return; - } - Err(err) => { - eprintln!("Error: {}", err); - process::exit(1); - } - } - } - - let errors = cache_init( - !args.flag_disable_cache, - args.flag_cache_config.as_ref(), - log_config, - ); - - if !errors.is_empty() { - eprintln!("Cache initialization failed. Errors:"); - for e in errors { - eprintln!("-> {}", e); - } - process::exit(1); - } - - let path = Path::new(&args.arg_file); - match handle_module( - path.to_path_buf(), - &args.arg_target, - &args.arg_output, - args.flag_g, - args.flag_enable_simd, - args.flag_optimize, - args.flag_cranelift, - args.flag_lightbeam, - ) { - Ok(()) => {} - Err(message) => { - println!(" error: {}", message); - process::exit(1); - } - } -} - -fn handle_module( - path: PathBuf, - target: &Option, - output: &str, - generate_debug_info: bool, - enable_simd: bool, - enable_optimize: bool, - cranelift: bool, - lightbeam: bool, -) -> Result<()> { - let data = wat::parse_file(path)?; - - let isa_builder = match *target { - Some(ref target) => { - let target = - Triple::from_str(&target).map_err(|_| anyhow!("could not parse --target"))?; - native::lookup(target)? - } - None => native::builder(), - }; - let mut flag_builder = settings::builder(); - - // There are two possible traps for division, and this way - // we get the proper one if code traps. - flag_builder.enable("avoid_div_traps").unwrap(); - - if enable_simd { - flag_builder.enable("enable_simd").unwrap(); - } - - if enable_optimize { - flag_builder.set("opt_level", "speed").unwrap(); - } - - let isa = isa_builder.finish(settings::Flags::new(flag_builder)); - - let mut obj = Artifact::new(isa.triple().clone(), String::from(output)); - - // TODO: Expose the tunables as command-line flags. - let tunables = Tunables::default(); - - // Decide how to compile. - let strategy = pick_compilation_strategy(cranelift, lightbeam)?; - - let ( - module, - module_translation, - lazy_function_body_inputs, - lazy_data_initializers, - target_config, - ) = { - let environ = ModuleEnvironment::new(isa.frontend_config(), tunables); - - let translation = environ.translate(&data)?; - ( - translation.module, - translation.module_translation.unwrap(), - translation.function_body_inputs, - translation.data_initializers, - translation.target_config, - ) - }; - - // TODO: use the traps information - let (compilation, relocations, address_transform, value_ranges, stack_slots, _traps) = - match strategy { - Strategy::Auto | Strategy::Cranelift => Cranelift::compile_module( - &module, - &module_translation, - lazy_function_body_inputs, - &*isa, - generate_debug_info, - )?, - #[cfg(feature = "lightbeam")] - Strategy::Lightbeam => Lightbeam::compile_module( - &module, - &module_translation, - lazy_function_body_inputs, - &*isa, - generate_debug_info, - )?, - #[cfg(not(feature = "lightbeam"))] - Strategy::Lightbeam => bail!("lightbeam support not enabled"), - other => bail!("unsupported compilation strategy {:?}", other), - }; - - let module_vmctx_info = { - let ofs = VMOffsets::new(target_config.pointer_bytes(), &module); - let memory_offset = ofs.vmctx_vmmemory_definition_base(DefinedMemoryIndex::new(0)) as i64; - ModuleVmctxInfo { - memory_offset, - stack_slots, - } - }; - - emit_module( - &mut obj, - &module, - &compilation, - &relocations, - &lazy_data_initializers, - &target_config, - )?; - - if generate_debug_info { - let debug_data = read_debuginfo(&data); - emit_debugsections( - &mut obj, - &module_vmctx_info, - target_config, - &debug_data, - &address_transform, - &value_ranges, - )? - } - - // FIXME: Make the format a parameter. - let file = ::std::fs::File::create(Path::new(output))?; - obj.write(file)?; - - Ok(()) +fn main() -> Result<()> { + WasmToObjCommand::from_args().execute() } diff --git a/src/bin/wasmtime.rs b/src/bin/wasmtime.rs index aa04be0c90..89c1078f48 100644 --- a/src/bin/wasmtime.rs +++ b/src/bin/wasmtime.rs @@ -1,442 +1,73 @@ -//! CLI tool to use the functions provided by the [wasmtime](../wasmtime/index.html) -//! crate. +//! The `wasmtime` command line tool. //! -//! Reads Wasm binary files (one Wasm module per file), translates the functions' code to Cranelift -//! IL. Can also execute the `start` function of the module by laying out the memories, globals -//! and tables, then emitting the translated code with hardcoded addresses to memory. +//! Primarily used to run WebAssembly modules. +//! See `wasmtime --help` for usage. -#![deny( - missing_docs, - trivial_numeric_casts, - unused_extern_crates, - unstable_features +use anyhow::Result; +use structopt::{clap::AppSettings, clap::ErrorKind, StructOpt}; +use wasmtime_cli::commands::{ + ConfigCommand, RunCommand, WasmToObjCommand, WastCommand, WASM2OBJ_AFTER_HELP, +}; + +/// Wasmtime WebAssembly Runtime +#[derive(StructOpt)] +#[structopt( + name = "wasmtime", + version = env!("CARGO_PKG_VERSION"), + global_settings = &[ + AppSettings::VersionlessSubcommands, + AppSettings::ColoredHelp + ], + after_help = "If a subcommand is not provided, the `run` subcommand will be used.\n\ + \n\ + Usage examples:\n\ + \n\ + Running a WebAssembly module with a start function:\n\ + \n \ + wasmtime example.wasm + \n\ + Passing command line arguments to a WebAssembly module:\n\ + \n \ + wasmtime example.wasm arg1 arg2 arg3\n\ + \n\ + Invoking a specific function (e.g. `add`) in a WebAssembly module:\n\ + \n \ + wasmtime example.wasm --invoke add 1 2\n" )] -#![warn(unused_import_braces)] -#![cfg_attr(feature = "clippy", plugin(clippy(conf_file = "../clippy.toml")))] -#![cfg_attr(feature = "cargo-clippy", allow(clippy::new_without_default))] -#![cfg_attr( - feature = "cargo-clippy", - warn( - clippy::float_arithmetic, - clippy::mut_mut, - clippy::nonminimal_bool, - clippy::option_map_unwrap_or, - clippy::option_map_unwrap_or_else, - clippy::unicode_not_nfc, - clippy::use_self - ) -)] - -use anyhow::{bail, Context as _, Result}; -use docopt::Docopt; -use serde::Deserialize; -use std::path::{Component, Path}; -use std::{collections::HashMap, ffi::OsStr, fs::File, process::exit}; -use wasi_common::preopen_dir; -use wasmtime::{Config, Engine, HostRef, Instance, Module, Store}; -use wasmtime_cli::pick_compilation_strategy; -use wasmtime_environ::{cache_create_new_config, cache_init}; -use wasmtime_interface_types::ModuleData; -use wasmtime_wasi::create_wasi_instance; -use wasmtime_wasi::old::snapshot_0::create_wasi_instance as create_wasi_instance_snapshot_0; -#[cfg(feature = "wasi-c")] -use wasmtime_wasi_c::instantiate_wasi_c; - -const USAGE: &str = " -Wasm runner. - -Takes a binary (wasm) or text (wat) WebAssembly module and instantiates it, -including calling the start function if one is present. Additional functions -given with --invoke are then called. - -Usage: - wasmtime [-odg] [--enable-simd] [--wasi-c] [--disable-cache | \ - --cache-config=] [--preload=...] [--env=...] [--dir=

...] \ - [--mapdir=...] [--lightbeam | --cranelift] [...] - wasmtime [-odg] [--enable-simd] [--wasi-c] [--disable-cache | \ - --cache-config=] [--env=...] [--dir=...] \ - [--mapdir=...] --invoke= [--lightbeam | --cranelift] [...] - wasmtime --create-cache-config [--cache-config=] - wasmtime --help | --version - -Options: - --invoke= name of function to run - -o, --optimize runs optimization passes on the translated functions - --disable-cache disables cache system - --cache-config= - use specified cache configuration; - can be used with --create-cache-config to specify custom file - --create-cache-config - creates default configuration and writes it to the disk, - use with --cache-config to specify custom config file - instead of default one - -g generate debug information - -d, --debug enable debug output on stderr/stdout - --lightbeam use Lightbeam for all compilation - --cranelift use Cranelift for all compilation - --enable-simd enable proposed SIMD instructions - --wasi-c enable the wasi-c implementation of `wasi_unstable` - --preload= load an additional wasm module before loading the main module - --env= pass an environment variable (\"key=value\") to the program - --dir= grant access to the given host directory - --mapdir= where has the form ::, grant access to - the given host directory with the given wasm directory name - -h, --help print this help message - --version print the Cranelift version -"; - -#[derive(Deserialize, Debug, Clone)] -struct Args { - arg_file: String, - arg_arg: Vec, - flag_optimize: bool, - flag_disable_cache: bool, - flag_cache_config: Option, - flag_create_cache_config: bool, - flag_debug: bool, - flag_g: bool, - flag_enable_simd: bool, - flag_lightbeam: bool, - flag_cranelift: bool, - flag_invoke: Option, - flag_preload: Vec, - flag_env: Vec, - flag_dir: Vec, - flag_mapdir: Vec, - flag_wasi_c: bool, +enum WasmtimeApp { + // !!! IMPORTANT: if subcommands are added or removed, update `parse_module` in `src/commands/run.rs`. !!! + /// Controls Wasmtime configuration settings + Config(ConfigCommand), + /// Runs a WebAssembly module + Run(RunCommand), + /// Translates a WebAssembly module to native object file + #[structopt(name = "wasm2obj", after_help = WASM2OBJ_AFTER_HELP)] + WasmToObj(WasmToObjCommand), + /// Runs a WebAssembly test script file + Wast(WastCommand), } -fn compute_preopen_dirs(flag_dir: &[String], flag_mapdir: &[String]) -> Vec<(String, File)> { - let mut preopen_dirs = Vec::new(); - - for dir in flag_dir { - let preopen_dir = preopen_dir(dir).unwrap_or_else(|err| { - println!("error while pre-opening directory {}: {}", dir, err); - exit(1); - }); - preopen_dirs.push((dir.clone(), preopen_dir)); - } - - for mapdir in flag_mapdir { - let parts: Vec<&str> = mapdir.split("::").collect(); - if parts.len() != 2 { - println!( - "--mapdir argument must contain exactly one double colon ('::'), separating a \ - guest directory name and a host directory name" - ); - exit(1); +impl WasmtimeApp { + /// Executes the command. + pub fn execute(&self) -> Result<()> { + match self { + Self::Config(c) => c.execute(), + Self::Run(c) => c.execute(), + Self::WasmToObj(c) => c.execute(), + Self::Wast(c) => c.execute(), } - let (key, value) = (parts[0], parts[1]); - let preopen_dir = preopen_dir(value).unwrap_or_else(|err| { - println!("error while pre-opening directory {}: {}", value, err); - exit(1); - }); - preopen_dirs.push((key.to_string(), preopen_dir)); } - - preopen_dirs -} - -/// Compute the argv array values. -fn compute_argv(argv0: &str, arg_arg: &[String]) -> Vec { - let mut result = Vec::new(); - - // Add argv[0], which is the program name. Only include the base name of the - // main wasm module, to avoid leaking path information. - result.push( - Path::new(argv0) - .components() - .next_back() - .map(Component::as_os_str) - .and_then(OsStr::to_str) - .unwrap_or("") - .to_owned(), - ); - - // Add the remaining arguments. - for arg in arg_arg { - result.push(arg.to_owned()); - } - - result -} - -/// Compute the environ array values. -fn compute_environ(flag_env: &[String]) -> Vec<(String, String)> { - let mut result = Vec::new(); - - // Add the environment variables, which must be of the form "key=value". - for env in flag_env { - let split = env.splitn(2, '=').collect::>(); - if split.len() != 2 { - println!( - "environment variables must be of the form \"key=value\"; got \"{}\"", - env - ); - } - result.push((split[0].to_owned(), split[1].to_owned())); - } - - result } fn main() -> Result<()> { - let version = env!("CARGO_PKG_VERSION"); - let args: Args = Docopt::new(USAGE) - .and_then(|d| { - d.help(true) - .version(Some(String::from(version))) - .deserialize() + WasmtimeApp::from_iter_safe(std::env::args()) + .unwrap_or_else(|e| match e.kind { + ErrorKind::HelpDisplayed + | ErrorKind::VersionDisplayed + | ErrorKind::MissingArgumentOrSubcommand => e.exit(), + _ => WasmtimeApp::Run( + RunCommand::from_iter_safe(std::env::args()).unwrap_or_else(|_| e.exit()), + ), }) - .unwrap_or_else(|e| e.exit()); - - let log_config = if args.flag_debug { - pretty_env_logger::init(); - None - } else { - let prefix = "wasmtime.dbg."; - wasmtime_cli::init_file_per_thread_logger(prefix); - Some(prefix) - }; - - if args.flag_create_cache_config { - match cache_create_new_config(args.flag_cache_config) { - Ok(path) => { - println!( - "Successfully created new configuation file at {}", - path.display() - ); - return Ok(()); - } - Err(err) => { - eprintln!("Error: {}", err); - exit(1); - } - } - } - - let errors = cache_init( - !args.flag_disable_cache, - args.flag_cache_config.as_ref(), - log_config, - ); - - if !errors.is_empty() { - eprintln!("Cache initialization failed. Errors:"); - for e in errors { - eprintln!("-> {}", e); - } - exit(1); - } - - let mut config = Config::new(); - - // Enable/disable producing of debug info. - config.debug_info(args.flag_g); - - // Enable verifier passes in debug mode. - config.cranelift_debug_verifier(cfg!(debug_assertions)); - - // Enable SIMD if requested - if args.flag_enable_simd { - config.wasm_simd(true); - } - - // Enable optimization if requested. - if args.flag_optimize { - config.cranelift_opt_level(wasmtime::OptLevel::Speed); - } - - // Decide how to compile. - config.strategy(pick_compilation_strategy( - args.flag_cranelift, - args.flag_lightbeam, - )?)?; - let engine = Engine::new(&config); - let store = HostRef::new(Store::new(&engine)); - - let mut module_registry = HashMap::new(); - - // Make wasi available by default. - let preopen_dirs = compute_preopen_dirs(&args.flag_dir, &args.flag_mapdir); - let argv = compute_argv(&args.arg_file, &args.arg_arg); - let environ = compute_environ(&args.flag_env); - - let wasi_unstable = HostRef::new(if args.flag_wasi_c { - #[cfg(feature = "wasi-c")] - { - let global_exports = store.borrow().global_exports().clone(); - let handle = instantiate_wasi_c(global_exports, &preopen_dirs, &argv, &environ)?; - Instance::from_handle(&store, handle) - } - #[cfg(not(feature = "wasi-c"))] - { - bail!("wasi-c feature not enabled at build time") - } - } else { - create_wasi_instance_snapshot_0(&store, &preopen_dirs, &argv, &environ)? - }); - - let wasi_snapshot_preview1 = HostRef::new(create_wasi_instance( - &store, - &preopen_dirs, - &argv, - &environ, - )?); - - module_registry.insert("wasi_unstable".to_owned(), wasi_unstable); - module_registry.insert("wasi_snapshot_preview1".to_owned(), wasi_snapshot_preview1); - - // Load the preload wasm modules. - for filename in &args.flag_preload { - let path = Path::new(&filename); - instantiate_module(&store, &module_registry, path) - .with_context(|| format!("failed to process preload at `{}`", path.display()))?; - } - - // Load the main wasm module. - let path = Path::new(&args.arg_file); - handle_module(&store, &module_registry, &args, path) - .with_context(|| format!("failed to process main module `{}`", path.display()))?; - Ok(()) -} - -fn instantiate_module( - store: &HostRef, - module_registry: &HashMap>, - path: &Path, -) -> Result<(HostRef, HostRef, Vec)> { - // Read the wasm module binary either as `*.wat` or a raw binary - let data = wat::parse_file(path.to_path_buf())?; - - let module = HostRef::new(Module::new(store, &data)?); - - // Resolve import using module_registry. - let imports = module - .borrow() - .imports() - .iter() - .map(|i| { - let module_name = i.module(); - if let Some(instance) = module_registry.get(module_name) { - let field_name = i.name(); - if let Some(export) = instance.borrow().find_export_by_name(field_name) { - Ok(export.clone()) - } else { - bail!( - "Import {} was not found in module {}", - field_name, - module_name - ) - } - } else { - bail!("Import module {} was not found", module_name) - } - }) - .collect::, _>>()?; - - let instance = HostRef::new(match Instance::new(store, &module, &imports) { - Ok(instance) => instance, - Err(trap) => bail!("Failed to instantiate {:?}: {:?}", path, trap), - }); - - Ok((instance, module, data)) -} - -fn handle_module( - store: &HostRef, - module_registry: &HashMap>, - args: &Args, - path: &Path, -) -> Result<()> { - let (instance, module, data) = instantiate_module(store, module_registry, path)?; - - // If a function to invoke was given, invoke it. - if let Some(f) = &args.flag_invoke { - let data = ModuleData::new(&data)?; - invoke_export(instance, &data, f, args)?; - } else if module - .borrow() - .exports() - .iter() - .any(|export| export.name().is_empty()) - { - // Launch the default command export. - let data = ModuleData::new(&data)?; - invoke_export(instance, &data, "", args)?; - } else { - // If the module doesn't have a default command export, launch the - // _start function if one is present, as a compatibility measure. - let data = ModuleData::new(&data)?; - invoke_export(instance, &data, "_start", args)?; - } - - Ok(()) -} - -fn invoke_export( - instance: HostRef, - data: &ModuleData, - name: &str, - args: &Args, -) -> Result<()> { - use wasm_webidl_bindings::ast; - use wasmtime_interface_types::Value; - - let mut handle = instance.borrow().handle().clone(); - - // Use the binding information in `ModuleData` to figure out what arguments - // need to be passed to the function that we're invoking. Currently we take - // the CLI parameters and attempt to parse them into function arguments for - // the function we'll invoke. - let binding = data.binding_for_export(&mut handle, name)?; - if !binding.param_types()?.is_empty() { - eprintln!( - "warning: using `--invoke` with a function that takes arguments \ - is experimental and may break in the future" - ); - } - let mut values = Vec::new(); - let mut args = args.arg_arg.iter(); - for ty in binding.param_types()? { - let val = match args.next() { - Some(s) => s, - None => bail!("not enough arguments for `{}`", name), - }; - values.push(match ty { - // TODO: integer parsing here should handle hexadecimal notation - // like `0x0...`, but the Rust standard library currently only - // parses base-10 representations. - ast::WebidlScalarType::Long => Value::I32(val.parse()?), - ast::WebidlScalarType::LongLong => Value::I64(val.parse()?), - ast::WebidlScalarType::UnsignedLong => Value::U32(val.parse()?), - ast::WebidlScalarType::UnsignedLongLong => Value::U64(val.parse()?), - - ast::WebidlScalarType::Float | ast::WebidlScalarType::UnrestrictedFloat => { - Value::F32(val.parse()?) - } - ast::WebidlScalarType::Double | ast::WebidlScalarType::UnrestrictedDouble => { - Value::F64(val.parse()?) - } - ast::WebidlScalarType::DomString => Value::String(val.to_string()), - t => bail!("unsupported argument type {:?}", t), - }); - } - - // Invoke the function and then afterwards print all the results that came - // out, if there are any. - let results = data - .invoke_export(&instance, name, &values) - .with_context(|| format!("failed to invoke `{}`", name))?; - if !results.is_empty() { - eprintln!( - "warning: using `--invoke` with a function that returns values \ - is experimental and may break in the future" - ); - } - for result in results { - println!("{}", result); - } - - Ok(()) + .execute() } diff --git a/src/bin/wast.rs b/src/bin/wast.rs index bd0ab19cc1..1407a7eb67 100644 --- a/src/bin/wast.rs +++ b/src/bin/wast.rs @@ -1,148 +1,12 @@ -//! CLI tool to run wast tests using the wasmtime libraries. +//! The `wast` command line tool. +//! +//! Runs WebAssembly test script files. +//! See `wast --help` for usage. -#![deny( - missing_docs, - trivial_numeric_casts, - unused_extern_crates, - unstable_features -)] -#![warn(unused_import_braces)] -#![cfg_attr(feature = "clippy", plugin(clippy(conf_file = "../../clippy.toml")))] -#![cfg_attr(feature = "cargo-clippy", allow(clippy::new_without_default))] -#![cfg_attr( - feature = "cargo-clippy", - warn( - clippy::float_arithmetic, - clippy::mut_mut, - clippy::nonminimal_bool, - clippy::option_map_unwrap_or, - clippy::option_map_unwrap_or_else, - clippy::unicode_not_nfc, - clippy::use_self - ) -)] - -use anyhow::{Context, Result}; -use docopt::Docopt; -use serde::Deserialize; -use std::path::Path; -use std::process; -use wasmtime::{Config, Engine, HostRef, Store}; -use wasmtime_cli::pick_compilation_strategy; -use wasmtime_environ::{cache_create_new_config, cache_init}; -use wasmtime_wast::WastContext; - -const USAGE: &str = " -Wast test runner. - -Usage: - wast [-do] [--enable-simd] [--disable-cache | --cache-config=] [--lightbeam \ - | --cranelift] ... - wast --create-cache-config [--cache-config=] - wast --help | --version - -Options: - -h, --help print this help message - --version print the Cranelift version - -o, --optimize runs optimization passes on the translated functions - --disable-cache disables cache system - --cache-config= - use specified cache configuration; - can be used with --create-cache-config to specify custom file - --create-cache-config - creates default configuration and writes it to the disk, - use with --cache-config to specify custom config file - instead of default one - --lightbeam use Lightbeam for all compilation - --cranelift use Cranelift for all compilation - -d, --debug enable debug output on stderr/stdout - --enable-simd enable proposed SIMD instructions -"; - -#[derive(Deserialize, Debug, Clone)] -struct Args { - arg_file: Vec, - flag_debug: bool, - flag_function: Option, - flag_optimize: bool, - flag_disable_cache: bool, - flag_cache_config: Option, - flag_create_cache_config: bool, - flag_enable_simd: bool, - flag_lightbeam: bool, - flag_cranelift: bool, -} +use anyhow::Result; +use structopt::StructOpt; +use wasmtime_cli::commands::WastCommand; fn main() -> Result<()> { - let version = env!("CARGO_PKG_VERSION"); - let args: Args = Docopt::new(USAGE) - .and_then(|d| { - d.help(true) - .version(Some(String::from(version))) - .deserialize() - }) - .unwrap_or_else(|e| e.exit()); - - let log_config = if args.flag_debug { - pretty_env_logger::init(); - None - } else { - let prefix = "cranelift.dbg."; - wasmtime_cli::init_file_per_thread_logger(prefix); - Some(prefix) - }; - - if args.flag_create_cache_config { - let path = cache_create_new_config(args.flag_cache_config)?; - println!( - "Successfully created new configuation file at {}", - path.display() - ); - return Ok(()); - } - - let errors = cache_init( - !args.flag_disable_cache, - args.flag_cache_config.as_ref(), - log_config, - ); - - if !errors.is_empty() { - eprintln!("Cache initialization failed. Errors:"); - for e in errors { - eprintln!("-> {}", e); - } - process::exit(1); - } - - let mut cfg = Config::new(); - // Enable verifier passes in debug mode. - cfg.cranelift_debug_verifier(cfg!(debug_assertions)); - - // Enable optimization if requested. - if args.flag_optimize { - cfg.cranelift_opt_level(wasmtime::OptLevel::Speed); - } - - // Enable SIMD if requested - if args.flag_enable_simd { - cfg.wasm_simd(true); - } - - // Decide how to compile. - cfg.strategy(pick_compilation_strategy( - args.flag_cranelift, - args.flag_lightbeam, - )?)?; - let store = HostRef::new(Store::new(&Engine::new(&cfg))); - let mut wast_context = WastContext::new(store); - - wast_context - .register_spectest() - .context("error instantiating \"spectest\"")?; - - for filename in &args.arg_file { - wast_context.run_file(Path::new(&filename))?; - } - Ok(()) + WastCommand::from_args().execute() } diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000000..e9891bab99 --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,8 @@ +//! The module for the Wasmtime CLI commands. + +mod config; +mod run; +mod wasm2obj; +mod wast; + +pub use self::{config::*, run::*, wasm2obj::*, wast::*}; diff --git a/src/commands/config.rs b/src/commands/config.rs new file mode 100644 index 0000000000..5507bf502c --- /dev/null +++ b/src/commands/config.rs @@ -0,0 +1,49 @@ +//! The module that implements the `wasmtime config` command. + +use anyhow::{anyhow, Result}; +use structopt::StructOpt; +use wasmtime_environ::cache_create_new_config; + +const CONFIG_NEW_AFTER_HELP: &str = + "If no file path is specified, the system configuration file path will be used."; + +/// Controls Wasmtime configuration settings +#[derive(StructOpt)] +#[structopt(name = "run")] +pub enum ConfigCommand { + /// Creates a new Wasmtime configuration file + #[structopt(after_help = CONFIG_NEW_AFTER_HELP)] + New(ConfigNewCommand), +} + +impl ConfigCommand { + /// Executes the command. + pub fn execute(&self) -> Result<()> { + match self { + Self::New(c) => c.execute(), + } + } +} + +/// Creates a new Wasmtime configuration file +#[derive(StructOpt)] +#[structopt(name = "new", after_help = CONFIG_NEW_AFTER_HELP)] +pub struct ConfigNewCommand { + /// The path of the new configuration file + #[structopt(index = 1, value_name = "FILE_PATH")] + path: Option, +} + +impl ConfigNewCommand { + /// Executes the command. + pub fn execute(&self) -> Result<()> { + let path = cache_create_new_config(self.path.as_ref()).map_err(|e| anyhow!(e))?; + + println!( + "Successfully created a new configuation file at '{}'.", + path.display() + ); + + Ok(()) + } +} diff --git a/src/commands/run.rs b/src/commands/run.rs new file mode 100644 index 0000000000..d23594fa9d --- /dev/null +++ b/src/commands/run.rs @@ -0,0 +1,372 @@ +//! The module that implements the `wasmtime run` command. + +use crate::{init_file_per_thread_logger, pick_compilation_strategy, CommonOptions}; +use anyhow::{bail, Context as _, Result}; +use std::{ + collections::HashMap, + ffi::{OsStr, OsString}, + fmt::Write, + fs::File, + path::{Component, Path, PathBuf}, +}; +use structopt::{clap::AppSettings, StructOpt}; +use wasi_common::preopen_dir; +use wasmtime::{Config, Engine, HostRef, Instance, Module, Store}; +use wasmtime_environ::cache_init; +use wasmtime_interface_types::ModuleData; +use wasmtime_wasi::{ + create_wasi_instance, old::snapshot_0::create_wasi_instance as create_wasi_instance_snapshot_0, +}; +#[cfg(feature = "wasi-c")] +use wasmtime_wasi_c::instantiate_wasi_c; +#[cfg(feature = "wasi-c")] +use wasmtime_wasi_c::instantiate_wasi_c; + +fn parse_module(s: &OsStr) -> Result { + // Do not accept wasmtime subcommand names as the module name + match s.to_str() { + Some("help") | Some("config") | Some("run") | Some("wasm2obj") | Some("wast") => { + Err("module name cannot be the same as a subcommand".into()) + } + _ => Ok(s.into()), + } +} + +fn parse_env_var(s: &str) -> Result<(String, String)> { + let parts: Vec<_> = s.splitn(2, '=').collect(); + if parts.len() != 2 { + bail!("must be of the form `key=value`"); + } + Ok((parts[0].to_owned(), parts[1].to_owned())) +} + +fn parse_map_dirs(s: &str) -> Result<(String, String)> { + let parts: Vec<&str> = s.split("::").collect(); + if parts.len() != 2 { + bail!("must contain exactly one double colon ('::')"); + } + Ok((parts[0].into(), parts[1].into())) +} + +/// Runs a WebAssembly module +#[derive(StructOpt)] +#[structopt(name = "run", setting = AppSettings::TrailingVarArg)] +pub struct RunCommand { + #[structopt(flatten)] + common: CommonOptions, + + /// Grant access to the given host directory + #[structopt(long = "dir", number_of_values = 1, value_name = "DIRECTORY")] + dirs: Vec, + + /// Pass an environment variable to the program + #[structopt(long = "env", number_of_values = 1, value_name = "NAME=VAL", parse(try_from_str = parse_env_var))] + vars: Vec<(String, String)>, + + /// The name of the function to run + #[structopt(long, value_name = "FUNCTION")] + invoke: Option, + + /// Grant access to a guest directory mapped as a host directory + #[structopt(long = "mapdir", number_of_values = 1, value_name = "GUEST_DIR::HOST_DIR", parse(try_from_str = parse_map_dirs))] + map_dirs: Vec<(String, String)>, + + /// The path of the WebAssembly module to run + #[structopt( + index = 1, + required = true, + value_name = "WASM_MODULE", + parse(try_from_os_str = parse_module), + )] + module: PathBuf, + + /// Load the given WebAssembly module before the main module + #[structopt( + long = "preload", + number_of_values = 1, + value_name = "MODULE_PATH", + parse(from_os_str) + )] + preloads: Vec, + + /// Enable the wasi-c implementation of `wasi_unstable` + #[structopt(long = "wasi-c")] + enable_wasi_c: bool, + + // NOTE: this must come last for trailing varargs + /// The arguments to pass to the module + #[structopt(value_name = "ARGS")] + module_args: Vec, +} + +impl RunCommand { + /// Executes the command. + pub fn execute(&self) -> Result<()> { + let log_config = if self.common.debug { + pretty_env_logger::init(); + None + } else { + let prefix = "wasmtime.dbg."; + init_file_per_thread_logger(prefix); + Some(prefix) + }; + + let errors = cache_init( + !self.common.disable_cache, + self.common.config.as_ref(), + log_config, + ); + + if !errors.is_empty() { + let mut message = String::new(); + writeln!(message, "Cache initialization failed. Errors:")?; + for e in errors { + writeln!(message, " -> {}", e)?; + } + bail!(message); + } + + let mut config = Config::new(); + config + .cranelift_debug_verifier(cfg!(debug_assertions)) + .debug_info(self.common.debug_info) + .wasm_simd(self.common.enable_simd) + .strategy(pick_compilation_strategy( + self.common.cranelift, + self.common.lightbeam, + )?)?; + + if self.common.optimize { + config.cranelift_opt_level(wasmtime::OptLevel::Speed); + } + + let engine = Engine::new(&config); + let store = HostRef::new(Store::new(&engine)); + let mut module_registry = HashMap::new(); + + // Make wasi available by default. + let preopen_dirs = self.compute_preopen_dirs()?; + let argv = self.compute_argv(); + + let wasi_unstable = HostRef::new(if self.enable_wasi_c { + #[cfg(feature = "wasi-c")] + { + let global_exports = store.borrow().global_exports().clone(); + let handle = instantiate_wasi_c(global_exports, &preopen_dirs, &argv, &self.vars)?; + Instance::from_handle(&store, handle) + } + #[cfg(not(feature = "wasi-c"))] + { + bail!("wasi-c feature not enabled at build time") + } + } else { + create_wasi_instance_snapshot_0(&store, &preopen_dirs, &argv, &self.vars)? + }); + + let wasi_snapshot_preview1 = HostRef::new(create_wasi_instance( + &store, + &preopen_dirs, + &argv, + &self.vars, + )?); + + module_registry.insert("wasi_unstable".to_owned(), wasi_unstable); + module_registry.insert("wasi_snapshot_preview1".to_owned(), wasi_snapshot_preview1); + + // Load the preload wasm modules. + for preload in self.preloads.iter() { + Self::instantiate_module(&store, &module_registry, preload) + .with_context(|| format!("failed to process preload at `{}`", preload.display()))?; + } + + // Load the main wasm module. + self.handle_module(&store, &module_registry) + .with_context(|| format!("failed to run main module `{}`", self.module.display()))?; + + Ok(()) + } + + fn compute_preopen_dirs(&self) -> Result> { + let mut preopen_dirs = Vec::new(); + + for dir in self.dirs.iter() { + preopen_dirs.push(( + dir.clone(), + preopen_dir(dir).with_context(|| format!("failed to open directory '{}'", dir))?, + )); + } + + for (guest, host) in self.map_dirs.iter() { + preopen_dirs.push(( + guest.clone(), + preopen_dir(host) + .with_context(|| format!("failed to open directory '{}'", host))?, + )); + } + + Ok(preopen_dirs) + } + + fn compute_argv(&self) -> Vec { + let mut result = Vec::new(); + + // Add argv[0], which is the program name. Only include the base name of the + // main wasm module, to avoid leaking path information. + result.push( + self.module + .components() + .next_back() + .map(Component::as_os_str) + .and_then(OsStr::to_str) + .unwrap_or("") + .to_owned(), + ); + + // Add the remaining arguments. + for arg in self.module_args.iter() { + result.push(arg.clone()); + } + + result + } + + fn instantiate_module( + store: &HostRef, + module_registry: &HashMap>, + path: &Path, + ) -> Result<(HostRef, HostRef, Vec)> { + // Read the wasm module binary either as `*.wat` or a raw binary + let data = wat::parse_file(path.to_path_buf())?; + + let module = HostRef::new(Module::new(store, &data)?); + + // Resolve import using module_registry. + let imports = module + .borrow() + .imports() + .iter() + .map(|i| { + let module_name = i.module(); + if let Some(instance) = module_registry.get(module_name) { + let field_name = i.name(); + if let Some(export) = instance.borrow().find_export_by_name(field_name) { + Ok(export.clone()) + } else { + bail!( + "Import {} was not found in module {}", + field_name, + module_name + ) + } + } else { + bail!("Import module {} was not found", module_name) + } + }) + .collect::, _>>()?; + + let instance = HostRef::new(match Instance::new(store, &module, &imports) { + Ok(instance) => instance, + Err(trap) => bail!("Failed to instantiate {:?}: {:?}", path, trap), + }); + + Ok((instance, module, data)) + } + + fn handle_module( + &self, + store: &HostRef, + module_registry: &HashMap>, + ) -> Result<()> { + let (instance, module, data) = + Self::instantiate_module(store, module_registry, &self.module)?; + + // If a function to invoke was given, invoke it. + if let Some(name) = self.invoke.as_ref() { + let data = ModuleData::new(&data)?; + self.invoke_export(instance, &data, name)?; + } else if module + .borrow() + .exports() + .iter() + .any(|export| export.name().is_empty()) + { + // Launch the default command export. + let data = ModuleData::new(&data)?; + self.invoke_export(instance, &data, "")?; + } else { + // If the module doesn't have a default command export, launch the + // _start function if one is present, as a compatibility measure. + let data = ModuleData::new(&data)?; + self.invoke_export(instance, &data, "_start")?; + } + + Ok(()) + } + + fn invoke_export( + &self, + instance: HostRef, + data: &ModuleData, + name: &str, + ) -> Result<()> { + use wasm_webidl_bindings::ast; + use wasmtime_interface_types::Value; + + let mut handle = instance.borrow().handle().clone(); + + // Use the binding information in `ModuleData` to figure out what arguments + // need to be passed to the function that we're invoking. Currently we take + // the CLI parameters and attempt to parse them into function arguments for + // the function we'll invoke. + let binding = data.binding_for_export(&mut handle, name)?; + if !binding.param_types()?.is_empty() { + eprintln!( + "warning: using `--invoke` with a function that takes arguments \ + is experimental and may break in the future" + ); + } + let mut args = self.module_args.iter(); + let mut values = Vec::new(); + for ty in binding.param_types()? { + let val = match args.next() { + Some(s) => s, + None => bail!("not enough arguments for `{}`", name), + }; + values.push(match ty { + // TODO: integer parsing here should handle hexadecimal notation + // like `0x0...`, but the Rust standard library currently only + // parses base-10 representations. + ast::WebidlScalarType::Long => Value::I32(val.parse()?), + ast::WebidlScalarType::LongLong => Value::I64(val.parse()?), + ast::WebidlScalarType::UnsignedLong => Value::U32(val.parse()?), + ast::WebidlScalarType::UnsignedLongLong => Value::U64(val.parse()?), + + ast::WebidlScalarType::Float | ast::WebidlScalarType::UnrestrictedFloat => { + Value::F32(val.parse()?) + } + ast::WebidlScalarType::Double | ast::WebidlScalarType::UnrestrictedDouble => { + Value::F64(val.parse()?) + } + ast::WebidlScalarType::DomString => Value::String(val.to_string()), + t => bail!("unsupported argument type {:?}", t), + }); + } + + // Invoke the function and then afterwards print all the results that came + // out, if there are any. + let results = data + .invoke_export(&instance, name, &values) + .with_context(|| format!("failed to invoke `{}`", name))?; + if !results.is_empty() { + eprintln!( + "warning: using `--invoke` with a function that returns values \ + is experimental and may break in the future" + ); + } + for result in results { + println!("{}", result); + } + + Ok(()) + } +} diff --git a/src/commands/wasm2obj.rs b/src/commands/wasm2obj.rs new file mode 100644 index 0000000000..0c5f04b972 --- /dev/null +++ b/src/commands/wasm2obj.rs @@ -0,0 +1,205 @@ +//! The module that implements the `wasmtime wasm2obj` command. + +use crate::{init_file_per_thread_logger, pick_compilation_strategy, CommonOptions}; +use anyhow::{anyhow, bail, Context as _, Result}; +use faerie::Artifact; +use std::{ + fmt::Write, + fs::File, + path::{Path, PathBuf}, + str::FromStr, +}; +use structopt::{clap::AppSettings, StructOpt}; +use target_lexicon::Triple; +use wasmtime::Strategy; +use wasmtime_debug::{emit_debugsections, read_debuginfo}; +#[cfg(feature = "lightbeam")] +use wasmtime_environ::Lightbeam; +use wasmtime_environ::{ + cache_init, entity::EntityRef, settings, settings::Configurable, wasm::DefinedMemoryIndex, + Compiler, Cranelift, ModuleEnvironment, ModuleVmctxInfo, Tunables, VMOffsets, +}; +use wasmtime_jit::native; +use wasmtime_obj::emit_module; + +/// The after help text for the `wasm2obj` command. +pub const WASM2OBJ_AFTER_HELP: &str = + "The translation is dependent on the environment chosen.\n\ + The default is a dummy environment that produces placeholder values."; + +fn parse_target(s: &str) -> Result { + Triple::from_str(&s).map_err(|e| anyhow!(e)) +} + +/// Translates a WebAssembly module to native object file +#[derive(StructOpt)] +#[structopt( + name = "wasm2obj", + version = env!("CARGO_PKG_VERSION"), + setting = AppSettings::ColoredHelp, + after_help = WASM2OBJ_AFTER_HELP, +)] +pub struct WasmToObjCommand { + #[structopt(flatten)] + common: CommonOptions, + + /// The path of the WebAssembly module to translate + #[structopt(index = 1, value_name = "MODULE_PATH", parse(from_os_str))] + module: PathBuf, + + /// The path of the output object file + #[structopt(index = 2, value_name = "OUTPUT_PATH")] + output: String, + + /// The target triple; default is the host triple + #[structopt(long, value_name = "TARGET", parse(try_from_str = parse_target))] + target: Option, +} + +impl WasmToObjCommand { + /// Executes the command. + pub fn execute(&self) -> Result<()> { + let log_config = if self.common.debug { + pretty_env_logger::init(); + None + } else { + let prefix = "wasm2obj.dbg."; + init_file_per_thread_logger(prefix); + Some(prefix) + }; + + let errors = cache_init( + !self.common.disable_cache, + self.common.config.as_ref(), + log_config, + ); + + if !errors.is_empty() { + let mut message = String::new(); + writeln!(message, "Cache initialization failed. Errors:")?; + for e in errors { + writeln!(message, " -> {}", e)?; + } + bail!(message); + } + + self.handle_module() + } + + fn handle_module(&self) -> Result<()> { + let strategy = pick_compilation_strategy(self.common.cranelift, self.common.lightbeam)?; + + let data = wat::parse_file(&self.module).context("failed to parse module")?; + + let isa_builder = match self.target.as_ref() { + Some(target) => native::lookup(target.clone())?, + None => native::builder(), + }; + let mut flag_builder = settings::builder(); + + // There are two possible traps for division, and this way + // we get the proper one if code traps. + flag_builder.enable("avoid_div_traps").unwrap(); + + if self.common.enable_simd { + flag_builder.enable("enable_simd").unwrap(); + } + + if self.common.optimize { + flag_builder.set("opt_level", "speed").unwrap(); + } + + let isa = isa_builder.finish(settings::Flags::new(flag_builder)); + + let mut obj = Artifact::new(isa.triple().clone(), self.output.clone()); + + // TODO: Expose the tunables as command-line flags. + let tunables = Tunables::default(); + + let ( + module, + module_translation, + lazy_function_body_inputs, + lazy_data_initializers, + target_config, + ) = { + let environ = ModuleEnvironment::new(isa.frontend_config(), tunables); + + let translation = environ + .translate(&data) + .context("failed to translate module")?; + + ( + translation.module, + translation.module_translation.unwrap(), + translation.function_body_inputs, + translation.data_initializers, + translation.target_config, + ) + }; + + // TODO: use the traps information + let (compilation, relocations, address_transform, value_ranges, stack_slots, _traps) = + match strategy { + Strategy::Auto | Strategy::Cranelift => Cranelift::compile_module( + &module, + &module_translation, + lazy_function_body_inputs, + &*isa, + self.common.debug_info, + ), + #[cfg(feature = "lightbeam")] + Strategy::Lightbeam => Lightbeam::compile_module( + &module, + &module_translation, + lazy_function_body_inputs, + &*isa, + generate_debug_info, + ), + #[cfg(not(feature = "lightbeam"))] + Strategy::Lightbeam => bail!("lightbeam support not enabled"), + other => bail!("unsupported compilation strategy {:?}", other), + } + .context("failed to compile module")?; + + let module_vmctx_info = { + let ofs = VMOffsets::new(target_config.pointer_bytes(), &module); + let memory_offset = + ofs.vmctx_vmmemory_definition_base(DefinedMemoryIndex::new(0)) as i64; + ModuleVmctxInfo { + memory_offset, + stack_slots, + } + }; + + emit_module( + &mut obj, + &module, + &compilation, + &relocations, + &lazy_data_initializers, + &target_config, + ) + .map_err(|e| anyhow!(e)) + .context("failed to emit module")?; + + if self.common.debug_info { + let debug_data = read_debuginfo(&data); + emit_debugsections( + &mut obj, + &module_vmctx_info, + target_config, + &debug_data, + &address_transform, + &value_ranges, + ) + .context("failed to emit debug sections")?; + } + + // FIXME: Make the format a parameter. + let file = File::create(Path::new(&self.output)).context("failed to create object file")?; + obj.write(file).context("failed to write object file")?; + + Ok(()) + } +} diff --git a/src/commands/wast.rs b/src/commands/wast.rs new file mode 100644 index 0000000000..8377e00848 --- /dev/null +++ b/src/commands/wast.rs @@ -0,0 +1,83 @@ +//! The module that implements the `wasmtime wast` command. + +use crate::{init_file_per_thread_logger, pick_compilation_strategy, CommonOptions}; +use anyhow::{bail, Context as _, Result}; +use std::{fmt::Write, path::PathBuf}; +use structopt::{clap::AppSettings, StructOpt}; +use wasmtime::{Config, Engine, HostRef, Store}; +use wasmtime_environ::cache_init; +use wasmtime_wast::WastContext; + +/// Runs a WebAssembly test script file +#[derive(StructOpt)] +#[structopt( + name = "wast", + version = env!("CARGO_PKG_VERSION"), + setting = AppSettings::ColoredHelp, +)] +pub struct WastCommand { + #[structopt(flatten)] + common: CommonOptions, + + /// The path of the WebAssembly test script to run + #[structopt(required = true, value_name = "SCRIPT_FILE", parse(from_os_str))] + scripts: Vec, +} + +impl WastCommand { + /// Executes the command. + pub fn execute(&self) -> Result<()> { + let log_config = if self.common.debug { + pretty_env_logger::init(); + None + } else { + let prefix = "wast.dbg."; + init_file_per_thread_logger(prefix); + Some(prefix) + }; + + let errors = cache_init( + !self.common.disable_cache, + self.common.config.as_ref(), + log_config, + ); + + if !errors.is_empty() { + let mut message = String::new(); + writeln!(message, "Cache initialization failed. Errors:")?; + for e in errors { + writeln!(message, " -> {}", e)?; + } + bail!(message); + } + + let mut config = Config::new(); + config + .cranelift_debug_verifier(cfg!(debug_assertions)) + .debug_info(self.common.debug_info) + .wasm_simd(self.common.enable_simd) + .strategy(pick_compilation_strategy( + self.common.cranelift, + self.common.lightbeam, + )?)?; + + if self.common.optimize { + config.cranelift_opt_level(wasmtime::OptLevel::Speed); + } + + let store = HostRef::new(Store::new(&Engine::new(&config))); + let mut wast_context = WastContext::new(store); + + wast_context + .register_spectest() + .expect("error instantiating \"spectest\""); + + for script in self.scripts.iter() { + wast_context + .run_file(script) + .with_context(|| format!("failed to run script file '{}'", script.display()))? + } + + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index aa64ce353e..a7324668fc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,37 @@ +//! The Wasmtime command line interface (CLI) crate. +//! +//! This crate implements the Wasmtime command line tools. + +#![deny( + missing_docs, + trivial_numeric_casts, + unused_extern_crates, + unstable_features +)] +#![warn(unused_import_braces)] +#![cfg_attr(feature = "clippy", plugin(clippy(conf_file = "../clippy.toml")))] +#![cfg_attr(feature = "cargo-clippy", allow(clippy::new_without_default))] +#![cfg_attr( + feature = "cargo-clippy", + warn( + clippy::float_arithmetic, + clippy::mut_mut, + clippy::nonminimal_bool, + clippy::option_map_unwrap_or, + clippy::option_map_unwrap_or_else, + clippy::unicode_not_nfc, + clippy::use_self + ) +)] + +pub mod commands; + use anyhow::{bail, Result}; +use std::path::PathBuf; +use structopt::StructOpt; use wasmtime::Strategy; -pub fn pick_compilation_strategy(cranelift: bool, lightbeam: bool) -> Result { +fn pick_compilation_strategy(cranelift: bool, lightbeam: bool) -> Result { Ok(match (lightbeam, cranelift) { (true, false) => Strategy::Lightbeam, (false, true) => Strategy::Cranelift, @@ -10,7 +40,7 @@ pub fn pick_compilation_strategy(cranelift: bool, lightbeam: bool) -> Result, + + /// Use Cranelift for all compilation + #[structopt(long, conflicts_with = "lightbeam")] + cranelift: bool, + + /// Enable debug output + #[structopt(short, long)] + debug: bool, + + /// Generate debug information + #[structopt(short = "g")] + debug_info: bool, + + /// Disable cache system + #[structopt(long)] + disable_cache: bool, + + /// Enable support for proposed SIMD instructions + #[structopt(long)] + enable_simd: bool, + + /// Use Lightbeam for all compilation + #[structopt(long, conflicts_with = "cranelift")] + lightbeam: bool, + + /// Run optimization passes on translated functions + #[structopt(short = "O", long)] + optimize: bool, +}