1 //! The module that implements the `wasmtime run` command. 2 3 #![cfg_attr( 4 not(feature = "component-model"), 5 allow(irrefutable_let_patterns, unreachable_patterns) 6 )] 7 8 use crate::common::{Profile, RunCommon, RunTarget}; 9 use anyhow::{Context as _, Error, Result, anyhow, bail}; 10 use clap::Parser; 11 use std::ffi::OsString; 12 use std::path::{Path, PathBuf}; 13 use std::sync::{Arc, Mutex}; 14 use std::thread; 15 use wasi_common::sync::{Dir, TcpListener, WasiCtxBuilder, ambient_authority}; 16 use wasmtime::{Engine, Func, Module, Store, StoreLimits, Val, ValType}; 17 use wasmtime_wasi::{WasiCtxView, WasiView}; 18 19 #[cfg(feature = "wasi-config")] 20 use wasmtime_wasi_config::{WasiConfig, WasiConfigVariables}; 21 #[cfg(feature = "wasi-http")] 22 use wasmtime_wasi_http::{ 23 DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS, DEFAULT_OUTGOING_BODY_CHUNK_SIZE, WasiHttpCtx, 24 }; 25 #[cfg(feature = "wasi-keyvalue")] 26 use wasmtime_wasi_keyvalue::{WasiKeyValue, WasiKeyValueCtx, WasiKeyValueCtxBuilder}; 27 #[cfg(feature = "wasi-nn")] 28 use wasmtime_wasi_nn::wit::WasiNnView; 29 #[cfg(feature = "wasi-threads")] 30 use wasmtime_wasi_threads::WasiThreadsCtx; 31 #[cfg(feature = "wasi-tls")] 32 use wasmtime_wasi_tls::{WasiTls, WasiTlsCtx}; 33 34 fn parse_preloads(s: &str) -> Result<(String, PathBuf)> { 35 let parts: Vec<&str> = s.splitn(2, '=').collect(); 36 if parts.len() != 2 { 37 bail!("must contain exactly one equals character ('=')"); 38 } 39 Ok((parts[0].into(), parts[1].into())) 40 } 41 42 /// Runs a WebAssembly module 43 #[derive(Parser)] 44 pub struct RunCommand { 45 #[command(flatten)] 46 #[expect(missing_docs, reason = "don't want to mess with clap doc-strings")] 47 pub run: RunCommon, 48 49 /// The name of the function to run 50 #[arg(long, value_name = "FUNCTION")] 51 pub invoke: Option<String>, 52 53 /// Load the given WebAssembly module before the main module 54 #[arg( 55 long = "preload", 56 number_of_values = 1, 57 value_name = "NAME=MODULE_PATH", 58 value_parser = parse_preloads, 59 )] 60 pub preloads: Vec<(String, PathBuf)>, 61 62 /// Override the value of `argv[0]`, typically the name of the executable of 63 /// the application being run. 64 /// 65 /// This can be useful to pass in situations where a CLI tool is being 66 /// executed that dispatches its functionality on the value of `argv[0]` 67 /// without needing to rename the original wasm binary. 68 #[arg(long)] 69 pub argv0: Option<String>, 70 71 /// The WebAssembly module to run and arguments to pass to it. 72 /// 73 /// Arguments passed to the wasm module will be configured as WASI CLI 74 /// arguments unless the `--invoke` CLI argument is passed in which case 75 /// arguments will be interpreted as arguments to the function specified. 76 #[arg(value_name = "WASM", trailing_var_arg = true, required = true)] 77 pub module_and_args: Vec<OsString>, 78 } 79 80 enum CliLinker { 81 Core(wasmtime::Linker<Host>), 82 #[cfg(feature = "component-model")] 83 Component(wasmtime::component::Linker<Host>), 84 } 85 86 impl RunCommand { 87 /// Executes the command. 88 pub fn execute(mut self) -> Result<()> { 89 self.run.common.init_logging()?; 90 91 let mut config = self.run.common.config(None)?; 92 config.async_support(true); 93 94 if self.run.common.wasm.timeout.is_some() { 95 config.epoch_interruption(true); 96 } 97 match self.run.profile { 98 Some(Profile::Native(s)) => { 99 config.profiler(s); 100 } 101 Some(Profile::Guest { .. }) => { 102 // Further configured down below as well. 103 config.epoch_interruption(true); 104 } 105 None => {} 106 } 107 108 let engine = Engine::new(&config)?; 109 110 // Read the wasm module binary either as `*.wat` or a raw binary. 111 let main = self 112 .run 113 .load_module(&engine, self.module_and_args[0].as_ref())?; 114 115 // Validate coredump-on-trap argument 116 if let Some(path) = &self.run.common.debug.coredump { 117 if path.contains("%") { 118 bail!("the coredump-on-trap path does not support patterns yet.") 119 } 120 } 121 122 let mut linker = match &main { 123 RunTarget::Core(_) => CliLinker::Core(wasmtime::Linker::new(&engine)), 124 #[cfg(feature = "component-model")] 125 RunTarget::Component(_) => { 126 CliLinker::Component(wasmtime::component::Linker::new(&engine)) 127 } 128 }; 129 if let Some(enable) = self.run.common.wasm.unknown_exports_allow { 130 match &mut linker { 131 CliLinker::Core(l) => { 132 l.allow_unknown_exports(enable); 133 } 134 #[cfg(feature = "component-model")] 135 CliLinker::Component(_) => { 136 bail!("--allow-unknown-exports not supported with components"); 137 } 138 } 139 } 140 141 let host = Host { 142 #[cfg(feature = "wasi-http")] 143 wasi_http_outgoing_body_buffer_chunks: self 144 .run 145 .common 146 .wasi 147 .http_outgoing_body_buffer_chunks, 148 #[cfg(feature = "wasi-http")] 149 wasi_http_outgoing_body_chunk_size: self.run.common.wasi.http_outgoing_body_chunk_size, 150 ..Default::default() 151 }; 152 153 let mut store = Store::new(&engine, host); 154 self.populate_with_wasi(&mut linker, &mut store, &main)?; 155 156 store.data_mut().limits = self.run.store_limits(); 157 store.limiter(|t| &mut t.limits); 158 159 // If fuel has been configured, we want to add the configured 160 // fuel amount to this store. 161 if let Some(fuel) = self.run.common.wasm.fuel { 162 store.set_fuel(fuel)?; 163 } 164 165 // Always run the module asynchronously to ensure that the module can be 166 // interrupted, even if it is blocking on I/O or a timeout or something. 167 let runtime = tokio::runtime::Builder::new_multi_thread() 168 .enable_time() 169 .enable_io() 170 .build()?; 171 172 let dur = self 173 .run 174 .common 175 .wasm 176 .timeout 177 .unwrap_or(std::time::Duration::MAX); 178 let result = runtime.block_on(async { 179 tokio::time::timeout(dur, async { 180 let mut profiled_modules: Vec<(String, Module)> = Vec::new(); 181 if let RunTarget::Core(m) = &main { 182 profiled_modules.push(("".to_string(), m.clone())); 183 } 184 185 // Load the preload wasm modules. 186 for (name, path) in self.preloads.iter() { 187 // Read the wasm module binary either as `*.wat` or a raw binary 188 let preload_target = self.run.load_module(&engine, path)?; 189 let preload_module = match preload_target { 190 RunTarget::Core(m) => m, 191 #[cfg(feature = "component-model")] 192 RunTarget::Component(_) => { 193 bail!("components cannot be loaded with `--preload`") 194 } 195 }; 196 profiled_modules.push((name.to_string(), preload_module.clone())); 197 198 // Add the module's functions to the linker. 199 match &mut linker { 200 #[cfg(feature = "cranelift")] 201 CliLinker::Core(linker) => { 202 linker 203 .module_async(&mut store, name, &preload_module) 204 .await 205 .context(format!( 206 "failed to process preload `{}` at `{}`", 207 name, 208 path.display() 209 ))?; 210 } 211 #[cfg(not(feature = "cranelift"))] 212 CliLinker::Core(_) => { 213 bail!("support for --preload disabled at compile time"); 214 } 215 #[cfg(feature = "component-model")] 216 CliLinker::Component(_) => { 217 bail!("--preload cannot be used with components"); 218 } 219 } 220 } 221 222 self.load_main_module(&mut store, &mut linker, &main, profiled_modules) 223 .await 224 .with_context(|| { 225 format!( 226 "failed to run main module `{}`", 227 self.module_and_args[0].to_string_lossy() 228 ) 229 }) 230 }) 231 .await 232 }); 233 234 // Load the main wasm module. 235 match result.unwrap_or_else(|elapsed| { 236 Err(anyhow::Error::from(wasmtime::Trap::Interrupt)) 237 .with_context(|| format!("timed out after {elapsed}")) 238 }) { 239 Ok(()) => (), 240 Err(e) => { 241 // Exit the process if Wasmtime understands the error; 242 // otherwise, fall back on Rust's default error printing/return 243 // code. 244 if store.data().legacy_p1_ctx.is_some() { 245 return Err(wasi_common::maybe_exit_on_error(e)); 246 } else if store.data().wasip1_ctx.is_some() { 247 if let Some(exit) = e.downcast_ref::<wasmtime_wasi::I32Exit>() { 248 std::process::exit(exit.0); 249 } 250 } 251 if e.is::<wasmtime::Trap>() { 252 eprintln!("Error: {e:?}"); 253 cfg_if::cfg_if! { 254 if #[cfg(unix)] { 255 std::process::exit(rustix::process::EXIT_SIGNALED_SIGABRT); 256 } else if #[cfg(windows)] { 257 // https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/abort?view=vs-2019 258 std::process::exit(3); 259 } 260 } 261 } 262 return Err(e); 263 } 264 } 265 266 Ok(()) 267 } 268 269 fn compute_argv(&self) -> Result<Vec<String>> { 270 let mut result = Vec::new(); 271 272 for (i, arg) in self.module_and_args.iter().enumerate() { 273 // For argv[0], which is the program name. Only include the base 274 // name of the main wasm module, to avoid leaking path information. 275 let arg = if i == 0 { 276 match &self.argv0 { 277 Some(s) => s.as_ref(), 278 None => Path::new(arg).components().next_back().unwrap().as_os_str(), 279 } 280 } else { 281 arg.as_ref() 282 }; 283 result.push( 284 arg.to_str() 285 .ok_or_else(|| anyhow!("failed to convert {arg:?} to utf-8"))? 286 .to_string(), 287 ); 288 } 289 290 Ok(result) 291 } 292 293 fn setup_epoch_handler( 294 &self, 295 store: &mut Store<Host>, 296 main_target: &RunTarget, 297 profiled_modules: Vec<(String, Module)>, 298 ) -> Result<Box<dyn FnOnce(&mut Store<Host>)>> { 299 if let Some(Profile::Guest { path, interval }) = &self.run.profile { 300 #[cfg(feature = "profiling")] 301 return Ok(self.setup_guest_profiler( 302 store, 303 main_target, 304 profiled_modules, 305 path, 306 *interval, 307 )); 308 #[cfg(not(feature = "profiling"))] 309 { 310 let _ = (profiled_modules, path, interval, main_target); 311 bail!("support for profiling disabled at compile time"); 312 } 313 } 314 315 if let Some(timeout) = self.run.common.wasm.timeout { 316 store.set_epoch_deadline(1); 317 let engine = store.engine().clone(); 318 thread::spawn(move || { 319 thread::sleep(timeout); 320 engine.increment_epoch(); 321 }); 322 } 323 324 Ok(Box::new(|_store| {})) 325 } 326 327 #[cfg(feature = "profiling")] 328 fn setup_guest_profiler( 329 &self, 330 store: &mut Store<Host>, 331 main_target: &RunTarget, 332 profiled_modules: Vec<(String, Module)>, 333 path: &str, 334 interval: std::time::Duration, 335 ) -> Box<dyn FnOnce(&mut Store<Host>)> { 336 use wasmtime::{AsContext, GuestProfiler, StoreContext, StoreContextMut, UpdateDeadline}; 337 338 let module_name = self.module_and_args[0].to_str().unwrap_or("<main module>"); 339 store.data_mut().guest_profiler = match main_target { 340 RunTarget::Core(_m) => Some(Arc::new(GuestProfiler::new( 341 module_name, 342 interval, 343 profiled_modules, 344 ))), 345 RunTarget::Component(component) => Some(Arc::new(GuestProfiler::new_component( 346 module_name, 347 interval, 348 component.clone(), 349 profiled_modules, 350 ))), 351 }; 352 353 fn sample( 354 mut store: StoreContextMut<Host>, 355 f: impl FnOnce(&mut GuestProfiler, StoreContext<Host>), 356 ) { 357 let mut profiler = store.data_mut().guest_profiler.take().unwrap(); 358 f( 359 Arc::get_mut(&mut profiler).expect("profiling doesn't support threads yet"), 360 store.as_context(), 361 ); 362 store.data_mut().guest_profiler = Some(profiler); 363 } 364 365 store.call_hook(|store, kind| { 366 sample(store, |profiler, store| profiler.call_hook(store, kind)); 367 Ok(()) 368 }); 369 370 if let Some(timeout) = self.run.common.wasm.timeout { 371 let mut timeout = (timeout.as_secs_f64() / interval.as_secs_f64()).ceil() as u64; 372 assert!(timeout > 0); 373 store.epoch_deadline_callback(move |store| { 374 sample(store, |profiler, store| { 375 profiler.sample(store, std::time::Duration::ZERO) 376 }); 377 timeout -= 1; 378 if timeout == 0 { 379 bail!("timeout exceeded"); 380 } 381 Ok(UpdateDeadline::Continue(1)) 382 }); 383 } else { 384 store.epoch_deadline_callback(move |store| { 385 sample(store, |profiler, store| { 386 profiler.sample(store, std::time::Duration::ZERO) 387 }); 388 Ok(UpdateDeadline::Continue(1)) 389 }); 390 } 391 392 store.set_epoch_deadline(1); 393 let engine = store.engine().clone(); 394 thread::spawn(move || { 395 loop { 396 thread::sleep(interval); 397 engine.increment_epoch(); 398 } 399 }); 400 401 let path = path.to_string(); 402 return Box::new(move |store| { 403 let profiler = Arc::try_unwrap(store.data_mut().guest_profiler.take().unwrap()) 404 .expect("profiling doesn't support threads yet"); 405 if let Err(e) = std::fs::File::create(&path) 406 .map_err(anyhow::Error::new) 407 .and_then(|output| profiler.finish(std::io::BufWriter::new(output))) 408 { 409 eprintln!("failed writing profile at {path}: {e:#}"); 410 } else { 411 eprintln!(); 412 eprintln!("Profile written to: {path}"); 413 eprintln!("View this profile at https://profiler.firefox.com/."); 414 } 415 }); 416 } 417 418 async fn load_main_module( 419 &self, 420 store: &mut Store<Host>, 421 linker: &mut CliLinker, 422 main_target: &RunTarget, 423 profiled_modules: Vec<(String, Module)>, 424 ) -> Result<()> { 425 // The main module might be allowed to have unknown imports, which 426 // should be defined as traps: 427 if self.run.common.wasm.unknown_imports_trap == Some(true) { 428 match linker { 429 CliLinker::Core(linker) => { 430 linker.define_unknown_imports_as_traps(main_target.unwrap_core())?; 431 } 432 #[cfg(feature = "component-model")] 433 CliLinker::Component(linker) => { 434 linker.define_unknown_imports_as_traps(main_target.unwrap_component())?; 435 } 436 } 437 } 438 439 // ...or as default values. 440 if self.run.common.wasm.unknown_imports_default == Some(true) { 441 match linker { 442 CliLinker::Core(linker) => { 443 linker.define_unknown_imports_as_default_values( 444 store, 445 main_target.unwrap_core(), 446 )?; 447 } 448 _ => bail!("cannot use `--default-values-unknown-imports` with components"), 449 } 450 } 451 452 let finish_epoch_handler = 453 self.setup_epoch_handler(store, main_target, profiled_modules)?; 454 455 let result = match linker { 456 CliLinker::Core(linker) => { 457 let module = main_target.unwrap_core(); 458 let instance = linker 459 .instantiate_async(&mut *store, &module) 460 .await 461 .context(format!( 462 "failed to instantiate {:?}", 463 self.module_and_args[0] 464 ))?; 465 466 // If `_initialize` is present, meaning a reactor, then invoke 467 // the function. 468 if let Some(func) = instance.get_func(&mut *store, "_initialize") { 469 func.typed::<(), ()>(&store)? 470 .call_async(&mut *store, ()) 471 .await?; 472 } 473 474 // Look for the specific function provided or otherwise look for 475 // "" or "_start" exports to run as a "main" function. 476 let func = if let Some(name) = &self.invoke { 477 Some( 478 instance 479 .get_func(&mut *store, name) 480 .ok_or_else(|| anyhow!("no func export named `{}` found", name))?, 481 ) 482 } else { 483 instance 484 .get_func(&mut *store, "") 485 .or_else(|| instance.get_func(&mut *store, "_start")) 486 }; 487 488 match func { 489 Some(func) => self.invoke_func(store, func).await, 490 None => Ok(()), 491 } 492 } 493 #[cfg(feature = "component-model")] 494 CliLinker::Component(linker) => { 495 let component = main_target.unwrap_component(); 496 let result = if self.invoke.is_some() { 497 self.invoke_component(&mut *store, component, linker).await 498 } else { 499 self.run_command_component(&mut *store, component, linker) 500 .await 501 }; 502 result.map_err(|e| self.handle_core_dump(&mut *store, e)) 503 } 504 }; 505 finish_epoch_handler(store); 506 507 result 508 } 509 510 #[cfg(feature = "component-model")] 511 async fn invoke_component( 512 &self, 513 store: &mut Store<Host>, 514 component: &wasmtime::component::Component, 515 linker: &mut wasmtime::component::Linker<Host>, 516 ) -> Result<()> { 517 use wasmtime::component::{ 518 Val, 519 types::ComponentItem, 520 wasm_wave::{ 521 untyped::UntypedFuncCall, 522 wasm::{DisplayFuncResults, WasmFunc}, 523 }, 524 }; 525 526 // Check if the invoke string is present 527 let invoke: &String = self.invoke.as_ref().unwrap(); 528 529 let untyped_call = UntypedFuncCall::parse(invoke).with_context(|| { 530 format!( 531 "Failed to parse invoke '{invoke}': See https://docs.wasmtime.dev/cli-options.html#run for syntax", 532 ) 533 })?; 534 535 let name = untyped_call.name(); 536 let matches = Self::search_component(store.engine(), component.component_type(), name); 537 match matches.len() { 538 0 => bail!("No export named `{name}` in component."), 539 1 => {} 540 _ => bail!( 541 "Multiple exports named `{name}`: {matches:?}. FIXME: support some way to disambiguate names" 542 ), 543 }; 544 let (params, result_len, export) = match &matches[0] { 545 (names, ComponentItem::ComponentFunc(func)) => { 546 let param_types = WasmFunc::params(func).collect::<Vec<_>>(); 547 let params = untyped_call.to_wasm_params(¶m_types).with_context(|| { 548 format!("while interpreting parameters in invoke \"{invoke}\"") 549 })?; 550 let mut export = None; 551 for name in names { 552 let ix = component 553 .get_export_index(export.as_ref(), name) 554 .expect("export exists"); 555 export = Some(ix); 556 } 557 ( 558 params, 559 func.results().len(), 560 export.expect("export has at least one name"), 561 ) 562 } 563 (names, ty) => { 564 bail!("Cannot invoke export {names:?}: expected ComponentFunc, got type {ty:?}"); 565 } 566 }; 567 568 let instance = linker.instantiate_async(&mut *store, component).await?; 569 570 let func = instance 571 .get_func(&mut *store, export) 572 .expect("found export index"); 573 574 let mut results = vec![Val::Bool(false); result_len]; 575 func.call_async(&mut *store, ¶ms, &mut results).await?; 576 577 println!("{}", DisplayFuncResults(&results)); 578 579 Ok(()) 580 } 581 582 /// Execute the default behavior for components on the CLI, looking for 583 /// `wasi:cli`-based commands and running their exported `run` function. 584 #[cfg(feature = "component-model")] 585 async fn run_command_component( 586 &self, 587 store: &mut Store<Host>, 588 component: &wasmtime::component::Component, 589 linker: &wasmtime::component::Linker<Host>, 590 ) -> Result<()> { 591 let instance = linker.instantiate_async(&mut *store, component).await?; 592 593 let mut result = None; 594 let _ = &mut result; 595 596 // If WASIp3 is enabled at compile time, enabled at runtime, and found 597 // in this component then use that to generate the result. 598 #[cfg(feature = "component-model-async")] 599 if self.run.common.wasi.p3.unwrap_or(crate::common::P3_DEFAULT) { 600 if let Ok(command) = wasmtime_wasi::p3::bindings::Command::new(&mut *store, &instance) { 601 result = Some( 602 instance 603 .run_concurrent(&mut *store, async |store| { 604 command.wasi_cli_run().call_run(store).await 605 }) 606 .await?, 607 ); 608 } 609 } 610 611 let result = match result { 612 Some(result) => result, 613 // If WASIp3 wasn't found then fall back to requiring WASIp2 and 614 // this'll report an error if the right export doesn't exist. 615 None => { 616 wasmtime_wasi::p2::bindings::Command::new(&mut *store, &instance)? 617 .wasi_cli_run() 618 .call_run(&mut *store) 619 .await 620 } 621 }; 622 let wasm_result = result.context("failed to invoke `run` function")?; 623 624 // Translate the `Result<(),()>` produced by wasm into a feigned 625 // explicit exit here with status 1 if `Err(())` is returned. 626 match wasm_result { 627 Ok(()) => Ok(()), 628 Err(()) => Err(wasmtime_wasi::I32Exit(1).into()), 629 } 630 } 631 632 #[cfg(feature = "component-model")] 633 fn search_component( 634 engine: &Engine, 635 component: wasmtime::component::types::Component, 636 name: &str, 637 ) -> Vec<(Vec<String>, wasmtime::component::types::ComponentItem)> { 638 use wasmtime::component::types::ComponentItem as CItem; 639 fn collect_exports( 640 engine: &Engine, 641 item: CItem, 642 basename: Vec<String>, 643 ) -> Vec<(Vec<String>, CItem)> { 644 match item { 645 CItem::Component(c) => c 646 .exports(engine) 647 .flat_map(move |(name, item)| { 648 let mut names = basename.clone(); 649 names.push(name.to_string()); 650 collect_exports(engine, item, names) 651 }) 652 .collect::<Vec<_>>(), 653 CItem::ComponentInstance(c) => c 654 .exports(engine) 655 .flat_map(move |(name, item)| { 656 let mut names = basename.clone(); 657 names.push(name.to_string()); 658 collect_exports(engine, item, names) 659 }) 660 .collect::<Vec<_>>(), 661 _ => vec![(basename, item)], 662 } 663 } 664 665 collect_exports(engine, CItem::Component(component), Vec::new()) 666 .into_iter() 667 .filter(|(names, _item)| names.last().expect("at least one name") == name) 668 .collect() 669 } 670 671 async fn invoke_func(&self, store: &mut Store<Host>, func: Func) -> Result<()> { 672 let ty = func.ty(&store); 673 if ty.params().len() > 0 { 674 eprintln!( 675 "warning: using `--invoke` with a function that takes arguments \ 676 is experimental and may break in the future" 677 ); 678 } 679 let mut args = self.module_and_args.iter().skip(1); 680 let mut values = Vec::new(); 681 for ty in ty.params() { 682 let val = match args.next() { 683 Some(s) => s, 684 None => { 685 if let Some(name) = &self.invoke { 686 bail!("not enough arguments for `{}`", name) 687 } else { 688 bail!("not enough arguments for command default") 689 } 690 } 691 }; 692 let val = val 693 .to_str() 694 .ok_or_else(|| anyhow!("argument is not valid utf-8: {val:?}"))?; 695 values.push(match ty { 696 // Supports both decimal and hexadecimal notation (with 0x prefix) 697 ValType::I32 => Val::I32(if val.starts_with("0x") || val.starts_with("0X") { 698 i32::from_str_radix(&val[2..], 16)? 699 } else { 700 val.parse::<i32>()? 701 }), 702 ValType::I64 => Val::I64(if val.starts_with("0x") || val.starts_with("0X") { 703 i64::from_str_radix(&val[2..], 16)? 704 } else { 705 val.parse::<i64>()? 706 }), 707 ValType::F32 => Val::F32(val.parse::<f32>()?.to_bits()), 708 ValType::F64 => Val::F64(val.parse::<f64>()?.to_bits()), 709 t => bail!("unsupported argument type {:?}", t), 710 }); 711 } 712 713 // Invoke the function and then afterwards print all the results that came 714 // out, if there are any. 715 let mut results = vec![Val::null_func_ref(); ty.results().len()]; 716 let invoke_res = func 717 .call_async(&mut *store, &values, &mut results) 718 .await 719 .with_context(|| { 720 if let Some(name) = &self.invoke { 721 format!("failed to invoke `{name}`") 722 } else { 723 format!("failed to invoke command default") 724 } 725 }); 726 727 if let Err(err) = invoke_res { 728 return Err(self.handle_core_dump(&mut *store, err)); 729 } 730 731 if !results.is_empty() { 732 eprintln!( 733 "warning: using `--invoke` with a function that returns values \ 734 is experimental and may break in the future" 735 ); 736 } 737 738 for result in results { 739 match result { 740 Val::I32(i) => println!("{i}"), 741 Val::I64(i) => println!("{i}"), 742 Val::F32(f) => println!("{}", f32::from_bits(f)), 743 Val::F64(f) => println!("{}", f64::from_bits(f)), 744 Val::V128(i) => println!("{}", i.as_u128()), 745 Val::ExternRef(None) => println!("<null externref>"), 746 Val::ExternRef(Some(_)) => println!("<externref>"), 747 Val::FuncRef(None) => println!("<null funcref>"), 748 Val::FuncRef(Some(_)) => println!("<funcref>"), 749 Val::AnyRef(None) => println!("<null anyref>"), 750 Val::AnyRef(Some(_)) => println!("<anyref>"), 751 Val::ExnRef(None) => println!("<null exnref>"), 752 Val::ExnRef(Some(_)) => println!("<exnref>"), 753 } 754 } 755 756 Ok(()) 757 } 758 759 #[cfg(feature = "coredump")] 760 fn handle_core_dump(&self, store: &mut Store<Host>, err: Error) -> Error { 761 let coredump_path = match &self.run.common.debug.coredump { 762 Some(path) => path, 763 None => return err, 764 }; 765 if !err.is::<wasmtime::Trap>() { 766 return err; 767 } 768 let source_name = self.module_and_args[0] 769 .to_str() 770 .unwrap_or_else(|| "unknown"); 771 772 if let Err(coredump_err) = write_core_dump(store, &err, &source_name, coredump_path) { 773 eprintln!("warning: coredump failed to generate: {coredump_err}"); 774 err 775 } else { 776 err.context(format!("core dumped at {coredump_path}")) 777 } 778 } 779 780 #[cfg(not(feature = "coredump"))] 781 fn handle_core_dump(&self, _store: &mut Store<Host>, err: Error) -> Error { 782 err 783 } 784 785 /// Populates the given `Linker` with WASI APIs. 786 fn populate_with_wasi( 787 &self, 788 linker: &mut CliLinker, 789 store: &mut Store<Host>, 790 module: &RunTarget, 791 ) -> Result<()> { 792 self.run.validate_p3_option()?; 793 let cli = self.run.validate_cli_enabled()?; 794 795 if cli != Some(false) { 796 match linker { 797 CliLinker::Core(linker) => { 798 match (self.run.common.wasi.preview2, self.run.common.wasi.threads) { 799 // If preview2 is explicitly disabled, or if threads 800 // are enabled, then use the historical preview1 801 // implementation. 802 (Some(false), _) | (None, Some(true)) => { 803 wasi_common::tokio::add_to_linker(linker, |host| { 804 host.legacy_p1_ctx.as_mut().unwrap() 805 })?; 806 self.set_legacy_p1_ctx(store)?; 807 } 808 // If preview2 was explicitly requested, always use it. 809 // Otherwise use it so long as threads are disabled. 810 // 811 // Note that for now `p0` is currently 812 // default-enabled but this may turn into 813 // default-disabled in the future. 814 (Some(true), _) | (None, Some(false) | None) => { 815 if self.run.common.wasi.preview0 != Some(false) { 816 wasmtime_wasi::p0::add_to_linker_async(linker, |t| t.wasip1_ctx())?; 817 } 818 wasmtime_wasi::p1::add_to_linker_async(linker, |t| t.wasip1_ctx())?; 819 self.set_wasi_ctx(store)?; 820 } 821 } 822 } 823 #[cfg(feature = "component-model")] 824 CliLinker::Component(linker) => { 825 self.run.add_wasmtime_wasi_to_linker(linker)?; 826 self.set_wasi_ctx(store)?; 827 } 828 } 829 } 830 831 if self.run.common.wasi.nn == Some(true) { 832 #[cfg(not(feature = "wasi-nn"))] 833 { 834 bail!("Cannot enable wasi-nn when the binary is not compiled with this feature."); 835 } 836 #[cfg(all(feature = "wasi-nn", feature = "component-model"))] 837 { 838 let (backends, registry) = self.collect_preloaded_nn_graphs()?; 839 match linker { 840 CliLinker::Core(linker) => { 841 wasmtime_wasi_nn::witx::add_to_linker(linker, |host| { 842 Arc::get_mut(host.wasi_nn_witx.as_mut().unwrap()) 843 .expect("wasi-nn is not implemented with multi-threading support") 844 })?; 845 store.data_mut().wasi_nn_witx = Some(Arc::new( 846 wasmtime_wasi_nn::witx::WasiNnCtx::new(backends, registry), 847 )); 848 } 849 #[cfg(feature = "component-model")] 850 CliLinker::Component(linker) => { 851 wasmtime_wasi_nn::wit::add_to_linker(linker, |h: &mut Host| { 852 let ctx = h.wasip1_ctx.as_mut().expect("wasi is not configured"); 853 let ctx = Arc::get_mut(ctx) 854 .expect("wasmtime_wasi is not compatible with threads") 855 .get_mut() 856 .unwrap(); 857 let nn_ctx = Arc::get_mut(h.wasi_nn_wit.as_mut().unwrap()) 858 .expect("wasi-nn is not implemented with multi-threading support"); 859 WasiNnView::new(ctx.ctx().table, nn_ctx) 860 })?; 861 store.data_mut().wasi_nn_wit = Some(Arc::new( 862 wasmtime_wasi_nn::wit::WasiNnCtx::new(backends, registry), 863 )); 864 } 865 } 866 } 867 } 868 869 if self.run.common.wasi.config == Some(true) { 870 #[cfg(not(feature = "wasi-config"))] 871 { 872 bail!( 873 "Cannot enable wasi-config when the binary is not compiled with this feature." 874 ); 875 } 876 #[cfg(all(feature = "wasi-config", feature = "component-model"))] 877 { 878 match linker { 879 CliLinker::Core(_) => { 880 bail!("Cannot enable wasi-config for core wasm modules"); 881 } 882 CliLinker::Component(linker) => { 883 let vars = WasiConfigVariables::from_iter( 884 self.run 885 .common 886 .wasi 887 .config_var 888 .iter() 889 .map(|v| (v.key.clone(), v.value.clone())), 890 ); 891 892 wasmtime_wasi_config::add_to_linker(linker, |h| { 893 WasiConfig::new(Arc::get_mut(h.wasi_config.as_mut().unwrap()).unwrap()) 894 })?; 895 store.data_mut().wasi_config = Some(Arc::new(vars)); 896 } 897 } 898 } 899 } 900 901 if self.run.common.wasi.keyvalue == Some(true) { 902 #[cfg(not(feature = "wasi-keyvalue"))] 903 { 904 bail!( 905 "Cannot enable wasi-keyvalue when the binary is not compiled with this feature." 906 ); 907 } 908 #[cfg(all(feature = "wasi-keyvalue", feature = "component-model"))] 909 { 910 match linker { 911 CliLinker::Core(_) => { 912 bail!("Cannot enable wasi-keyvalue for core wasm modules"); 913 } 914 CliLinker::Component(linker) => { 915 let ctx = WasiKeyValueCtxBuilder::new() 916 .in_memory_data( 917 self.run 918 .common 919 .wasi 920 .keyvalue_in_memory_data 921 .iter() 922 .map(|v| (v.key.clone(), v.value.clone())), 923 ) 924 .build(); 925 926 wasmtime_wasi_keyvalue::add_to_linker(linker, |h| { 927 let ctx = h.wasip1_ctx.as_mut().expect("wasip2 is not configured"); 928 let ctx = Arc::get_mut(ctx).unwrap().get_mut().unwrap(); 929 WasiKeyValue::new( 930 Arc::get_mut(h.wasi_keyvalue.as_mut().unwrap()).unwrap(), 931 ctx.ctx().table, 932 ) 933 })?; 934 store.data_mut().wasi_keyvalue = Some(Arc::new(ctx)); 935 } 936 } 937 } 938 } 939 940 if self.run.common.wasi.threads == Some(true) { 941 #[cfg(not(feature = "wasi-threads"))] 942 { 943 // Silence the unused warning for `module` as it is only used in the 944 // conditionally-compiled wasi-threads. 945 let _ = &module; 946 947 bail!( 948 "Cannot enable wasi-threads when the binary is not compiled with this feature." 949 ); 950 } 951 #[cfg(feature = "wasi-threads")] 952 { 953 let linker = match linker { 954 CliLinker::Core(linker) => linker, 955 _ => bail!("wasi-threads does not support components yet"), 956 }; 957 let module = module.unwrap_core(); 958 wasmtime_wasi_threads::add_to_linker(linker, store, &module, |host| { 959 host.wasi_threads.as_ref().unwrap() 960 })?; 961 store.data_mut().wasi_threads = Some(Arc::new(WasiThreadsCtx::new( 962 module.clone(), 963 Arc::new(linker.clone()), 964 )?)); 965 } 966 } 967 968 if self.run.common.wasi.http == Some(true) { 969 #[cfg(not(all(feature = "wasi-http", feature = "component-model")))] 970 { 971 bail!("Cannot enable wasi-http when the binary is not compiled with this feature."); 972 } 973 #[cfg(all(feature = "wasi-http", feature = "component-model"))] 974 { 975 match linker { 976 CliLinker::Core(_) => { 977 bail!("Cannot enable wasi-http for core wasm modules"); 978 } 979 CliLinker::Component(linker) => { 980 wasmtime_wasi_http::add_only_http_to_linker_sync(linker)?; 981 } 982 } 983 984 store.data_mut().wasi_http = Some(Arc::new(WasiHttpCtx::new())); 985 } 986 } 987 988 if self.run.common.wasi.tls == Some(true) { 989 #[cfg(all(not(all(feature = "wasi-tls", feature = "component-model"))))] 990 { 991 bail!("Cannot enable wasi-tls when the binary is not compiled with this feature."); 992 } 993 #[cfg(all(feature = "wasi-tls", feature = "component-model",))] 994 { 995 match linker { 996 CliLinker::Core(_) => { 997 bail!("Cannot enable wasi-tls for core wasm modules"); 998 } 999 CliLinker::Component(linker) => { 1000 let mut opts = wasmtime_wasi_tls::LinkOptions::default(); 1001 opts.tls(true); 1002 wasmtime_wasi_tls::add_to_linker(linker, &mut opts, |h| { 1003 let ctx = h.wasip1_ctx.as_mut().expect("wasi is not configured"); 1004 let ctx = Arc::get_mut(ctx).unwrap().get_mut().unwrap(); 1005 WasiTls::new( 1006 Arc::get_mut(h.wasi_tls.as_mut().unwrap()).unwrap(), 1007 ctx.ctx().table, 1008 ) 1009 })?; 1010 1011 let ctx = wasmtime_wasi_tls::WasiTlsCtxBuilder::new().build(); 1012 store.data_mut().wasi_tls = Some(Arc::new(ctx)); 1013 } 1014 } 1015 } 1016 } 1017 1018 Ok(()) 1019 } 1020 1021 fn set_legacy_p1_ctx(&self, store: &mut Store<Host>) -> Result<()> { 1022 let mut builder = WasiCtxBuilder::new(); 1023 builder.inherit_stdio().args(&self.compute_argv()?)?; 1024 1025 if self.run.common.wasi.inherit_env == Some(true) { 1026 for (k, v) in std::env::vars() { 1027 builder.env(&k, &v)?; 1028 } 1029 } 1030 for (key, value) in self.run.vars.iter() { 1031 let value = match value { 1032 Some(value) => value.clone(), 1033 None => match std::env::var_os(key) { 1034 Some(val) => val 1035 .into_string() 1036 .map_err(|_| anyhow!("environment variable `{key}` not valid utf-8"))?, 1037 None => { 1038 // leave the env var un-set in the guest 1039 continue; 1040 } 1041 }, 1042 }; 1043 builder.env(key, &value)?; 1044 } 1045 1046 let mut num_fd: usize = 3; 1047 1048 if self.run.common.wasi.listenfd == Some(true) { 1049 num_fd = ctx_set_listenfd(num_fd, &mut builder)?; 1050 } 1051 1052 for listener in self.run.compute_preopen_sockets()? { 1053 let listener = TcpListener::from_std(listener); 1054 builder.preopened_socket(num_fd as _, listener)?; 1055 num_fd += 1; 1056 } 1057 1058 for (host, guest) in self.run.dirs.iter() { 1059 let dir = Dir::open_ambient_dir(host, ambient_authority()) 1060 .with_context(|| format!("failed to open directory '{host}'"))?; 1061 builder.preopened_dir(dir, guest)?; 1062 } 1063 1064 store.data_mut().legacy_p1_ctx = Some(builder.build()); 1065 Ok(()) 1066 } 1067 1068 /// Note the naming here is subtle, but this is effectively setting up a 1069 /// `wasmtime_wasi::WasiCtx` structure. 1070 /// 1071 /// This is stored in `Host` as `WasiP1Ctx` which internally contains the 1072 /// `WasiCtx` and `ResourceTable` used for WASI implementations. Exactly 1073 /// which "p" for WASIpN is more a reference to 1074 /// `wasmtime-wasi`-vs-`wasi-common` here more than anything else. 1075 fn set_wasi_ctx(&self, store: &mut Store<Host>) -> Result<()> { 1076 let mut builder = wasmtime_wasi::WasiCtxBuilder::new(); 1077 builder.inherit_stdio().args(&self.compute_argv()?); 1078 self.run.configure_wasip2(&mut builder)?; 1079 let ctx = builder.build_p1(); 1080 store.data_mut().wasip1_ctx = Some(Arc::new(Mutex::new(ctx))); 1081 Ok(()) 1082 } 1083 1084 #[cfg(feature = "wasi-nn")] 1085 fn collect_preloaded_nn_graphs( 1086 &self, 1087 ) -> Result<(Vec<wasmtime_wasi_nn::Backend>, wasmtime_wasi_nn::Registry)> { 1088 let graphs = self 1089 .run 1090 .common 1091 .wasi 1092 .nn_graph 1093 .iter() 1094 .map(|g| (g.format.clone(), g.dir.clone())) 1095 .collect::<Vec<_>>(); 1096 wasmtime_wasi_nn::preload(&graphs) 1097 } 1098 } 1099 1100 /// The `T` in `Store<T>` for what the CLI is running. 1101 /// 1102 /// This structures has a number of contexts used for various WASI proposals. 1103 /// Note that all of them are optional meaning that they're `None` by default 1104 /// and enabled with various CLI flags (some CLI flags are on-by-default). Note 1105 /// additionally that this structure is `Clone` to implement the `wasi-threads` 1106 /// proposal. Many WASI proposals are not compatible with `wasi-threads` so to 1107 /// model this `Arc` and `Arc<Mutex<T>>` is used for many configurations. If a 1108 /// WASI proposal is inherently threadsafe it's protected with just an `Arc` to 1109 /// share its configuration across many threads. 1110 /// 1111 /// If mutation is required then `Mutex` is used. Note though that the mutex is 1112 /// not actually locked as access always goes through `Arc::get_mut` which 1113 /// effectively asserts that there's only one thread. In short much of this is 1114 /// not compatible with `wasi-threads`. 1115 #[derive(Default, Clone)] 1116 struct Host { 1117 // Legacy wasip1 context using `wasi_common`, not set unless opted-in-to 1118 // with the CLI. 1119 legacy_p1_ctx: Option<wasi_common::WasiCtx>, 1120 1121 // Context for both WASIp1 and WASIp2 (and beyond) for the `wasmtime_wasi` 1122 // crate. This has both `wasmtime_wasi::WasiCtx` as well as a 1123 // `ResourceTable` internally to be used. 1124 // 1125 // The Mutex is only needed to satisfy the Sync constraint but we never 1126 // actually perform any locking on it as we use Mutex::get_mut for every 1127 // access. 1128 wasip1_ctx: Option<Arc<Mutex<wasmtime_wasi::p1::WasiP1Ctx>>>, 1129 1130 #[cfg(feature = "wasi-nn")] 1131 wasi_nn_wit: Option<Arc<wasmtime_wasi_nn::wit::WasiNnCtx>>, 1132 #[cfg(feature = "wasi-nn")] 1133 wasi_nn_witx: Option<Arc<wasmtime_wasi_nn::witx::WasiNnCtx>>, 1134 1135 #[cfg(feature = "wasi-threads")] 1136 wasi_threads: Option<Arc<WasiThreadsCtx<Host>>>, 1137 #[cfg(feature = "wasi-http")] 1138 wasi_http: Option<Arc<WasiHttpCtx>>, 1139 #[cfg(feature = "wasi-http")] 1140 wasi_http_outgoing_body_buffer_chunks: Option<usize>, 1141 #[cfg(feature = "wasi-http")] 1142 wasi_http_outgoing_body_chunk_size: Option<usize>, 1143 limits: StoreLimits, 1144 #[cfg(feature = "profiling")] 1145 guest_profiler: Option<Arc<wasmtime::GuestProfiler>>, 1146 1147 #[cfg(feature = "wasi-config")] 1148 wasi_config: Option<Arc<WasiConfigVariables>>, 1149 #[cfg(feature = "wasi-keyvalue")] 1150 wasi_keyvalue: Option<Arc<WasiKeyValueCtx>>, 1151 #[cfg(feature = "wasi-tls")] 1152 wasi_tls: Option<Arc<WasiTlsCtx>>, 1153 } 1154 1155 impl Host { 1156 fn wasip1_ctx(&mut self) -> &mut wasmtime_wasi::p1::WasiP1Ctx { 1157 let ctx = self.wasip1_ctx.as_mut().expect("wasi is not configured"); 1158 Arc::get_mut(ctx) 1159 .expect("wasmtime_wasi is not compatible with threads") 1160 .get_mut() 1161 .unwrap() 1162 } 1163 } 1164 1165 impl WasiView for Host { 1166 fn ctx(&mut self) -> WasiCtxView<'_> { 1167 WasiView::ctx(self.wasip1_ctx()) 1168 } 1169 } 1170 1171 #[cfg(feature = "wasi-http")] 1172 impl wasmtime_wasi_http::types::WasiHttpView for Host { 1173 fn ctx(&mut self) -> &mut WasiHttpCtx { 1174 let ctx = self.wasi_http.as_mut().unwrap(); 1175 Arc::get_mut(ctx).expect("wasmtime_wasi is not compatible with threads") 1176 } 1177 1178 fn table(&mut self) -> &mut wasmtime::component::ResourceTable { 1179 WasiView::ctx(self).table 1180 } 1181 1182 fn outgoing_body_buffer_chunks(&mut self) -> usize { 1183 self.wasi_http_outgoing_body_buffer_chunks 1184 .unwrap_or_else(|| DEFAULT_OUTGOING_BODY_BUFFER_CHUNKS) 1185 } 1186 1187 fn outgoing_body_chunk_size(&mut self) -> usize { 1188 self.wasi_http_outgoing_body_chunk_size 1189 .unwrap_or_else(|| DEFAULT_OUTGOING_BODY_CHUNK_SIZE) 1190 } 1191 } 1192 1193 #[cfg(not(unix))] 1194 fn ctx_set_listenfd(num_fd: usize, _builder: &mut WasiCtxBuilder) -> Result<usize> { 1195 Ok(num_fd) 1196 } 1197 1198 #[cfg(unix)] 1199 fn ctx_set_listenfd(mut num_fd: usize, builder: &mut WasiCtxBuilder) -> Result<usize> { 1200 use listenfd::ListenFd; 1201 1202 for env in ["LISTEN_FDS", "LISTEN_FDNAMES"] { 1203 if let Ok(val) = std::env::var(env) { 1204 builder.env(env, &val)?; 1205 } 1206 } 1207 1208 let mut listenfd = ListenFd::from_env(); 1209 1210 for i in 0..listenfd.len() { 1211 if let Some(stdlistener) = listenfd.take_tcp_listener(i)? { 1212 let _ = stdlistener.set_nonblocking(true)?; 1213 let listener = TcpListener::from_std(stdlistener); 1214 builder.preopened_socket((3 + i) as _, listener)?; 1215 num_fd = 3 + i; 1216 } 1217 } 1218 1219 Ok(num_fd) 1220 } 1221 1222 #[cfg(feature = "coredump")] 1223 fn write_core_dump( 1224 store: &mut Store<Host>, 1225 err: &anyhow::Error, 1226 name: &str, 1227 path: &str, 1228 ) -> Result<()> { 1229 use std::fs::File; 1230 use std::io::Write; 1231 1232 let core_dump = err 1233 .downcast_ref::<wasmtime::WasmCoreDump>() 1234 .expect("should have been configured to capture core dumps"); 1235 1236 let core_dump = core_dump.serialize(store, name); 1237 1238 let mut core_dump_file = 1239 File::create(path).context(format!("failed to create file at `{path}`"))?; 1240 core_dump_file 1241 .write_all(&core_dump) 1242 .with_context(|| format!("failed to write core dump file at `{path}`"))?; 1243 Ok(()) 1244 } 1245