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