From 1f37f748b2e95e1bf461f9c63da269bc3b08c0eb Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Mon, 20 Apr 2020 16:31:50 -0700 Subject: [PATCH] Reactor support. This implements the new WASI ABI described here: https://github.com/WebAssembly/WASI/blob/master/design/application-abi.md It adds APIs to `Instance` and `Linker` with support for running WASI programs, and also simplifies the process of instantiating WASI API modules. This currently only includes Rust API support. --- RELEASES.md | 7 + crates/runtime/src/instance.rs | 4 - .../test-programs/tests/wasm_tests/runtime.rs | 11 +- crates/wasi-common/wig/src/wasi.rs | 2 + crates/wasi/src/lib.rs | 39 ++++- crates/wasmtime/src/instance.rs | 56 ++++++- crates/wasmtime/src/linker.rs | 26 ++- examples/wasi/main.rs | 32 +--- src/commands/run.rs | 151 ++++++------------ tests/all/cli_tests.rs | 83 ++++++++++ tests/wasm/greeter_command.wat | 22 +++ tests/wasm/greeter_reactor.wat | 22 +++ tests/wasm/minimal-command.wat | 3 + tests/wasm/minimal-reactor.wat | 3 + 14 files changed, 320 insertions(+), 141 deletions(-) create mode 100644 tests/wasm/greeter_command.wat create mode 100644 tests/wasm/greeter_reactor.wat create mode 100644 tests/wasm/minimal-command.wat create mode 100644 tests/wasm/minimal-reactor.wat diff --git a/RELEASES.md b/RELEASES.md index 0bafdbf08f36..80ce30aca387 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -8,6 +8,13 @@ Unreleased ### Added +* The [WASI commands and reactors ABI] is now supported. To use, create a + `Linker` instance with `wasmtime_wasi::wasi_linker`, and instantiate modules + with `Linker::instantiate_wasi_abi`. This will automatically run commands + and automatically initialize reactors. + +[WASI commands and reactors ABI]: https://github.com/WebAssembly/WASI/blob/master/design/application-abi.md#current-unstable-abi + ### Changed ### Fixed diff --git a/crates/runtime/src/instance.rs b/crates/runtime/src/instance.rs index 0e0550141a42..6c2f984c4a83 100644 --- a/crates/runtime/src/instance.rs +++ b/crates/runtime/src/instance.rs @@ -1330,8 +1330,4 @@ pub enum InstantiationError { /// A trap ocurred during instantiation, after linking. #[error("Trap occurred during instantiation")] Trap(Trap), - - /// A trap occurred while running the wasm start function. - #[error("Trap occurred while invoking start function")] - StartTrap(Trap), } diff --git a/crates/test-programs/tests/wasm_tests/runtime.rs b/crates/test-programs/tests/wasm_tests/runtime.rs index 0157a9c61400..3945b5d047d9 100644 --- a/crates/test-programs/tests/wasm_tests/runtime.rs +++ b/crates/test-programs/tests/wasm_tests/runtime.rs @@ -65,17 +65,14 @@ pub fn instantiate( }) .collect::, _>>()?; - let instance = Instance::new(&module, &imports).context(format!( + let instance = Instance::new_wasi_abi(&module, &imports).context(format!( "error while instantiating Wasm module '{}'", bin_name, ))?; - instance - .get_export("_start") - .context("expected a _start export")? - .into_func() - .context("expected export to be a func")? - .call(&[])?; + if instance.is_some() { + bail!("expected module to be a command with a \"_start\" function") + } Ok(()) } diff --git a/crates/wasi-common/wig/src/wasi.rs b/crates/wasi-common/wig/src/wasi.rs index 39e263c7a67a..e389f83b1c5a 100644 --- a/crates/wasi-common/wig/src/wasi.rs +++ b/crates/wasi-common/wig/src/wasi.rs @@ -209,6 +209,7 @@ pub fn define_struct(args: TokenStream) -> TokenStream { let memory = match caller.get_export("memory") { Some(wasmtime::Extern::Memory(m)) => m, _ => { + log::warn!("callee does not export a memory as \"memory\""); let e = wasi_common::old::snapshot_0::wasi::__WASI_ERRNO_INVAL; #handle_early_error } @@ -463,6 +464,7 @@ pub fn define_struct_for_wiggle(args: TokenStream) -> TokenStream { let mem = match caller.get_export("memory") { Some(wasmtime::Extern::Memory(m)) => m, _ => { + log::warn!("callee does not export a memory as \"memory\""); let e = wasi_common::wasi::Errno::Inval; #handle_early_error } diff --git a/crates/wasi/src/lib.rs b/crates/wasi/src/lib.rs index e92b1d21d99f..17b359edab95 100644 --- a/crates/wasi/src/lib.rs +++ b/crates/wasi/src/lib.rs @@ -1,4 +1,6 @@ -use wasmtime::Trap; +use anyhow::Result; +use std::fs::File; +use wasmtime::{Linker, Store, Trap}; pub mod old; @@ -28,3 +30,38 @@ fn wasi_proc_exit(status: i32) -> Result<(), Trap> { )) } } + +/// Creates a new [`Linker`], similar to `Linker::new`, and initializes it +/// with WASI exports. +pub fn wasi_linker( + store: &Store, + preopen_dirs: &[(String, File)], + argv: &[String], + vars: &[(String, String)], +) -> Result { + let mut linker = Linker::new(store); + + let mut cx = WasiCtxBuilder::new(); + cx.inherit_stdio().args(argv).envs(vars); + + for (name, file) in preopen_dirs { + cx.preopened_dir(file.try_clone()?, name); + } + + let cx = cx.build()?; + let wasi = Wasi::new(linker.store(), cx); + wasi.add_to_linker(&mut linker)?; + + let mut cx = old::snapshot_0::WasiCtxBuilder::new(); + cx.inherit_stdio().args(argv).envs(vars); + + for (name, file) in preopen_dirs { + cx.preopened_dir(file.try_clone()?, name); + } + + let cx = cx.build()?; + let wasi = old::snapshot_0::Wasi::new(linker.store(), cx); + wasi.add_to_linker(&mut linker)?; + + Ok(linker) +} diff --git a/crates/wasmtime/src/instance.rs b/crates/wasmtime/src/instance.rs index a048eecaf5db..c43e2d96c1a7 100644 --- a/crates/wasmtime/src/instance.rs +++ b/crates/wasmtime/src/instance.rs @@ -51,9 +51,7 @@ fn instantiate( ) .map_err(|e| -> Error { match e { - InstantiationError::StartTrap(trap) | InstantiationError::Trap(trap) => { - Trap::from_runtime(trap).into() - } + InstantiationError::Trap(trap) => Trap::from_runtime(trap).into(), other => other.into(), } })?; @@ -101,6 +99,54 @@ pub struct Instance { } impl Instance { + /// Creates a new [`Instance`] from the previously compiled [`Module`] and + /// list of `imports` specified, similar to `Instance::new`, and performs + /// [WASI ABI initialization]: + /// - If the module is a command, the `_start` function is run and `None` + /// is returned. + /// - If the module is a reactor, the `_initialize` function is run and + /// the initialized `Instance` is returned. + /// + /// [WASI ABI initialization]: https://github.com/WebAssembly/WASI/blob/master/design/application-abi.md#current-unstable-abi + pub fn new_wasi_abi(module: &Module, imports: &[Extern]) -> Result, Error> { + let instance = Instance::new(module, imports)?; + + // Invoke the WASI start function of the instance, if one is present. + let command_start = instance.get_export("_start"); + let reactor_start = instance.get_export("_initialize"); + match (command_start, reactor_start) { + (Some(command_start), None) => { + if let Some(func) = command_start.into_func() { + func.get0::<()>()?()?; + + // For commands, we consume the instance after running the program. + Ok(None) + } else { + bail!("_start must be a function".to_owned()) + } + } + (None, Some(reactor_start)) => { + if let Some(func) = reactor_start.into_func() { + func.get0::<()>()?()?; + + // For reactors, we return the instance after running the initialization. + Ok(Some(instance)) + } else { + bail!("_initialize must be a function".to_owned()) + } + } + (None, None) => { + // Treat modules which don't declare themselves as commands or reactors as + // reactors which have no initialization to do. + Ok(Some(instance)) + } + (Some(_), Some(_)) => { + // Module declares to be both a command and a reactor. + bail!("Program cannot be both a command and a reactor".to_owned()) + } + } + } + /// Creates a new [`Instance`] from the previously compiled [`Module`] and /// list of `imports` specified. /// @@ -111,6 +157,10 @@ impl Instance { /// automatically run (if provided) and then the [`Instance`] will be /// returned. /// + /// Note that this function does not perform `WASI` ABI initialization + /// (eg. it does not run the `_start` or `_initialize` functions). To + /// perform them, use `new_wasi_abi` instead. + /// /// ## Providing Imports /// /// The `imports` array here is a bit tricky. The entries in the list of diff --git a/crates/wasmtime/src/linker.rs b/crates/wasmtime/src/linker.rs index 888ad1cffb4c..eee96c01af9b 100644 --- a/crates/wasmtime/src/linker.rs +++ b/crates/wasmtime/src/linker.rs @@ -66,6 +66,9 @@ impl Linker { /// linker will be connected with `store` and must come from the same /// `store`. /// + /// To create a new [`Linker`] prepopulated with WASI APIs, see + /// [`wasi_linker`](wasmtime_wasi::wasi_linker). + /// /// # Examples /// /// ``` @@ -338,6 +341,20 @@ impl Linker { idx } + /// Attempts to instantiate the `module` provided, similar to + /// `Linker::instantiate`, and performs [WASI ABI initialization]: + /// - If the module is a command, the `_start` function is run and `None` + /// is returned. + /// - If the module is a reactor, the `_initialize` function is run and + /// the initialized `Instance` is returned. + /// + /// [WASI ABI initialization]: https://github.com/WebAssembly/WASI/blob/master/design/application-abi.md#current-unstable-abi + pub fn instantiate_wasi_abi(&self, module: &Module) -> Result> { + let imports = self.compute_imports(module)?; + + Instance::new_wasi_abi(module, &imports) + } + /// Attempts to instantiate the `module` provided. /// /// This method will attempt to assemble a list of imports that correspond @@ -376,7 +393,14 @@ impl Linker { /// # } /// ``` pub fn instantiate(&self, module: &Module) -> Result { + let imports = self.compute_imports(module)?; + + Instance::new(module, &imports) + } + + fn compute_imports(&self, module: &Module) -> Result> { let mut imports = Vec::new(); + for import in module.imports() { if let Some(item) = self.get(&import) { imports.push(item); @@ -413,7 +437,7 @@ impl Linker { ) } - Instance::new(module, &imports) + Ok(imports) } /// Returns the [`Store`] that this linker is connected to. diff --git a/examples/wasi/main.rs b/examples/wasi/main.rs index 8971364cd1b2..2e999a9fc061 100644 --- a/examples/wasi/main.rs +++ b/examples/wasi/main.rs @@ -5,36 +5,18 @@ use anyhow::Result; use wasmtime::*; -use wasmtime_wasi::{Wasi, WasiCtx}; +use wasmtime_wasi::wasi_linker; fn main() -> Result<()> { let store = Store::default(); let module = Module::from_file(&store, "target/wasm32-wasi/debug/wasi.wasm")?; - // Create an instance of `Wasi` which contains a `WasiCtx`. Note that - // `WasiCtx` provides a number of ways to configure what the target program - // will have access to. - let wasi = Wasi::new(&store, WasiCtx::new(std::env::args())?); - let mut imports = Vec::new(); - for import in module.imports() { - if import.module() == "wasi_snapshot_preview1" { - if let Some(export) = wasi.get_export(import.name()) { - imports.push(Extern::from(export.clone())); - continue; - } - } - panic!( - "couldn't find import for `{}::{}`", - import.module(), - import.name() - ); - } + // Create a new `Linker` with no preloaded directories, command-line arguments, + // or environment variables. + let mut linker = wasi_linker(&store, &[], &[], &[])?; + + // Instanciate and run our module with the imports we've created. + let _instance = linker.instantiate_wasi_abi(&module)?; - // Instance our module with the imports we've created, then we can run the - // standard wasi `_start` function. - let instance = Instance::new(&module, &imports)?; - let start = instance.get_func("_start").unwrap(); - let start = start.get0::<()>()?; - start()?; Ok(()) } diff --git a/src/commands/run.rs b/src/commands/run.rs index d253402097f5..29b549c9ed38 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -7,13 +7,13 @@ use std::time::Duration; use std::{ ffi::{OsStr, OsString}, fs::File, - path::{Component, Path, PathBuf}, + path::{Component, PathBuf}, process, }; use structopt::{clap::AppSettings, StructOpt}; use wasi_common::preopen_dir; -use wasmtime::{Engine, Instance, Module, Store, Trap, Val, ValType}; -use wasmtime_wasi::{old::snapshot_0::Wasi as WasiSnapshot0, Wasi}; +use wasmtime::{Engine, Instance, Linker, Module, Store, Trap, Val, ValType}; +use wasmtime_wasi::wasi_linker; fn parse_module(s: &OsStr) -> Result { // Do not accept wasmtime subcommand names as the module name @@ -51,6 +51,14 @@ fn parse_dur(s: &str) -> Result { Ok(dur) } +fn parse_preloads(s: &str) -> Result<(String, PathBuf)> { + let parts: Vec<&str> = s.splitn(2, '=').collect(); + if parts.len() != 2 { + bail!("must contain exactly one equals character ('=')"); + } + Ok((parts[0].into(), parts[1].into())) +} + /// Runs a WebAssembly module #[derive(StructOpt)] #[structopt(name = "run", setting = AppSettings::TrailingVarArg)] @@ -87,10 +95,10 @@ pub struct RunCommand { #[structopt( long = "preload", number_of_values = 1, - value_name = "MODULE_PATH", - parse(from_os_str) + value_name = "NAME::MODULE_PATH", + parse(try_from_str = parse_preloads) )] - preloads: Vec, + preloads: Vec<(String, PathBuf)>, /// Maximum execution time of wasm code before timing out (1, 2s, 100ms, etc) #[structopt( @@ -127,17 +135,35 @@ impl RunCommand { let preopen_dirs = self.compute_preopen_dirs()?; let argv = self.compute_argv(); - let module_registry = ModuleRegistry::new(&store, &preopen_dirs, &argv, &self.vars)?; + let mut linker = wasi_linker(&store, &preopen_dirs, &argv, &self.vars)?; // 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()))?; + for (name, path) in self.preloads.iter() { + // Read the wasm module binary either as `*.wat` or a raw binary + let module = Module::from_file(linker.store(), path)?; + let instance = linker + .instantiate_wasi_abi(&module) + .context(format!("failed to instantiate {:?}", path))?; + + // If it was a command, don't register it. + let instance = if let Some(instance) = instance { + instance + } else { + continue; + }; + + linker.instance(name, &instance).with_context(|| { + format!( + "failed to process preload `{}` at `{}`", + name, + path.display() + ) + })?; } // Load the main wasm module. match self - .handle_module(&store, &module_registry) + .load_main_module(&mut linker) .with_context(|| format!("failed to run main module `{}`", self.module.display())) { Ok(()) => (), @@ -220,67 +246,31 @@ impl RunCommand { result } - fn instantiate_module( - store: &Store, - module_registry: &ModuleRegistry, - path: &Path, - ) -> Result { - // Read the wasm module binary either as `*.wat` or a raw binary - let data = wat::parse_file(path)?; - - let module = Module::new(store, &data)?; - - // Resolve import using module_registry. - let imports = module - .imports() - .map(|i| { - let export = match i.module() { - "wasi_snapshot_preview1" => { - module_registry.wasi_snapshot_preview1.get_export(i.name()) - } - "wasi_unstable" => module_registry.wasi_unstable.get_export(i.name()), - other => bail!("import module `{}` was not found", other), - }; - match export { - Some(export) => Ok(export.clone().into()), - None => bail!( - "import `{}` was not found in module `{}`", - i.name(), - i.module() - ), - } - }) - .collect::, _>>()?; - - let instance = Instance::new(&module, &imports) - .context(format!("failed to instantiate {:?}", path))?; - - Ok(instance) - } - - fn handle_module(&self, store: &Store, module_registry: &ModuleRegistry) -> Result<()> { + fn load_main_module(&self, linker: &mut Linker) -> Result<()> { if let Some(timeout) = self.wasm_timeout { - let handle = store.interrupt_handle()?; + let handle = linker.store().interrupt_handle()?; thread::spawn(move || { thread::sleep(timeout); handle.interrupt(); }); } - let instance = Self::instantiate_module(store, module_registry, &self.module)?; + + // Read the wasm module binary either as `*.wat` or a raw binary + let module = Module::from_file(linker.store(), &self.module)?; + let instance = linker + .instantiate_wasi_abi(&module) + .context(format!("failed to instantiate {:?}", self.module))?; // If a function to invoke was given, invoke it. if let Some(name) = self.invoke.as_ref() { - self.invoke_export(instance, name)?; - } else if instance.exports().any(|export| export.name().is_empty()) { - // Launch the default command export. - self.invoke_export(instance, "")?; + if let Some(instance) = instance { + self.invoke_export(instance, name) + } else { + bail!("Cannot invoke exports on a command after it has executed") + } } else { - // If the module doesn't have a default command export, launch the - // _start function if one is present, as a compatibility measure. - self.invoke_export(instance, "_start")?; + Ok(()) } - - Ok(()) } fn invoke_export(&self, instance: Instance, name: &str) -> Result<()> { @@ -346,42 +336,3 @@ impl RunCommand { Ok(()) } } - -struct ModuleRegistry { - wasi_snapshot_preview1: Wasi, - wasi_unstable: WasiSnapshot0, -} - -impl ModuleRegistry { - fn new( - store: &Store, - preopen_dirs: &[(String, File)], - argv: &[String], - vars: &[(String, String)], - ) -> Result { - let mut cx1 = wasi_common::WasiCtxBuilder::new(); - - cx1.inherit_stdio().args(argv).envs(vars); - - for (name, file) in preopen_dirs { - cx1.preopened_dir(file.try_clone()?, name); - } - - let cx1 = cx1.build()?; - - let mut cx2 = wasi_common::old::snapshot_0::WasiCtxBuilder::new(); - - cx2.inherit_stdio().args(argv).envs(vars); - - for (name, file) in preopen_dirs { - cx2.preopened_dir(file.try_clone()?, name); - } - - let cx2 = cx2.build()?; - - Ok(ModuleRegistry { - wasi_snapshot_preview1: Wasi::new(store, cx1), - wasi_unstable: WasiSnapshot0::new(store, cx2), - }) - } -} diff --git a/tests/all/cli_tests.rs b/tests/all/cli_tests.rs index c8edbd1fb6e5..d521da8bbb28 100644 --- a/tests/all/cli_tests.rs +++ b/tests/all/cli_tests.rs @@ -256,3 +256,86 @@ fn exit126_wasi_snapshot1() -> Result<()> { assert!(String::from_utf8_lossy(&output.stderr).contains("invalid exit status")); Ok(()) } + +// Run a minimal command program. +#[test] +fn minimal_command() -> Result<()> { + let wasm = build_wasm("tests/wasm/minimal-command.wat")?; + let stdout = run_wasmtime(&[wasm.path().to_str().unwrap(), "--disable-cache"])?; + assert_eq!(stdout, ""); + Ok(()) +} + +// Run a minimal reactor program. +#[test] +fn minimal_reactor() -> Result<()> { + let wasm = build_wasm("tests/wasm/minimal-reactor.wat")?; + let stdout = run_wasmtime(&[wasm.path().to_str().unwrap(), "--disable-cache"])?; + assert_eq!(stdout, ""); + Ok(()) +} + +// Attempt to call invoke on a command. +#[test] +fn command_invoke() -> Result<()> { + let wasm = build_wasm("tests/wasm/minimal-command.wat")?; + assert!( + run_wasmtime(&[ + "run", + wasm.path().to_str().unwrap(), + "--invoke", + "_start", + "--disable-cache", + ]) + .is_err(), + "shall fail" + ); + Ok(()) +} + +// Attempt to call invoke on a command. +#[test] +fn reactor_invoke() -> Result<()> { + let wasm = build_wasm("tests/wasm/minimal-reactor.wat")?; + run_wasmtime(&[ + "run", + wasm.path().to_str().unwrap(), + "--invoke", + "_initialize", + "--disable-cache", + ])?; + Ok(()) +} + +// Run the greeter test, which runs a preloaded reactor and a command. +#[test] +fn greeter() -> Result<()> { + let wasm = build_wasm("tests/wasm/greeter_command.wat")?; + let stdout = run_wasmtime(&[ + "run", + wasm.path().to_str().unwrap(), + "--disable-cache", + "--preload", + "reactor=tests/wasm/greeter_reactor.wat", + ])?; + assert_eq!( + stdout, + "Hello _initialize\nHello _start\nHello greet\nHello done\n" + ); + Ok(()) +} + +// Run the greeter test, but this time preload a command. +#[test] +fn greeter_preload_command() -> Result<()> { + let wasm = build_wasm("tests/wasm/greeter_reactor.wat")?; + let stdout = run_wasmtime(&[ + "run", + wasm.path().to_str().unwrap(), + "--disable-cache", + "--preload", + "reactor=tests/wasm/hello_wasi_snapshot1.wat", + ])?; + assert_eq!(stdout, "Hello, world!\nHello _initialize\n"); + Ok(()) +} diff --git a/tests/wasm/greeter_command.wat b/tests/wasm/greeter_command.wat new file mode 100644 index 000000000000..b80d197c5a40 --- /dev/null +++ b/tests/wasm/greeter_command.wat @@ -0,0 +1,22 @@ +(module + (import "wasi_snapshot_preview1" "fd_write" + (func $__wasi_fd_write (param i32 i32 i32 i32) (result i32))) + (import "reactor" "greet" (func $greet)) + (func (export "_start") + (call $print (i32.const 32) (i32.const 13)) + (call $greet) + (call $print (i32.const 64) (i32.const 11)) + ) + (func $print (param $ptr i32) (param $len i32) + (i32.store (i32.const 8) (local.get $len)) + (i32.store (i32.const 4) (local.get $ptr)) + (drop (call $__wasi_fd_write + (i32.const 1) + (i32.const 4) + (i32.const 1) + (i32.const 0))) + ) + (memory (export "memory") 1) + (data (i32.const 32) "Hello _start\0a") + (data (i32.const 64) "Hello done\0a") +) diff --git a/tests/wasm/greeter_reactor.wat b/tests/wasm/greeter_reactor.wat new file mode 100644 index 000000000000..2f0914f8be56 --- /dev/null +++ b/tests/wasm/greeter_reactor.wat @@ -0,0 +1,22 @@ +(module + (import "wasi_snapshot_preview1" "fd_write" + (func $__wasi_fd_write (param i32 i32 i32 i32) (result i32))) + (func (export "_initialize") + (call $print (i32.const 32) (i32.const 18)) + ) + (func (export "greet") + (call $print (i32.const 64) (i32.const 12)) + ) + (func $print (param $ptr i32) (param $len i32) + (i32.store (i32.const 8) (local.get $len)) + (i32.store (i32.const 4) (local.get $ptr)) + (drop (call $__wasi_fd_write + (i32.const 1) + (i32.const 4) + (i32.const 1) + (i32.const 0))) + ) + (memory (export "memory") 1) + (data (i32.const 32) "Hello _initialize\0a") + (data (i32.const 64) "Hello greet\0a") +) diff --git a/tests/wasm/minimal-command.wat b/tests/wasm/minimal-command.wat new file mode 100644 index 000000000000..6e9f4f5ffd3a --- /dev/null +++ b/tests/wasm/minimal-command.wat @@ -0,0 +1,3 @@ +(module + (func (export "_start")) +) diff --git a/tests/wasm/minimal-reactor.wat b/tests/wasm/minimal-reactor.wat new file mode 100644 index 000000000000..bf7bd2148be0 --- /dev/null +++ b/tests/wasm/minimal-reactor.wat @@ -0,0 +1,3 @@ +(module + (func (export "_initialize")) +)