diff --git a/Cargo.toml b/Cargo.toml index c67fd983cf87..34461629acdc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ wasmtime-wasi = { workspace = true, features = ["exit"] } wasmtime-wasi-nn = { workspace = true, optional = true } wasmtime-wasi-threads = { workspace = true, optional = true } wasmtime-wasi-http = { workspace = true, optional = true } +wasmtime-runtime = { workspace = true } clap = { workspace = true, features = ["color", "suggestions", "derive"] } anyhow = { workspace = true } target-lexicon = { workspace = true } diff --git a/ci/run-tests.sh b/ci/run-tests.sh index f8c52d241d2e..b29b6eb71d9b 100755 --- a/ci/run-tests.sh +++ b/ci/run-tests.sh @@ -4,6 +4,7 @@ cargo test \ --features "test-programs/test_programs" \ --features wasi-threads \ --features wasi-http \ + --features component-model \ --workspace \ --exclude 'wasmtime-wasi-*' \ --exclude wasi-tests \ diff --git a/crates/test-programs/tests/wasi-http-modules.rs b/crates/test-programs/tests/wasi-http-modules.rs index 98e40662effa..ba978c1037f1 100644 --- a/crates/test-programs/tests/wasi-http-modules.rs +++ b/crates/test-programs/tests/wasi-http-modules.rs @@ -63,7 +63,7 @@ impl WasiHttpView for Ctx { async fn instantiate_module(module: Module, ctx: Ctx) -> Result<(Store, Func), anyhow::Error> { let mut linker = Linker::new(&ENGINE); wasmtime_wasi_http::add_to_linker(&mut linker)?; - wasmtime_wasi::preview2::preview1::add_to_linker(&mut linker)?; + wasmtime_wasi::preview2::preview1::add_to_linker_async(&mut linker)?; let mut store = Store::new(&ENGINE, ctx); diff --git a/crates/test-programs/tests/wasi-preview1-host-in-preview2.rs b/crates/test-programs/tests/wasi-preview1-host-in-preview2.rs index 064620b0dabc..05dbd32717d9 100644 --- a/crates/test-programs/tests/wasi-preview1-host-in-preview2.rs +++ b/crates/test-programs/tests/wasi-preview1-host-in-preview2.rs @@ -4,7 +4,7 @@ use tempfile::TempDir; use wasmtime::{Config, Engine, Linker, Store}; use wasmtime_wasi::preview2::{ pipe::MemoryOutputPipe, - preview1::{add_to_linker, WasiPreview1Adapter, WasiPreview1View}, + preview1::{add_to_linker_async, WasiPreview1Adapter, WasiPreview1View}, DirPerms, FilePerms, IsATTY, Table, WasiCtx, WasiCtxBuilder, WasiView, }; @@ -34,7 +34,7 @@ async fn run(name: &str, inherit_stdio: bool) -> Result<()> { let stderr = MemoryOutputPipe::new(); let r = { let mut linker = Linker::new(&ENGINE); - add_to_linker(&mut linker)?; + add_to_linker_async(&mut linker)?; // Create our wasi context. // Additionally register any preopened directories if we have them. diff --git a/crates/wasi-nn/src/wit.rs b/crates/wasi-nn/src/wit.rs index b63a22025cbc..25510374d5cf 100644 --- a/crates/wasi-nn/src/wit.rs +++ b/crates/wasi-nn/src/wit.rs @@ -128,6 +128,10 @@ impl gen::inference::Host for WasiNnCtx { } } +impl gen::errors::Host for WasiNnCtx {} + +impl gen::tensor::Host for WasiNnCtx {} + impl TryFrom for crate::backend::BackendKind { type Error = UsageError; fn try_from(value: gen::graph::GraphEncoding) -> Result { diff --git a/crates/wasi/src/preview2/preview1.rs b/crates/wasi/src/preview2/preview1.rs index 7ab9604d656d..534c3950d5a8 100644 --- a/crates/wasi/src/preview2/preview1.rs +++ b/crates/wasi/src/preview2/preview1.rs @@ -215,7 +215,7 @@ impl WasiPreview1Adapter { // Any context that needs to support preview 1 will impl this trait. They can // construct the needed member with WasiPreview1Adapter::new(). -pub trait WasiPreview1View: Send + Sync + WasiView { +pub trait WasiPreview1View: WasiView { fn adapter(&self) -> &WasiPreview1Adapter; fn adapter_mut(&mut self) -> &mut WasiPreview1Adapter; } @@ -390,23 +390,18 @@ trait WasiPreview1ViewExt: impl WasiPreview1ViewExt for T {} -pub fn add_to_linker< - T: WasiPreview1View - + bindings::cli::environment::Host - + bindings::cli::exit::Host - + bindings::filesystem::types::Host - + bindings::filesystem::preopens::Host - + bindings::sync_io::poll::poll::Host - + bindings::random::random::Host - + bindings::io::streams::Host - + bindings::clocks::monotonic_clock::Host - + bindings::clocks::wall_clock::Host, ->( +pub fn add_to_linker_async( linker: &mut wasmtime::Linker, ) -> anyhow::Result<()> { wasi_snapshot_preview1::add_to_linker(linker, |t| t) } +pub fn add_to_linker_sync( + linker: &mut wasmtime::Linker, +) -> anyhow::Result<()> { + sync::add_wasi_snapshot_preview1_to_linker(linker, |t| t) +} + // Generate the wasi_snapshot_preview1::WasiSnapshotPreview1 trait, // and the module types. // None of the generated modules, traits, or types should be used externally @@ -425,6 +420,32 @@ wiggle::from_witx!({ errors: { errno => trappable Error }, }); +mod sync { + use anyhow::Result; + use std::future::Future; + + wiggle::wasmtime_integration!({ + witx: ["$CARGO_MANIFEST_DIR/witx/wasi_snapshot_preview1.witx"], + target: super, + block_on[in_tokio]: { + wasi_snapshot_preview1::{ + fd_advise, fd_close, fd_datasync, fd_fdstat_get, fd_filestat_get, fd_filestat_set_size, + fd_filestat_set_times, fd_read, fd_pread, fd_seek, fd_sync, fd_readdir, fd_write, + fd_pwrite, poll_oneoff, path_create_directory, path_filestat_get, + path_filestat_set_times, path_link, path_open, path_readlink, path_remove_directory, + path_rename, path_symlink, path_unlink_file + } + }, + errors: { errno => trappable Error }, + }); + + // Small wrapper around `in_tokio` to add a `Result` layer which is always + // `Ok` + fn in_tokio(future: F) -> Result { + Ok(crate::preview2::in_tokio(future)) + } +} + impl wiggle::GuestErrorType for types::Errno { fn success() -> Self { Self::Success diff --git a/crates/wasi/src/preview2/table.rs b/crates/wasi/src/preview2/table.rs index 7761dd74b1e2..f4efd8005ed1 100644 --- a/crates/wasi/src/preview2/table.rs +++ b/crates/wasi/src/preview2/table.rs @@ -285,3 +285,9 @@ impl Table { }) } } + +impl Default for Table { + fn default() -> Self { + Table::new() + } +} diff --git a/crates/wiggle/generate/src/config.rs b/crates/wiggle/generate/src/config.rs index f989cdda5628..a007d5448ee1 100644 --- a/crates/wiggle/generate/src/config.rs +++ b/crates/wiggle/generate/src/config.rs @@ -1,5 +1,5 @@ use { - proc_macro2::Span, + proc_macro2::{Span, TokenStream}, std::{collections::HashMap, iter::FromIterator, path::PathBuf}, syn::{ braced, bracketed, @@ -61,14 +61,21 @@ impl Parse for ConfigField { input.parse::()?; input.parse::()?; Ok(ConfigField::Async(AsyncConf { - blocking: false, + block_with: None, functions: input.parse()?, })) } else if lookahead.peek(kw::block_on) { input.parse::()?; + let block_with = if input.peek(syn::token::Bracket) { + let content; + let _ = bracketed!(content in input); + content.parse()? + } else { + quote::quote!(wiggle::run_in_dummy_executor) + }; input.parse::()?; Ok(ConfigField::Async(AsyncConf { - blocking: true, + block_with: Some(block_with), functions: input.parse()?, })) } else if lookahead.peek(kw::wasmtime) { @@ -381,16 +388,16 @@ impl std::fmt::Debug for UserErrorConfField { #[derive(Clone, Default, Debug)] /// Modules and funcs that have async signatures pub struct AsyncConf { - blocking: bool, + block_with: Option, functions: AsyncFunctions, } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug)] pub enum Asyncness { /// Wiggle function is synchronous, wasmtime Func is synchronous Sync, /// Wiggle function is asynchronous, but wasmtime Func is synchronous - Blocking, + Blocking { block_with: TokenStream }, /// Wiggle function and wasmtime Func are asynchronous. Async, } @@ -402,10 +409,10 @@ impl Asyncness { _ => false, } } - pub fn is_blocking(&self) -> bool { + pub fn blocking(&self) -> Option<&TokenStream> { match self { - Self::Blocking => true, - _ => false, + Self::Blocking { block_with } => Some(block_with), + _ => None, } } pub fn is_sync(&self) -> bool { @@ -429,10 +436,11 @@ impl Default for AsyncFunctions { impl AsyncConf { pub fn get(&self, module: &str, function: &str) -> Asyncness { - let a = if self.blocking { - Asyncness::Blocking - } else { - Asyncness::Async + let a = match &self.block_with { + Some(block_with) => Asyncness::Blocking { + block_with: block_with.clone(), + }, + None => Asyncness::Async, }; match &self.functions { AsyncFunctions::Some(fs) => { @@ -577,54 +585,12 @@ impl Parse for WasmtimeConfig { impl Parse for WasmtimeConfigField { fn parse(input: ParseStream) -> Result { - let lookahead = input.lookahead1(); - if lookahead.peek(kw::target) { + if input.peek(kw::target) { input.parse::()?; input.parse::()?; Ok(WasmtimeConfigField::Target(input.parse()?)) - - // The remainder of this function is the ConfigField impl, wrapped in - // WasmtimeConfigField::Core. This is required to get the correct lookahead error. - } else if lookahead.peek(kw::witx) { - input.parse::()?; - input.parse::()?; - Ok(WasmtimeConfigField::Core(ConfigField::Witx( - WitxConf::Paths(input.parse()?), - ))) - } else if lookahead.peek(kw::witx_literal) { - input.parse::()?; - input.parse::()?; - Ok(WasmtimeConfigField::Core(ConfigField::Witx( - WitxConf::Literal(input.parse()?), - ))) - } else if lookahead.peek(kw::errors) { - input.parse::()?; - input.parse::()?; - Ok(WasmtimeConfigField::Core(ConfigField::Error( - input.parse()?, - ))) - } else if lookahead.peek(Token![async]) { - input.parse::()?; - input.parse::()?; - Ok(WasmtimeConfigField::Core(ConfigField::Async(AsyncConf { - blocking: false, - functions: input.parse()?, - }))) - } else if lookahead.peek(kw::block_on) { - input.parse::()?; - input.parse::()?; - Ok(WasmtimeConfigField::Core(ConfigField::Async(AsyncConf { - blocking: true, - functions: input.parse()?, - }))) - } else if lookahead.peek(kw::mutable) { - input.parse::()?; - input.parse::()?; - Ok(WasmtimeConfigField::Core(ConfigField::Mutable( - input.parse::()?.value, - ))) } else { - Err(lookahead.error()) + Ok(WasmtimeConfigField::Core(input.parse()?)) } } } diff --git a/crates/wiggle/generate/src/wasmtime.rs b/crates/wiggle/generate/src/wasmtime.rs index 68872a3d8195..0db270e27306 100644 --- a/crates/wiggle/generate/src/wasmtime.rs +++ b/crates/wiggle/generate/src/wasmtime.rs @@ -142,14 +142,14 @@ fn generate_func( } } - Asyncness::Blocking => { + Asyncness::Blocking { block_with } => { quote! { linker.func_wrap( #module_str, #field_str, move |mut caller: wiggle::wasmtime_crate::Caller<'_, T> #(, #arg_decls)*| -> wiggle::anyhow::Result<#ret_ty> { let result = async { #body }; - wiggle::run_in_dummy_executor(result)? + #block_with(result)? }, )?; } diff --git a/src/commands/compile.rs b/src/commands/compile.rs index 9bb600acdc05..ad115cdab20f 100644 --- a/src/commands/compile.rs +++ b/src/commands/compile.rs @@ -98,25 +98,13 @@ impl CompileCommand { output }); - // If the component-model proposal is enabled and the binary we're - // compiling looks like a component, tested by sniffing the first 8 - // bytes with the current component model proposal. - #[cfg(feature = "component-model")] - { - if let Ok(wasmparser::Chunk::Parsed { - payload: - wasmparser::Payload::Version { - encoding: wasmparser::Encoding::Component, - .. - }, - .. - }) = wasmparser::Parser::new(0).parse(&input, true) - { - fs::write(output, engine.precompile_component(&input)?)?; - return Ok(()); - } - } - fs::write(output, engine.precompile_module(&input)?)?; + let output_bytes = if wasmparser::Parser::is_component(&input) { + engine.precompile_component(&input)? + } else { + engine.precompile_module(&input)? + }; + fs::write(&output, output_bytes) + .with_context(|| format!("failed to write output: {}", output.display()))?; Ok(()) } diff --git a/src/commands/run.rs b/src/commands/run.rs index 8b88f6b5afe1..f500844b9d8b 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -1,6 +1,11 @@ //! The module that implements the `wasmtime run` command. -use anyhow::{anyhow, bail, Context as _, Result}; +#![cfg_attr( + not(feature = "component-model"), + allow(irrefutable_let_patterns, unreachable_patterns) +)] + +use anyhow::{anyhow, bail, Context as _, Error, Result}; use clap::Parser; use once_cell::sync::Lazy; use std::fs::File; @@ -10,13 +15,17 @@ use std::sync::Arc; use std::thread; use std::time::Duration; use wasmtime::{ - AsContextMut, Engine, Func, GuestProfiler, Linker, Module, Store, StoreLimits, + AsContextMut, Engine, Func, GuestProfiler, Module, Precompiled, Store, StoreLimits, StoreLimitsBuilder, UpdateDeadline, Val, ValType, }; use wasmtime_cli_flags::{CommonOptions, WasiModules}; use wasmtime_wasi::maybe_exit_on_error; +use wasmtime_wasi::preview2; use wasmtime_wasi::sync::{ambient_authority, Dir, TcpListener, WasiCtxBuilder}; +#[cfg(feature = "component-model")] +use wasmtime::component::Component; + #[cfg(feature = "wasi-nn")] use wasmtime_wasi_nn::WasiNnCtx; @@ -237,6 +246,14 @@ pub struct RunCommand { /// arguments will be interpreted as arguments to the function specified. #[clap(value_name = "WASM", trailing_var_arg = true, required = true)] module_and_args: Vec, + + /// Indicates that the implementation of WASI preview1 should be backed by + /// the preview2 implementation for components. + /// + /// This will become the default in the future and this option will be + /// removed. For now this is primarily here for testing. + #[clap(long)] + preview2: bool, } #[derive(Clone)] @@ -245,6 +262,36 @@ enum Profile { Guest { path: String, interval: Duration }, } +enum CliLinker { + Core(wasmtime::Linker), + #[cfg(feature = "component-model")] + Component(wasmtime::component::Linker), +} + +enum CliModule { + Core(wasmtime::Module), + #[cfg(feature = "component-model")] + Component(Component), +} + +impl CliModule { + fn unwrap_core(&self) -> &Module { + match self { + CliModule::Core(module) => module, + #[cfg(feature = "component-model")] + CliModule::Component(_) => panic!("expected a core wasm module, not a component"), + } + } + + #[cfg(feature = "component-model")] + fn unwrap_component(&self) -> &Component { + match self { + CliModule::Component(c) => c, + CliModule::Core(_) => panic!("expected a component, not a core wasm module"), + } + } +} + impl RunCommand { /// Executes the command. pub fn execute(&self) -> Result<()> { @@ -270,7 +317,8 @@ impl RunCommand { let engine = Engine::new(&config)?; - let preopen_sockets = self.compute_preopen_sockets()?; + // Read the wasm module binary either as `*.wat` or a raw binary. + let main = self.load_module(&engine, &self.module_and_args[0])?; // Validate coredump-on-trap argument if let Some(coredump_path) = self.coredump_on_trap.as_ref() { @@ -279,30 +327,28 @@ impl RunCommand { } } - // Make wasi available by default. - let preopen_dirs = self.compute_preopen_dirs()?; - let argv = self.compute_argv()?; - - let mut linker = Linker::new(&engine); - linker.allow_unknown_exports(self.allow_unknown_exports); - - // Read the wasm module binary either as `*.wat` or a raw binary. - let module = self.load_module(linker.engine(), &self.module_and_args[0])?; - let mut modules = vec![(String::new(), module.clone())]; + let mut linker = match &main { + CliModule::Core(_) => CliLinker::Core(wasmtime::Linker::new(&engine)), + #[cfg(feature = "component-model")] + CliModule::Component(_) => { + CliLinker::Component(wasmtime::component::Linker::new(&engine)) + } + }; + if self.allow_unknown_exports { + match &mut linker { + CliLinker::Core(l) => { + l.allow_unknown_exports(true); + } + #[cfg(feature = "component-model")] + CliLinker::Component(_) => { + bail!("--allow-unknown-exports not supported with components"); + } + } + } let host = Host::default(); let mut store = Store::new(&engine, host); - populate_with_wasi( - &mut linker, - &mut store, - module.clone(), - preopen_dirs, - &argv, - &self.vars, - &self.common.wasi_modules.unwrap_or(WasiModules::default()), - self.listenfd, - preopen_sockets, - )?; + self.populate_with_wasi(&mut linker, &mut store, &main)?; let mut limits = StoreLimitsBuilder::new(); if let Some(max) = self.max_memory_size { @@ -332,22 +378,38 @@ impl RunCommand { } // Load the preload wasm modules. + let mut modules = Vec::new(); + if let CliModule::Core(m) = &main { + modules.push((String::new(), m.clone())); + } for (name, path) in self.preloads.iter() { // Read the wasm module binary either as `*.wat` or a raw binary - let module = self.load_module(&engine, path)?; + let module = match self.load_module(&engine, path)? { + CliModule::Core(m) => m, + #[cfg(feature = "component-model")] + CliModule::Component(_) => bail!("components cannot be loaded with `--preload`"), + }; modules.push((name.clone(), module.clone())); // Add the module's functions to the linker. - linker.module(&mut store, name, &module).context(format!( - "failed to process preload `{}` at `{}`", - name, - path.display() - ))?; + match &mut linker { + CliLinker::Core(linker) => { + linker.module(&mut store, name, &module).context(format!( + "failed to process preload `{}` at `{}`", + name, + path.display() + ))?; + } + #[cfg(feature = "component-model")] + CliLinker::Component(_) => { + bail!("--preload cannot be used with components"); + } + } } // Load the main wasm module. match self - .load_main_module(&mut store, &mut linker, module, modules, &argv[0]) + .load_main_module(&mut store, &mut linker, &main, modules) .with_context(|| { format!( "failed to run main module `{}`", @@ -426,10 +488,10 @@ impl RunCommand { fn setup_epoch_handler( &self, store: &mut Store, - module_name: &str, modules: Vec<(String, Module)>, ) -> Box)> { if let Some(Profile::Guest { path, interval }) = &self.profile { + let module_name = self.module_and_args[0].to_str().unwrap_or("
"); let interval = *interval; store.data_mut().guest_profiler = Some(Arc::new(GuestProfiler::new(module_name, interval, modules))); @@ -504,57 +566,88 @@ impl RunCommand { fn load_main_module( &self, store: &mut Store, - linker: &mut Linker, - module: Module, + linker: &mut CliLinker, + module: &CliModule, modules: Vec<(String, Module)>, - module_name: &str, ) -> Result<()> { // The main module might be allowed to have unknown imports, which // should be defined as traps: if self.trap_unknown_imports { - linker.define_unknown_imports_as_traps(&module)?; + match linker { + CliLinker::Core(linker) => { + linker.define_unknown_imports_as_traps(module.unwrap_core())?; + } + _ => bail!("cannot use `--trap-unknown-imports` with components"), + } } // ...or as default values. if self.default_values_unknown_imports { - linker.define_unknown_imports_as_default_values(&module)?; + match linker { + CliLinker::Core(linker) => { + linker.define_unknown_imports_as_default_values(module.unwrap_core())?; + } + _ => bail!("cannot use `--default-values-unknown-imports` with components"), + } } - // Use "" as a default module name. - linker.module(&mut *store, "", &module).context(format!( - "failed to instantiate {:?}", - self.module_and_args[0] - ))?; + let finish_epoch_handler = self.setup_epoch_handler(store, modules); + + let result = match linker { + CliLinker::Core(linker) => { + // Use "" as a default module name. + let module = module.unwrap_core(); + linker.module(&mut *store, "", &module).context(format!( + "failed to instantiate {:?}", + self.module_and_args[0] + ))?; + + // If a function to invoke was given, invoke it. + let func = if let Some(name) = &self.invoke { + match linker + .get(&mut *store, "", name) + .ok_or_else(|| anyhow!("no export named `{}` found", name))? + .into_func() + { + Some(func) => func, + None => bail!("export of `{}` wasn't a function", name), + } + } else { + linker.get_default(&mut *store, "")? + }; - // If a function to invoke was given, invoke it. - let func = if let Some(name) = &self.invoke { - self.find_export(store, linker, name)? - } else { - linker.get_default(&mut *store, "")? - }; + self.invoke_func(store, func) + } + #[cfg(feature = "component-model")] + CliLinker::Component(linker) => { + let component = module.unwrap_component(); + let instance = linker.instantiate(&mut *store, component)?; - // Finish all lookups before starting any epoch timers. - let finish_epoch_handler = self.setup_epoch_handler(store, module_name, modules); - let result = self.invoke_func(store, func); - finish_epoch_handler(store); - result - } + if self.invoke.is_some() { + bail!("using `--invoke` with components is not supported"); + } - fn find_export( - &self, - store: &mut Store, - linker: &Linker, - name: &str, - ) -> Result { - let func = match linker - .get(&mut *store, "", name) - .ok_or_else(|| anyhow!("no export named `{}` found", name))? - .into_func() - { - Some(func) => func, - None => bail!("export of `{}` wasn't a function", name), + // TODO: use the actual world + let func = instance + .get_typed_func::<(), (Result<(), ()>,)>(&mut *store, "run") + .context("failed to load `run` function")?; + + let result = func + .call(&mut *store, ()) + .context("failed to invoke `run` function") + .map_err(|e| self.handle_coredump(e)); + + // Translate the `Result<(),()>` produced by wasm into a feigned + // explicit exit here with status 1 if `Err(())` is returned. + result.and_then(|(wasm_result,)| match wasm_result { + Ok(()) => Ok(()), + Err(()) => Err(wasmtime_wasi::I32Exit(1).into()), + }) + } }; - Ok(func) + finish_epoch_handler(store); + + result } fn invoke_func(&self, store: &mut Store, func: Func) -> Result<()> { @@ -605,26 +698,7 @@ impl RunCommand { }); if let Err(err) = invoke_res { - let err = if err.is::() { - if let Some(coredump_path) = self.coredump_on_trap.as_ref() { - let source_name = self.module_and_args[0] - .to_str() - .unwrap_or_else(|| "unknown"); - - if let Err(coredump_err) = generate_coredump(&err, &source_name, coredump_path) - { - eprintln!("warning: coredump failed to generate: {}", coredump_err); - err - } else { - err.context(format!("core dumped at {}", coredump_path)) - } - } else { - err - } - } else { - err - }; - return Err(err); + return Err(self.handle_coredump(err)); } if !results.is_empty() { @@ -649,54 +723,255 @@ impl RunCommand { Ok(()) } - fn load_module(&self, engine: &Engine, path: &Path) -> Result { + fn handle_coredump(&self, err: Error) -> Error { + let coredump_path = match &self.coredump_on_trap { + Some(path) => path, + None => return err, + }; + if !err.is::() { + return err; + } + let source_name = self.module_and_args[0] + .to_str() + .unwrap_or_else(|| "unknown"); + + if let Err(coredump_err) = generate_coredump(&err, &source_name, coredump_path) { + eprintln!("warning: coredump failed to generate: {}", coredump_err); + err + } else { + err.context(format!("core dumped at {}", coredump_path)) + } + } + + fn load_module(&self, engine: &Engine, path: &Path) -> Result { let path = match path.to_str() { #[cfg(unix)] Some("-") => "/dev/stdin".as_ref(), _ => path, }; + // First attempt to load the module as an mmap. If this succeeds then + // detection can be done with the contents of the mmap and if a + // precompiled module is detected then `deserialize_file` can be used + // which is a slightly more optimal version than `deserialize` since we + // can leave most of the bytes on disk until they're referenced. + // + // If the mmap fails, for example if stdin is a pipe, then fall back to + // `std::fs::read` to load the contents. At that point precompiled + // modules must go through the `deserialize` functions. + // + // Note that this has the unfortunate side effect for precompiled + // modules on disk that they're opened once to detect what they are and + // then again internally in Wasmtime as part of the `deserialize_file` + // API. Currently there's no way to pass the `MmapVec` here through to + // Wasmtime itself (that'd require making `wasmtime-runtime` a public + // dependency or `MmapVec` a public type, both of which aren't ready to + // happen at this time). It's hoped though that opening a file twice + // isn't too bad in the grand scheme of things with respect to the CLI. + match wasmtime_runtime::MmapVec::from_file(path) { + Ok(map) => self.load_module_contents( + engine, + path, + &map, + || unsafe { Module::deserialize_file(engine, path) }, + #[cfg(feature = "component-model")] + || unsafe { Component::deserialize_file(engine, path) }, + ), + Err(_) => { + let bytes = std::fs::read(path) + .with_context(|| format!("failed to read file: {}", path.display()))?; + self.load_module_contents( + engine, + path, + &bytes, + || unsafe { Module::deserialize(engine, &bytes) }, + #[cfg(feature = "component-model")] + || unsafe { Component::deserialize(engine, &bytes) }, + ) + } + } + } + + fn load_module_contents( + &self, + engine: &Engine, + path: &Path, + bytes: &[u8], + deserialize_module: impl FnOnce() -> Result, + #[cfg(feature = "component-model")] deserialize_component: impl FnOnce() -> Result, + ) -> Result { + Ok(match engine.detect_precompiled(bytes) { + Some(Precompiled::Module) => { + self.ensure_allow_precompiled()?; + CliModule::Core(deserialize_module()?) + } + #[cfg(feature = "component-model")] + Some(Precompiled::Component) => { + self.ensure_allow_precompiled()?; + self.ensure_allow_components()?; + CliModule::Component(deserialize_component()?) + } + #[cfg(not(feature = "component-model"))] + Some(Precompiled::Component) => { + bail!("support for components was not enabled at compile time"); + } + None => { + // Parse the text format here specifically to add the `path` to + // the error message if there's a syntax error. + let wasm = wat::parse_bytes(bytes).map_err(|mut e| { + e.set_path(path); + e + })?; + if wasmparser::Parser::is_component(&wasm) { + #[cfg(feature = "component-model")] + { + self.ensure_allow_components()?; + CliModule::Component(Component::new(engine, &wasm)?) + } + #[cfg(not(feature = "component-model"))] + { + bail!("support for components was not enabled at compile time"); + } + } else { + CliModule::Core(Module::new(engine, &wasm)?) + } + } + }) + } + + fn ensure_allow_precompiled(&self) -> Result<()> { if self.allow_precompiled { - unsafe { Module::from_trusted_file(engine, path) } + Ok(()) } else { - Module::from_file(engine, path) - .context("if you're trying to run a precompiled module, pass --allow-precompiled") + bail!("running a precompiled module requires the `--allow-precompiled` flag") } } -} -#[derive(Default, Clone)] -struct Host { - wasi: Option, - #[cfg(feature = "wasi-nn")] - wasi_nn: Option>, - #[cfg(feature = "wasi-threads")] - wasi_threads: Option>>, - // #[cfg(feature = "wasi-http")] - // wasi_http: Option>, - limits: StoreLimits, - guest_profiler: Option>, -} + #[cfg(feature = "component-model")] + fn ensure_allow_components(&self) -> Result<()> { + if !self + .common + .wasm_features + .unwrap_or_default() + .component_model + .unwrap_or(false) + { + bail!("cannot execute a component without `--wasm-features component-model`"); + } -/// Populates the given `Linker` with WASI APIs. -fn populate_with_wasi( - linker: &mut Linker, - store: &mut Store, - module: Module, - preopen_dirs: Vec<(String, Dir)>, - argv: &[String], - vars: &[(String, Option)], - wasi_modules: &WasiModules, - listenfd: bool, - mut tcplisten: Vec, -) -> Result<()> { - if wasi_modules.wasi_common { - wasmtime_wasi::add_to_linker(linker, |host| host.wasi.as_mut().unwrap())?; + Ok(()) + } + /// Populates the given `Linker` with WASI APIs. + fn populate_with_wasi( + &self, + linker: &mut CliLinker, + store: &mut Store, + module: &CliModule, + ) -> Result<()> { + let wasi_modules = self.common.wasi_modules.unwrap_or(WasiModules::default()); + + if wasi_modules.wasi_common { + match linker { + CliLinker::Core(linker) => { + if self.preview2 { + wasmtime_wasi::preview2::preview1::add_to_linker_sync(linker)?; + self.set_preview2_ctx(store)?; + } else { + wasmtime_wasi::add_to_linker(linker, |host| { + host.preview1_ctx.as_mut().unwrap() + })?; + self.set_preview1_ctx(store)?; + } + } + #[cfg(feature = "component-model")] + CliLinker::Component(linker) => { + wasmtime_wasi::preview2::command::sync::add_to_linker(linker)?; + self.set_preview2_ctx(store)?; + } + } + } + + if wasi_modules.wasi_nn { + #[cfg(not(feature = "wasi-nn"))] + { + bail!("Cannot enable wasi-nn when the binary is not compiled with this feature."); + } + #[cfg(feature = "wasi-nn")] + { + match linker { + CliLinker::Core(linker) => { + wasmtime_wasi_nn::witx::add_to_linker(linker, |host| { + // This WASI proposal is currently not protected against + // concurrent access--i.e., when wasi-threads is actively + // spawning new threads, we cannot (yet) safely allow access and + // fail if more than one thread has `Arc`-references to the + // context. Once this proposal is updated (as wasi-common has + // been) to allow concurrent access, this `Arc::get_mut` + // limitation can be removed. + Arc::get_mut(host.wasi_nn.as_mut().unwrap()) + .expect("wasi-nn is not implemented with multi-threading support") + })?; + } + #[cfg(feature = "component-model")] + CliLinker::Component(linker) => { + wasmtime_wasi_nn::wit::ML::add_to_linker(linker, |host| { + Arc::get_mut(host.wasi_nn.as_mut().unwrap()) + .expect("wasi-nn is not implemented with multi-threading support") + })?; + } + } + store.data_mut().wasi_nn = Some(Arc::new(WasiNnCtx::default())); + } + } + + if wasi_modules.wasi_threads { + #[cfg(not(feature = "wasi-threads"))] + { + // Silence the unused warning for `module` as it is only used in the + // conditionally-compiled wasi-threads. + drop(&module); + + bail!( + "Cannot enable wasi-threads when the binary is not compiled with this feature." + ); + } + #[cfg(feature = "wasi-threads")] + { + let linker = match linker { + CliLinker::Core(linker) => linker, + _ => bail!("wasi-threads does not support components yet"), + }; + let module = module.unwrap_core(); + wasmtime_wasi_threads::add_to_linker(linker, store, &module, |host| { + host.wasi_threads.as_ref().unwrap() + })?; + store.data_mut().wasi_threads = Some(Arc::new(WasiThreadsCtx::new( + module.clone(), + Arc::new(linker.clone()), + )?)); + } + } + + if wasi_modules.wasi_http { + #[cfg(not(feature = "wasi-http"))] + { + bail!("Cannot enable wasi-http when the binary is not compiled with this feature."); + } + #[cfg(feature = "wasi-http")] + { + bail!("wasi-http support will be swapped over to component CLI support soon"); + } + } + + Ok(()) + } + + fn set_preview1_ctx(&self, store: &mut Store) -> Result<()> { let mut builder = WasiCtxBuilder::new(); - builder.inherit_stdio().args(argv)?; + builder.inherit_stdio().args(&self.compute_argv()?)?; - for (key, value) in vars { + for (key, value) in self.vars.iter() { let value = match value { Some(value) => value.clone(), None => std::env::var(key) @@ -707,77 +982,111 @@ fn populate_with_wasi( let mut num_fd: usize = 3; - if listenfd { + if self.listenfd { num_fd = ctx_set_listenfd(num_fd, &mut builder)?; } - for listener in tcplisten.drain(..) { + for listener in self.compute_preopen_sockets()? { builder.preopened_socket(num_fd as _, listener)?; num_fd += 1; } - for (name, dir) in preopen_dirs.into_iter() { + for (name, dir) in self.compute_preopen_dirs()? { builder.preopened_dir(dir, name)?; } - store.data_mut().wasi = Some(builder.build()); + store.data_mut().preview1_ctx = Some(builder.build()); + Ok(()) } - if wasi_modules.wasi_nn { - #[cfg(not(feature = "wasi-nn"))] - { - bail!("Cannot enable wasi-nn when the binary is not compiled with this feature."); + fn set_preview2_ctx(&self, store: &mut Store) -> Result<()> { + let mut builder = preview2::WasiCtxBuilder::new(); + builder.inherit_stdio().args(&self.compute_argv()?); + + for (key, value) in self.vars.iter() { + let value = match value { + Some(value) => value.clone(), + None => std::env::var(key) + .map_err(|_| anyhow!("environment varialbe `{key}` not found"))?, + }; + builder.env(key, &value); } - #[cfg(feature = "wasi-nn")] - { - wasmtime_wasi_nn::witx::add_to_linker(linker, |host| { - // This WASI proposal is currently not protected against - // concurrent access--i.e., when wasi-threads is actively - // spawning new threads, we cannot (yet) safely allow access and - // fail if more than one thread has `Arc`-references to the - // context. Once this proposal is updated (as wasi-common has - // been) to allow concurrent access, this `Arc::get_mut` - // limitation can be removed. - Arc::get_mut(host.wasi_nn.as_mut().unwrap()) - .expect("wasi-nn is not implemented with multi-threading support") - })?; - store.data_mut().wasi_nn = Some(Arc::new(WasiNnCtx::default())); + + if self.listenfd { + bail!("components do not support --listenfd"); + } + for _ in self.compute_preopen_sockets()? { + bail!("components do not support --tcplisten"); + } + + for (name, dir) in self.compute_preopen_dirs()? { + builder.preopened_dir( + dir, + preview2::DirPerms::all(), + preview2::FilePerms::all(), + name, + ); } + + let data = store.data_mut(); + let table = Arc::get_mut(&mut data.preview2_table).unwrap(); + let ctx = builder.build(table)?; + data.preview2_ctx = Some(Arc::new(ctx)); + Ok(()) } +} - if wasi_modules.wasi_threads { - #[cfg(not(feature = "wasi-threads"))] - { - // Silence the unused warning for `module` as it is only used in the - // conditionally-compiled wasi-threads. - drop(&module); +#[derive(Default, Clone)] +struct Host { + preview1_ctx: Option, + preview2_ctx: Option>, - bail!("Cannot enable wasi-threads when the binary is not compiled with this feature."); - } - #[cfg(feature = "wasi-threads")] - { - wasmtime_wasi_threads::add_to_linker(linker, store, &module, |host| { - host.wasi_threads.as_ref().unwrap() - })?; - store.data_mut().wasi_threads = Some(Arc::new(WasiThreadsCtx::new( - module, - Arc::new(linker.clone()), - )?)); - } + // Resource table for preview2 if the `preview2_ctx` is in use, otherwise + // "just" an empty table. + preview2_table: Arc, + + // State necessary for the preview1 implementation of WASI backed by the + // preview2 host implementation. Only used with the `--preview2` flag right + // now when running core modules. + preview2_adapter: Arc, + + #[cfg(feature = "wasi-nn")] + wasi_nn: Option>, + #[cfg(feature = "wasi-threads")] + wasi_threads: Option>>, + // #[cfg(feature = "wasi-http")] + // wasi_http: Option, + limits: StoreLimits, + guest_profiler: Option>, +} + +impl preview2::WasiView for Host { + fn table(&self) -> &preview2::Table { + &self.preview2_table } - if wasi_modules.wasi_http { - #[cfg(not(feature = "wasi-http"))] - { - bail!("Cannot enable wasi-http when the binary is not compiled with this feature."); - } - #[cfg(feature = "wasi-http")] - { - bail!("wasi-http support will be swapped over to component CLI support soon"); - } + fn table_mut(&mut self) -> &mut preview2::Table { + Arc::get_mut(&mut self.preview2_table).expect("preview2 is not compatible with threads") } - Ok(()) + fn ctx(&self) -> &preview2::WasiCtx { + self.preview2_ctx.as_ref().unwrap() + } + + fn ctx_mut(&mut self) -> &mut preview2::WasiCtx { + let ctx = self.preview2_ctx.as_mut().unwrap(); + Arc::get_mut(ctx).expect("preview2 is not compatible with threads") + } +} + +impl preview2::preview1::WasiPreview1View for Host { + fn adapter(&self) -> &preview2::preview1::WasiPreview1Adapter { + &self.preview2_adapter + } + + fn adapter_mut(&mut self) -> &mut preview2::preview1::WasiPreview1Adapter { + Arc::get_mut(&mut self.preview2_adapter).expect("preview2 is not compatible with threads") + } } #[cfg(not(unix))] diff --git a/tests/all/cli_tests.rs b/tests/all/cli_tests.rs index d7fd98cc103f..1f8200cc82af 100644 --- a/tests/all/cli_tests.rs +++ b/tests/all/cli_tests.rs @@ -531,7 +531,7 @@ fn run_cwasm_from_stdin() -> Result<()> { .output()?; assert!(output.status.success(), "a file as stdin should work"); - // If stdin is a pipe, however, that should fail + // If stdin is a pipe, that should also work let input = std::fs::read(&cwasm)?; let mut child = get_wasmtime_command()? .args(args) @@ -544,9 +544,7 @@ fn run_cwasm_from_stdin() -> Result<()> { let _ = stdin.write_all(&input); }); let output = child.wait_with_output()?; - if output.status.success() { - bail!("wasmtime should fail loading precompiled modules from piped files, but suceeded"); - } + assert!(output.status.success()); t.join().unwrap(); Ok(()) } @@ -731,3 +729,105 @@ fn wasi_misaligned_pointer() -> Result<()> { ); Ok(()) } + +#[test] +#[ignore] // FIXME(#6811) currently is flaky and may produce no output +fn hello_with_preview2() -> Result<()> { + let wasm = build_wasm("tests/all/cli_tests/hello_wasi_snapshot1.wat")?; + let stdout = run_wasmtime(&[ + "--disable-cache", + "--preview2", + wasm.path().to_str().unwrap(), + ])?; + assert_eq!(stdout, "Hello, world!\n"); + Ok(()) +} + +#[test] +#[cfg_attr(not(feature = "component-model"), ignore)] +fn component_missing_feature() -> Result<()> { + let path = "tests/all/cli_tests/empty-component.wat"; + let wasm = build_wasm(path)?; + let output = get_wasmtime_command()? + .arg("--disable-cache") + .arg(wasm.path()) + .output()?; + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("cannot execute a component without `--wasm-features component-model`"), + "bad stderr: {stderr}" + ); + + // also tests with raw *.wat input + let output = get_wasmtime_command()? + .arg("--disable-cache") + .arg(path) + .output()?; + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("cannot execute a component without `--wasm-features component-model`"), + "bad stderr: {stderr}" + ); + + Ok(()) +} + +// If the text format is invalid then the filename should be mentioned in the +// error message. +#[test] +fn bad_text_syntax() -> Result<()> { + let output = get_wasmtime_command()? + .arg("--disable-cache") + .arg("tests/all/cli_tests/bad-syntax.wat") + .output()?; + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("--> tests/all/cli_tests/bad-syntax.wat"), + "bad stderr: {stderr}" + ); + Ok(()) +} + +#[test] +#[cfg_attr(not(feature = "component-model"), ignore)] +fn run_basic_component() -> Result<()> { + let path = "tests/all/cli_tests/component-basic.wat"; + let wasm = build_wasm(path)?; + + // Run both the `*.wasm` binary and the text format + run_wasmtime(&[ + "--disable-cache", + "--wasm-features=component-model", + wasm.path().to_str().unwrap(), + ])?; + run_wasmtime(&["--disable-cache", "--wasm-features=component-model", path])?; + + Ok(()) +} + +#[test] +#[cfg_attr(not(feature = "component-model"), ignore)] +fn run_precompiled_component() -> Result<()> { + let td = TempDir::new()?; + let cwasm = td.path().join("component-basic.cwasm"); + let stdout = run_wasmtime(&[ + "compile", + "tests/all/cli_tests/component-basic.wat", + "-o", + cwasm.to_str().unwrap(), + "--wasm-features=component-model", + ])?; + assert_eq!(stdout, ""); + let stdout = run_wasmtime(&[ + "run", + "--wasm-features=component-model", + "--allow-precompiled", + cwasm.to_str().unwrap(), + ])?; + assert_eq!(stdout, ""); + + Ok(()) +} diff --git a/tests/all/cli_tests/bad-syntax.wat b/tests/all/cli_tests/bad-syntax.wat new file mode 100644 index 000000000000..8b23bab9c09d --- /dev/null +++ b/tests/all/cli_tests/bad-syntax.wat @@ -0,0 +1 @@ +(module ;; missing the close paren here diff --git a/tests/all/cli_tests/component-basic.wat b/tests/all/cli_tests/component-basic.wat new file mode 100644 index 000000000000..2b7f54a7af75 --- /dev/null +++ b/tests/all/cli_tests/component-basic.wat @@ -0,0 +1,10 @@ +(component + (core module $m + (func (export "run") (result i32) + i32.const 0) + ) + (core instance $i (instantiate $m)) + (func (export "run") (result (result)) + (canon lift (core func $i "run"))) + +) diff --git a/tests/all/cli_tests/empty-component.wat b/tests/all/cli_tests/empty-component.wat new file mode 100644 index 000000000000..e5627d1d0fc7 --- /dev/null +++ b/tests/all/cli_tests/empty-component.wat @@ -0,0 +1 @@ +(component)