From 3715e19c67ab7e59fadf750a1de2ba48f1e6f4ce Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Tue, 26 May 2020 08:39:40 -0700 Subject: [PATCH] Reactor support. (#1565) * 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. * Add comments and fix a typo in a comment. * Fix a rustdoc warning. * Tidy an unneeded `mut`. * Factor out instance initialization with `NewInstance`. This also separates instantiation from initialization in a manner similar to https://github.com/bytecodealliance/lucet/pull/506. * Update fuzzing oracles for the API changes. * Remove `wasi_linker` and clarify that Commands/Reactors aren't connected to WASI. * Move Command/Reactor semantics into the Linker. * C API support. * Fix fuzzer build. * Update usage syntax from "::" to "=". * Remove `NewInstance` and `start()`. * Elaborate on Commands and Reactors and add a spec link. * Add more comments. * Fix wat syntax. * Fix wat. * Use the `Debug` formatter to format an anyhow::Error. * Fix wat. --- RELEASES.md | 5 +- crates/c-api/include/wasmtime.h | 12 + crates/c-api/src/linker.rs | 34 ++- crates/runtime/src/instance.rs | 4 - .../test-programs/tests/wasm_tests/runtime.rs | 41 +-- crates/wasi-common/wig/src/wasi.rs | 2 + crates/wasmtime/src/instance.rs | 83 +++--- crates/wasmtime/src/linker.rs | 278 +++++++++++++++++- crates/wasmtime/src/module.rs | 51 +++- crates/wasmtime/src/types.rs | 3 +- examples/wasi/main.c | 57 ++-- examples/wasi/main.rs | 28 +- src/commands/run.rs | 196 ++++++------ tests/all/cli_tests.rs | 94 ++++++ tests/wasm/greeter_callable_command.wat | 23 ++ tests/wasm/greeter_command.wat | 22 ++ tests/wasm/greeter_reactor.wat | 22 ++ tests/wasm/minimal-command.wat | 3 + tests/wasm/minimal-reactor.wat | 3 + 19 files changed, 720 insertions(+), 241 deletions(-) create mode 100644 tests/wasm/greeter_callable_command.wat 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..0ec09eae814a 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -8,7 +8,10 @@ Unreleased ### Added -### Changed +* The [Commands and Reactors ABI] is now supported in the Rust API. `Linker::module` + loads a module and automatically handles Commands and Reactors semantics. + +[Commands and Reactors ABI]: https://github.com/WebAssembly/WASI/blob/master/design/application-abi.md#current-unstable-abi ### Fixed diff --git a/crates/c-api/include/wasmtime.h b/crates/c-api/include/wasmtime.h index ec8ba8897719..1b43f97422fd 100644 --- a/crates/c-api/include/wasmtime.h +++ b/crates/c-api/include/wasmtime.h @@ -117,6 +117,18 @@ WASM_API_EXTERN own wasmtime_error_t* wasmtime_linker_instantiate( own wasm_trap_t **trap ); +WASM_API_EXTERN own wasmtime_error_t* wasmtime_linker_module( + const wasmtime_linker_t *linker, + const wasm_name_t *name, + const wasm_module_t *module +); + +WASM_API_EXTERN own wasmtime_error_t* wasmtime_linker_get_default( + const wasmtime_linker_t *linker, + const wasm_name_t *name, + own wasm_func_t **func +); + /////////////////////////////////////////////////////////////////////////////// // // wasmtime_caller_t extension, binding the `Caller` type in the Rust API diff --git a/crates/c-api/src/linker.rs b/crates/c-api/src/linker.rs index 4faee0de8499..3578544ef288 100644 --- a/crates/c-api/src/linker.rs +++ b/crates/c-api/src/linker.rs @@ -1,8 +1,8 @@ use crate::{bad_utf8, handle_result, wasmtime_error_t}; use crate::{wasm_extern_t, wasm_store_t, ExternHost}; -use crate::{wasm_instance_t, wasm_module_t, wasm_name_t, wasm_trap_t}; +use crate::{wasm_func_t, wasm_instance_t, wasm_module_t, wasm_name_t, wasm_trap_t}; use std::str; -use wasmtime::{Extern, Linker}; +use wasmtime::{Extern, HostRef, Linker}; #[repr(C)] pub struct wasmtime_linker_t { @@ -89,3 +89,33 @@ pub unsafe extern "C" fn wasmtime_linker_instantiate( let result = linker.linker.instantiate(&module.module.borrow()); super::instance::handle_instantiate(result, instance_ptr, trap_ptr) } + +#[no_mangle] +pub unsafe extern "C" fn wasmtime_linker_module( + linker: &mut wasmtime_linker_t, + name: &wasm_name_t, + module: &wasm_module_t, +) -> Option> { + let linker = &mut linker.linker; + let name = match str::from_utf8(name.as_slice()) { + Ok(s) => s, + Err(_) => return bad_utf8(), + }; + handle_result(linker.module(name, &module.module.borrow()), |_linker| ()) +} + +#[no_mangle] +pub unsafe extern "C" fn wasmtime_linker_get_default( + linker: &mut wasmtime_linker_t, + name: &wasm_name_t, + func: &mut *mut wasm_func_t, +) -> Option> { + let linker = &mut linker.linker; + let name = match str::from_utf8(name.as_slice()) { + Ok(s) => s, + Err(_) => return bad_utf8(), + }; + handle_result(linker.get_default(name), |f| { + *func = Box::into_raw(Box::new(HostRef::new(f).into())) + }) +} 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..de465d1309cd 100644 --- a/crates/test-programs/tests/wasm_tests/runtime.rs +++ b/crates/test-programs/tests/wasm_tests/runtime.rs @@ -1,8 +1,8 @@ -use anyhow::{bail, Context}; +use anyhow::Context; use std::fs::File; use std::path::Path; use wasi_common::VirtualDirEntry; -use wasmtime::{Instance, Module, Store}; +use wasmtime::{Linker, Module, Store}; #[derive(Clone, Copy, Debug)] pub enum PreopenType { @@ -48,36 +48,19 @@ pub fn instantiate( let (reader, _writer) = os_pipe::pipe()?; builder.stdin(reader_to_file(reader)); let snapshot1 = wasmtime_wasi::Wasi::new(&store, builder.build()?); - let module = Module::new(&store, &data).context("failed to create wasm module")?; - let imports = module - .imports() - .map(|i| { - let field_name = i.name(); - if let Some(export) = snapshot1.get_export(field_name) { - Ok(export.clone().into()) - } else { - bail!( - "import {} was not found in module {}", - field_name, - i.module() - ) - } - }) - .collect::, _>>()?; - let instance = Instance::new(&module, &imports).context(format!( - "error while instantiating Wasm module '{}'", - bin_name, - ))?; + let mut linker = Linker::new(&store); - instance - .get_export("_start") - .context("expected a _start export")? - .into_func() - .context("expected export to be a func")? - .call(&[])?; + snapshot1.add_to_linker(&mut linker)?; + + let module = Module::new(&store, &data).context("failed to create wasm module")?; - Ok(()) + linker + .module("", &module) + .and_then(|m| m.get_default("")) + .and_then(|f| f.get0::<()>()) + .and_then(|f| f().map_err(Into::into)) + .context(format!("error while testing Wasm module '{}'", bin_name,)) } #[cfg(unix)] diff --git a/crates/wasi-common/wig/src/wasi.rs b/crates/wasi-common/wig/src/wasi.rs index 579e6f3ca18d..479cb2d867f8 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/wasmtime/src/instance.rs b/crates/wasmtime/src/instance.rs index a048eecaf5db..eff5139b7597 100644 --- a/crates/wasmtime/src/instance.rs +++ b/crates/wasmtime/src/instance.rs @@ -26,9 +26,25 @@ fn instantiate( sig_registry: &SignatureRegistry, host: Box, ) -> Result { + // For now we have a restriction that the `Store` that we're working + // with is the same for everything involved here. + for import in imports { + if !import.comes_from_same_store(store) { + bail!("cross-`Store` instantiation is not currently supported"); + } + } + + if imports.len() != compiled_module.module().imports.len() { + bail!( + "wrong number of imports provided, {} != {}", + imports.len(), + compiled_module.module().imports.len() + ); + } + let mut resolver = SimpleResolver { imports }; - unsafe { - let config = store.engine().config(); + let config = store.engine().config(); + let instance = unsafe { let instance = compiled_module.instantiate( &mut resolver, sig_registry, @@ -51,31 +67,38 @@ 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(), } })?; - // If a start function is present, now that we've got our compiled - // instance we can invoke it. Make sure we use all the trap-handling - // configuration in `store` as well. - if let Some(start) = instance.module().start_func { - let f = match instance.lookup_by_declaration(&EntityIndex::Function(start)) { - wasmtime_runtime::Export::Function(f) => f, - _ => unreachable!(), // valid modules shouldn't hit this - }; - super::func::catch_traps(instance.vmctx_ptr(), store, || { + instance + }; + + let start_func = instance.handle.module().start_func; + + // If a start function is present, invoke it. Make sure we use all the + // trap-handling configuration in `store` as well. + if let Some(start) = start_func { + let f = match instance + .handle + .lookup_by_declaration(&EntityIndex::Function(start)) + { + wasmtime_runtime::Export::Function(f) => f, + _ => unreachable!(), // valid modules shouldn't hit this + }; + let vmctx_ptr = instance.handle.vmctx_ptr(); + unsafe { + super::func::catch_traps(vmctx_ptr, store, || { mem::transmute::< *const VMFunctionBody, unsafe extern "C" fn(*mut VMContext, *mut VMContext), - >(f.address)(f.vmctx, instance.vmctx_ptr()) + >(f.address)(f.vmctx, vmctx_ptr) })?; } - - Ok(instance) } + + Ok(instance) } /// An instantiated WebAssembly module. @@ -111,6 +134,15 @@ impl Instance { /// automatically run (if provided) and then the [`Instance`] will be /// returned. /// + /// Per the WebAssembly spec, instantiation includes running the module's + /// start function, if it has one (not to be confused with the `_start` + /// function, which is not run). + /// + /// Note that this is a low-level function that just performance an + /// instantiation. See the `Linker` struct for an API which provides a + /// convenient way to link imports and provides automatic Command and Reactor + /// behavior. + /// /// ## Providing Imports /// /// The `imports` array here is a bit tricky. The entries in the list of @@ -147,23 +179,6 @@ impl Instance { /// [`ExternType`]: crate::ExternType pub fn new(module: &Module, imports: &[Extern]) -> Result { let store = module.store(); - - // For now we have a restriction that the `Store` that we're working - // with is the same for everything involved here. - for import in imports { - if !import.comes_from_same_store(store) { - bail!("cross-`Store` instantiation is not currently supported"); - } - } - - if imports.len() != module.imports().len() { - bail!( - "wrong number of imports provided, {} != {}", - imports.len(), - module.imports().len() - ); - } - let info = module.register_frame_info(); let handle = instantiate( store, diff --git a/crates/wasmtime/src/linker.rs b/crates/wasmtime/src/linker.rs index 888ad1cffb4c..ed9aacdd5676 100644 --- a/crates/wasmtime/src/linker.rs +++ b/crates/wasmtime/src/linker.rs @@ -1,7 +1,8 @@ use crate::{ Extern, ExternType, Func, FuncType, GlobalType, ImportType, Instance, IntoFunc, Module, Store, + Trap, }; -use anyhow::{anyhow, bail, Result}; +use anyhow::{anyhow, bail, Context, Result}; use std::collections::hash_map::{Entry, HashMap}; use std::rc::Rc; @@ -270,6 +271,187 @@ impl Linker { Ok(self) } + /// Define automatic instantiations of a [`Module`] in this linker. + /// + /// This automatically handles [Commands and Reactors] instantiation and + /// initialization. + /// + /// Exported functions of a Command module may be called directly, however + /// instead of having a single instance which is reused for each call, + /// each call creates a new instance, which lives for the duration of the + /// call. The imports of the Command are resolved once, and reused for + /// each instantiation, so all dependencies need to be present at the time + /// when `Linker::module` is called. + /// + /// For Reactors, a single instance is created, and an initialization + /// function is called, and then its exports may be called. + /// + /// Ordinary modules which don't declare themselves to be either Commands + /// or Reactors are treated as Reactors without any initialization calls. + /// + /// [Commands and Reactors]: https://github.com/WebAssembly/WASI/blob/master/design/application-abi.md#current-unstable-abi + /// + /// # Errors + /// + /// Returns an error if the any item is redefined twice in this linker (for + /// example the same `module_name` was already defined) and shadowing is + /// disallowed, if `instance` comes from a different [`Store`] than this + /// [`Linker`] originally was created with, or if a Reactor initialization + /// function traps. + /// + /// # Examples + /// + /// ``` + /// # use wasmtime::*; + /// # fn main() -> anyhow::Result<()> { + /// # let store = Store::default(); + /// let mut linker = Linker::new(&store); + /// + /// // Instantiate a small instance and inform the linker that the name of + /// // this instance is `instance1`. This defines the `instance1::run` name + /// // for our next module to use. + /// let wat = r#"(module (func (export "run") ))"#; + /// let module = Module::new(&store, wat)?; + /// linker.module("instance1", &module)?; + /// + /// let wat = r#" + /// (module + /// (import "instance1" "run" (func $instance1_run)) + /// (func (export "run") + /// call $instance1_run + /// ) + /// ) + /// "#; + /// let module = Module::new(&store, wat)?; + /// let instance = linker.instantiate(&module)?; + /// # Ok(()) + /// # } + /// ``` + /// + /// For a Command, a new instance is created for each call. + /// + /// ``` + /// # use wasmtime::*; + /// # fn main() -> anyhow::Result<()> { + /// # let store = Store::default(); + /// let mut linker = Linker::new(&store); + /// + /// // Create a Command that attempts to count the number of times it is run, but is + /// // foiled by each call getting a new instance. + /// let wat = r#" + /// (module + /// (global $counter (mut i32) (i32.const 0)) + /// (func (export "_start") + /// (global.set $counter (i32.add (global.get $counter) (i32.const 1))) + /// ) + /// (func (export "read_counter") (result i32) + /// (global.get $counter) + /// ) + /// ) + /// "#; + /// let module = Module::new(&store, wat)?; + /// linker.module("commander", &module)?; + /// let run = linker.get_default("")?.get0::<()>()?; + /// run()?; + /// run()?; + /// run()?; + /// + /// let wat = r#" + /// (module + /// (import "commander" "_start" (func $commander_start)) + /// (import "commander" "read_counter" (func $commander_read_counter (result i32))) + /// (func (export "run") (result i32) + /// call $commander_start + /// call $commander_start + /// call $commander_start + /// call $commander_read_counter + /// ) + /// ) + /// "#; + /// let module = Module::new(&store, wat)?; + /// linker.module("", &module)?; + /// let count = linker.get_one_by_name("", "run")?.into_func().unwrap().get0::()?()?; + /// assert_eq!(count, 0, "a Command should get a fresh instance on each invocation"); + /// + /// # Ok(()) + /// # } + /// ``` + pub fn module(&mut self, module_name: &str, module: &Module) -> Result<&mut Self> { + match ModuleKind::categorize(module)? { + ModuleKind::Command => self.command(module_name, module), + ModuleKind::Reactor => { + let instance = self.instantiate(&module)?; + + if let Some(export) = instance.get_export("_initialize") { + if let Extern::Func(func) = export { + func.get0::<()>() + .and_then(|f| f().map_err(Into::into)) + .context("calling the Reactor initialization function")?; + } + } + + self.instance(module_name, &instance) + } + } + } + + fn command(&mut self, module_name: &str, module: &Module) -> Result<&mut Self> { + for export in module.exports() { + if let Some(func_ty) = export.ty().func() { + let imports = self.compute_imports(module)?; + let module = module.clone(); + let export_name = export.name().to_owned(); + let func = Func::new(&self.store, func_ty.clone(), move |_, params, results| { + // Create a new instance for this command execution. + let instance = Instance::new(&module, &imports).map_err(|error| match error + .downcast::() + { + Ok(trap) => trap, + Err(error) => Trap::new(format!("{:?}", error)), + })?; + + // `unwrap()` everything here because we know the instance contains a + // function export with the given name and signature because we're + // iterating over the module it was instantiated from. + let command_results = instance + .get_export(&export_name) + .unwrap() + .into_func() + .unwrap() + .call(params) + .map_err(|error| error.downcast::().unwrap())?; + + // Copy the return values into the output slice. + for (result, command_result) in + results.iter_mut().zip(command_results.into_vec()) + { + *result = command_result; + } + + Ok(()) + }); + self.insert(module_name, export.name(), Extern::Func(func))?; + } else if export.name() == "memory" && export.ty().memory().is_some() { + // Allow an exported "memory" memory for now. + } else if export.name() == "__indirect_function_table" && export.ty().table().is_some() + { + // Allow an exported "__indirect_function_table" table for now. + } else if export.name() == "__data_end" && export.ty().global().is_some() { + // Allow an exported "__data_end" memory for compatibility with toolchains + // which use --export-dynamic, which unfortunately doesn't work the way + // we want it to. + } else if export.name() == "__heap_base" && export.ty().global().is_some() { + // Allow an exported "__data_end" memory for compatibility with toolchains + // which use --export-dynamic, which unfortunately doesn't work the way + // we want it to. + } else { + bail!("command export '{}' is not a function", export.name()); + } + } + + Ok(self) + } + /// Aliases one module's name as another. /// /// This method will alias all currently defined under `module` to also be @@ -350,6 +532,10 @@ impl Linker { /// incorrect signature or if it was not prevoiusly defined then an error /// will be returned because the import can not be satisfied. /// + /// Per the WebAssembly spec, instantiation includes running the module's + /// start function, if it has one (not to be confused with the `_start` + /// function, which is not run). + /// /// # Errors /// /// This method can fail because an import may not be found, or because @@ -376,7 +562,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 +606,7 @@ impl Linker { ) } - Instance::new(module, &imports) + Ok(imports) } /// Returns the [`Store`] that this linker is connected to. @@ -485,4 +678,85 @@ impl Linker { } Ok(ret.clone()) } + + /// Returns the "default export" of a module. + /// + /// An export with an empty string is considered to be a "default export". + /// "_start" is also recognized for compatibility. + pub fn get_default(&self, module: &str) -> Result { + let mut items = self.get_by_name(module, ""); + if let Some(external) = items.next() { + if items.next().is_some() { + bail!("too many items named `` in `{}`", module); + } + if let Extern::Func(func) = external { + return Ok(func.clone()); + } + bail!("default export in '{}' is not a function", module); + } + + // For compatibility, also recognize "_start". + let mut items = self.get_by_name(module, "_start"); + if let Some(external) = items.next() { + if items.next().is_some() { + bail!("too many items named `_start` in `{}`", module); + } + if let Extern::Func(func) = external { + return Ok(func.clone()); + } + bail!("`_start` in '{}' is not a function", module); + } + + // Otherwise return a no-op function. + Ok(Func::new( + &self.store, + FuncType::new(Vec::new().into_boxed_slice(), Vec::new().into_boxed_slice()), + move |_, _, _| Ok(()), + )) + } +} + +/// Modules can be interpreted either as Commands (instance lifetime ends +/// when the start function returns) or Reactor (instance persists). +enum ModuleKind { + /// The instance is a Command, and this is its start function. The + /// instance should be consumed. + Command, + + /// The instance is a Reactor, and this is its initialization function, + /// along with the instance itself, which should persist. + Reactor, +} + +impl ModuleKind { + /// Determine whether the given module is a Command or a Reactor. + fn categorize(module: &Module) -> Result { + let command_start = module.get_export("_start"); + let reactor_start = module.get_export("_initialize"); + match (command_start, reactor_start) { + (Some(command_start), None) => { + if let Some(_) = command_start.func() { + Ok(ModuleKind::Command) + } else { + bail!("`_start` must be a function") + } + } + (None, Some(reactor_start)) => { + if let Some(_) = reactor_start.func() { + Ok(ModuleKind::Reactor) + } else { + bail!("`_initialize` must be a function") + } + } + (None, None) => { + // Module declares neither of the recognized functions, so treat + // it as a reactor with no initialization function. + Ok(ModuleKind::Reactor) + } + (Some(_), Some(_)) => { + // Module declares itself to be both a Command and a Reactor. + bail!("Program cannot be both a Command and a Reactor") + } + } + } } diff --git a/crates/wasmtime/src/module.rs b/crates/wasmtime/src/module.rs index c741d434e579..102b0df82dad 100644 --- a/crates/wasmtime/src/module.rs +++ b/crates/wasmtime/src/module.rs @@ -1,6 +1,6 @@ use crate::frame_info::GlobalFrameInfoRegistration; use crate::runtime::Store; -use crate::types::{EntityType, ExportType, ImportType}; +use crate::types::{EntityType, ExportType, ExternType, ImportType}; use anyhow::{Error, Result}; use std::path::Path; use std::sync::{Arc, Mutex}; @@ -478,6 +478,55 @@ impl Module { }) } + /// Looks up an export in this [`Module`] by name. + /// + /// This function will return the type of an export with the given name. + /// + /// # Examples + /// + /// There may be no export with that name: + /// + /// ``` + /// # use wasmtime::*; + /// # fn main() -> anyhow::Result<()> { + /// # let store = Store::default(); + /// let module = Module::new(&store, "(module)")?; + /// assert!(module.get_export("foo").is_none()); + /// # Ok(()) + /// # } + /// ``` + /// + /// When there is an export with that name, it is returned: + /// + /// ``` + /// # use wasmtime::*; + /// # fn main() -> anyhow::Result<()> { + /// # let store = Store::default(); + /// let wat = r#" + /// (module + /// (func (export "foo")) + /// (memory (export "memory") 1) + /// ) + /// "#; + /// let module = Module::new(&store, wat)?; + /// let foo = module.get_export("foo"); + /// assert!(foo.is_some()); + /// + /// let foo = foo.unwrap(); + /// match foo { + /// ExternType::Func(_) => { /* ... */ } + /// _ => panic!("unexpected export type!"), + /// } + /// + /// # Ok(()) + /// # } + /// ``` + pub fn get_export<'module>(&'module self, name: &'module str) -> Option { + let module = self.inner.compiled.module_ref(); + let entity_index = module.exports.get(name)?; + Some(EntityType::new(entity_index, module).extern_type()) + } + /// Returns the [`Store`] that this [`Module`] was compiled into. pub fn store(&self) -> &Store { &self.inner.store diff --git a/crates/wasmtime/src/types.rs b/crates/wasmtime/src/types.rs index 5fe37eeea143..e03c6e41c08f 100644 --- a/crates/wasmtime/src/types.rs +++ b/crates/wasmtime/src/types.rs @@ -419,7 +419,8 @@ impl<'module> EntityType<'module> { } } - fn extern_type(&self) -> ExternType { + /// Convert this `EntityType` to an `ExternType`. + pub(crate) fn extern_type(&self) -> ExternType { match self { EntityType::Function(sig) => FuncType::from_wasmtime_signature(sig) .expect("core wasm function type should be supported") diff --git a/examples/wasi/main.c b/examples/wasi/main.c index 1226f94ce21f..68a978ccd28a 100644 --- a/examples/wasi/main.c +++ b/examples/wasi/main.c @@ -72,51 +72,30 @@ int main() { if (wasi == NULL) exit_with_error("failed to instantiate WASI", NULL, trap); - // Create import list for our module using wasi - wasm_importtype_vec_t import_types; - wasm_module_imports(module, &import_types); - const wasm_extern_t **imports = calloc(import_types.size, sizeof(void*)); - assert(imports); - for (int i = 0; i < import_types.size; i++) { - const wasm_extern_t *binding = wasi_instance_bind_import(wasi, import_types.data[i]); - if (binding != NULL) { - imports[i] = binding; - } else { - printf("> Failed to satisfy import\n"); - exit(1); - } - } + wasmtime_linker_t *linker = wasmtime_linker_new(store); + error = wasmtime_linker_define_wasi(linker, wasi); + if (error != NULL) + exit_with_error("failed to link wasi", error, NULL); // Instantiate the module + wasm_name_t empty; + wasm_name_new_from_string(&empty, ""); wasm_instance_t *instance = NULL; - error = wasmtime_instance_new(module, imports, import_types.size, &instance, &trap); - if (instance == NULL) - exit_with_error("failed to instantiate", error, trap); - free(imports); - wasm_importtype_vec_delete(&import_types); + error = wasmtime_linker_module(linker, &empty, module); + if (error != NULL) + exit_with_error("failed to instantiate module", error, NULL); - // Lookup our `_start` export function - wasm_extern_vec_t externs; - wasm_instance_exports(instance, &externs); - wasm_exporttype_vec_t exports; - wasm_module_exports(module, &exports); - wasm_extern_t *start_extern = NULL; - for (int i = 0; i < exports.size; i++) { - const wasm_name_t *name = wasm_exporttype_name(exports.data[i]); - if (strncmp(name->data, "_start", name->size) == 0) - start_extern = externs.data[i]; - } - assert(start_extern); - wasm_func_t *start = wasm_extern_as_func(start_extern); - assert(start != NULL); - error = wasmtime_func_call(start, NULL, 0, NULL, 0, &trap); - if (error != NULL || trap != NULL) - exit_with_error("failed to call `_start`", error, trap); + // Run it. + wasm_func_t* func; + wasmtime_linker_get_default(linker, &empty, &func); + if (error != NULL) + exit_with_error("failed to locate default export for module", error, NULL); + error = wasmtime_func_call(func, NULL, 0, NULL, 0, &trap); + if (error != NULL) + exit_with_error("error calling default export", error, trap); // Clean up after ourselves at this point - wasm_exporttype_vec_delete(&exports); - wasm_extern_vec_delete(&externs); - wasm_instance_delete(instance); + wasm_name_delete(&empty); wasm_module_delete(module); wasm_store_delete(store); wasm_engine_delete(engine); diff --git a/examples/wasi/main.rs b/examples/wasi/main.rs index 8971364cd1b2..c663e9527751 100644 --- a/examples/wasi/main.rs +++ b/examples/wasi/main.rs @@ -9,32 +9,18 @@ use wasmtime_wasi::{Wasi, WasiCtx}; fn main() -> Result<()> { let store = Store::default(); - let module = Module::from_file(&store, "target/wasm32-wasi/debug/wasi.wasm")?; + let mut linker = Linker::new(&store); // 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() - ); - } + wasi.add_to_linker(&mut linker)?; + + // Instantiate our module with the imports we've created, and run it. + let module = Module::from_file(&store, "target/wasm32-wasi/debug/wasi.wasm")?; + linker.module("", &module)?; + linker.get_default("")?.get0::<()>()?()?; - // 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 e638606b82b9..0e3d3e466111 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 wasi_common::{preopen_dir, WasiCtxBuilder}; +use wasmtime::{Engine, Func, Linker, Module, Store, Trap, Val, ValType}; +use wasmtime_wasi::Wasi; 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,25 @@ 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 = Linker::new(&store); + populate_with_wasi(&mut linker, &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)?; + + // Add the module's functions to the linker. + linker.module(name, &module).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,79 +236,40 @@ 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. + // Use "" as a default module name. + let module = Module::from_file(linker.store(), &self.module)?; + linker + .module("", &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, "")?; + self.invoke_export(linker, name) } 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")?; + let func = linker.get_default("")?; + self.invoke_func(func, None) } - - Ok(()) } - fn invoke_export(&self, instance: Instance, name: &str) -> Result<()> { - let func = if let Some(export) = instance.get_export(name) { - if let Some(func) = export.into_func() { - func - } else { - bail!("export of `{}` wasn't a function", name) - } - } else { - bail!("failed to find export of `{}` in module", name) + fn invoke_export(&self, linker: &Linker, name: &str) -> Result<()> { + let func = match linker.get_one_by_name("", name)?.into_func() { + Some(func) => func, + None => bail!("export of `{}` wasn't a function", name), }; + self.invoke_func(func, Some(name)) + } + + fn invoke_func(&self, func: Func, name: Option<&str>) -> Result<()> { let ty = func.ty(); if ty.params().len() > 0 { eprintln!( @@ -305,7 +282,13 @@ impl RunCommand { for ty in ty.params() { let val = match args.next() { Some(s) => s, - None => bail!("not enough arguments for `{}`", name), + None => { + if let Some(name) = name { + bail!("not enough arguments for `{}`", name) + } else { + bail!("not enough arguments for command default") + } + } }; values.push(match ty { // TODO: integer parsing here should handle hexadecimal notation @@ -321,9 +304,13 @@ impl RunCommand { // Invoke the function and then afterwards print all the results that came // out, if there are any. - let results = func - .call(&values) - .with_context(|| format!("failed to invoke `{}`", name))?; + let results = func.call(&values).with_context(|| { + if let Some(name) = name { + format!("failed to invoke `{}`", name) + } else { + format!("failed to invoke command default") + } + })?; if !results.is_empty() { eprintln!( "warning: using `--invoke` with a function that returns values \ @@ -347,41 +334,36 @@ impl RunCommand { } } -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()?; +/// Populates the given `Linker` with WASI APIs. +fn populate_with_wasi( + linker: &mut Linker, + preopen_dirs: &[(String, File)], + argv: &[String], + vars: &[(String, String)], +) -> Result<()> { + // Add the current snapshot to the linker. + 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 mut cx2 = wasi_common::old::snapshot_0::WasiCtxBuilder::new(); + let cx = cx.build()?; + let wasi = Wasi::new(linker.store(), cx); + wasi.add_to_linker(linker)?; - cx2.inherit_stdio().args(argv).envs(vars); + // Repeat the above, but this time for snapshot 0. + let mut cx = wasi_common::old::snapshot_0::WasiCtxBuilder::new(); + cx.inherit_stdio().args(argv).envs(vars); - for (name, file) in preopen_dirs { - cx2.preopened_dir(file.try_clone()?, name); - } + for (name, file) in preopen_dirs { + cx.preopened_dir(file.try_clone()?, name); + } - let cx2 = cx2.build()?; + let cx = cx.build()?; + let wasi = wasmtime_wasi::old::snapshot_0::Wasi::new(linker.store(), cx); + wasi.add_to_linker(linker)?; - Ok(ModuleRegistry { - wasi_snapshot_preview1: Wasi::new(store, cx1), - wasi_unstable: WasiSnapshot0::new(store, cx2), - }) - } + Ok(()) } diff --git a/tests/all/cli_tests.rs b/tests/all/cli_tests.rs index c8edbd1fb6e5..41a0e155adc3 100644 --- a/tests/all/cli_tests.rs +++ b/tests/all/cli_tests.rs @@ -256,3 +256,97 @@ 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")?; + run_wasmtime(&[ + "run", + wasm.path().to_str().unwrap(), + "--invoke", + "_start", + "--disable-cache", + ])?; + 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 _initialize\n"); + Ok(()) +} + +// Run the greeter test, which runs a preloaded reactor and a command. +#[test] +fn greeter_preload_callable_command() -> 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_callable_command.wat", + ])?; + assert_eq!(stdout, "Hello _start\nHello callable greet\nHello done\n"); + Ok(()) +} diff --git a/tests/wasm/greeter_callable_command.wat b/tests/wasm/greeter_callable_command.wat new file mode 100644 index 000000000000..939debaf4889 --- /dev/null +++ b/tests/wasm/greeter_callable_command.wat @@ -0,0 +1,23 @@ +;; Like greeter_reactor, but exports "_start" instead of "_initialize". +(module + (import "wasi_snapshot_preview1" "fd_write" + (func $__wasi_fd_write (param i32 i32 i32 i32) (result i32))) + (func (export "_start") + (call $print (i32.const 32) (i32.const 22)) + ) + (func (export "greet") + (call $print (i32.const 64) (i32.const 21)) + ) + (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 callable _start\0a") + (data (i32.const 64) "Hello callable greet\0a") +) 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")) +)