1 //! Common functionality shared between command implementations. 2 3 use anyhow::{bail, Context, Result}; 4 use clap::Parser; 5 use std::{path::Path, time::Duration}; 6 use wasmtime::{Engine, Module, Precompiled, StoreLimits, StoreLimitsBuilder}; 7 use wasmtime_cli_flags::{opt::WasmtimeOptionValue, CommonOptions}; 8 9 #[cfg(feature = "component-model")] 10 use wasmtime::component::Component; 11 12 pub enum RunTarget { 13 Core(Module), 14 15 #[cfg(feature = "component-model")] 16 Component(Component), 17 } 18 19 impl RunTarget { 20 pub fn unwrap_core(&self) -> &Module { 21 match self { 22 RunTarget::Core(module) => module, 23 #[cfg(feature = "component-model")] 24 RunTarget::Component(_) => panic!("expected a core wasm module, not a component"), 25 } 26 } 27 28 #[cfg(feature = "component-model")] 29 pub fn unwrap_component(&self) -> &Component { 30 match self { 31 RunTarget::Component(c) => c, 32 RunTarget::Core(_) => panic!("expected a component, not a core wasm module"), 33 } 34 } 35 } 36 37 /// Common command line arguments for run commands. 38 #[derive(Parser, PartialEq)] 39 pub struct RunCommon { 40 #[command(flatten)] 41 pub common: CommonOptions, 42 43 /// Allow executing precompiled WebAssembly modules as `*.cwasm` files. 44 /// 45 /// Note that this option is not safe to pass if the module being passed in 46 /// is arbitrary user input. Only `wasmtime`-precompiled modules generated 47 /// via the `wasmtime compile` command or equivalent should be passed as an 48 /// argument with this option specified. 49 #[arg(long = "allow-precompiled")] 50 pub allow_precompiled: bool, 51 52 /// Profiling strategy (valid options are: perfmap, jitdump, vtune, guest) 53 /// 54 /// The perfmap, jitdump, and vtune profiling strategies integrate Wasmtime 55 /// with external profilers such as `perf`. The guest profiling strategy 56 /// enables in-process sampling and will write the captured profile to 57 /// `wasmtime-guest-profile.json` by default which can be viewed at 58 /// https://profiler.firefox.com/. 59 /// 60 /// The `guest` option can be additionally configured as: 61 /// 62 /// --profile=guest[,path[,interval]] 63 /// 64 /// where `path` is where to write the profile and `interval` is the 65 /// duration between samples. When used with `--wasm-timeout` the timeout 66 /// will be rounded up to the nearest multiple of this interval. 67 #[arg( 68 long, 69 value_name = "STRATEGY", 70 value_parser = Profile::parse, 71 )] 72 pub profile: Option<Profile>, 73 } 74 75 impl RunCommon { 76 pub fn store_limits(&self) -> StoreLimits { 77 let mut limits = StoreLimitsBuilder::new(); 78 if let Some(max) = self.common.wasm.max_memory_size { 79 limits = limits.memory_size(max); 80 } 81 if let Some(max) = self.common.wasm.max_table_elements { 82 limits = limits.table_elements(max); 83 } 84 if let Some(max) = self.common.wasm.max_instances { 85 limits = limits.instances(max); 86 } 87 if let Some(max) = self.common.wasm.max_tables { 88 limits = limits.tables(max); 89 } 90 if let Some(max) = self.common.wasm.max_memories { 91 limits = limits.memories(max); 92 } 93 if let Some(enable) = self.common.wasm.trap_on_grow_failure { 94 limits = limits.trap_on_grow_failure(enable); 95 } 96 97 limits.build() 98 } 99 100 pub fn ensure_allow_precompiled(&self) -> Result<()> { 101 if self.allow_precompiled { 102 Ok(()) 103 } else { 104 bail!("running a precompiled module requires the `--allow-precompiled` flag") 105 } 106 } 107 108 #[cfg(feature = "component-model")] 109 fn ensure_allow_components(&self) -> Result<()> { 110 if self.common.wasm.component_model != Some(true) { 111 bail!("cannot execute a component without `--wasm component-model`"); 112 } 113 114 Ok(()) 115 } 116 117 pub fn load_module(&self, engine: &Engine, path: &Path) -> Result<RunTarget> { 118 let path = match path.to_str() { 119 #[cfg(unix)] 120 Some("-") => "/dev/stdin".as_ref(), 121 _ => path, 122 }; 123 124 // First attempt to load the module as an mmap. If this succeeds then 125 // detection can be done with the contents of the mmap and if a 126 // precompiled module is detected then `deserialize_file` can be used 127 // which is a slightly more optimal version than `deserialize` since we 128 // can leave most of the bytes on disk until they're referenced. 129 // 130 // If the mmap fails, for example if stdin is a pipe, then fall back to 131 // `std::fs::read` to load the contents. At that point precompiled 132 // modules must go through the `deserialize` functions. 133 // 134 // Note that this has the unfortunate side effect for precompiled 135 // modules on disk that they're opened once to detect what they are and 136 // then again internally in Wasmtime as part of the `deserialize_file` 137 // API. Currently there's no way to pass the `MmapVec` here through to 138 // Wasmtime itself (that'd require making `wasmtime-runtime` a public 139 // dependency or `MmapVec` a public type, both of which aren't ready to 140 // happen at this time). It's hoped though that opening a file twice 141 // isn't too bad in the grand scheme of things with respect to the CLI. 142 match wasmtime_runtime::MmapVec::from_file(path) { 143 Ok(map) => self.load_module_contents( 144 engine, 145 path, 146 &map, 147 || unsafe { Module::deserialize_file(engine, path) }, 148 #[cfg(feature = "component-model")] 149 || unsafe { Component::deserialize_file(engine, path) }, 150 ), 151 Err(_) => { 152 let bytes = std::fs::read(path) 153 .with_context(|| format!("failed to read file: {}", path.display()))?; 154 self.load_module_contents( 155 engine, 156 path, 157 &bytes, 158 || unsafe { Module::deserialize(engine, &bytes) }, 159 #[cfg(feature = "component-model")] 160 || unsafe { Component::deserialize(engine, &bytes) }, 161 ) 162 } 163 } 164 } 165 166 pub fn load_module_contents( 167 &self, 168 engine: &Engine, 169 path: &Path, 170 bytes: &[u8], 171 deserialize_module: impl FnOnce() -> Result<Module>, 172 #[cfg(feature = "component-model")] deserialize_component: impl FnOnce() -> Result<Component>, 173 ) -> Result<RunTarget> { 174 Ok(match engine.detect_precompiled(bytes) { 175 Some(Precompiled::Module) => { 176 self.ensure_allow_precompiled()?; 177 RunTarget::Core(deserialize_module()?) 178 } 179 #[cfg(feature = "component-model")] 180 Some(Precompiled::Component) => { 181 self.ensure_allow_precompiled()?; 182 self.ensure_allow_components()?; 183 RunTarget::Component(deserialize_component()?) 184 } 185 #[cfg(not(feature = "component-model"))] 186 Some(Precompiled::Component) => { 187 bail!("support for components was not enabled at compile time"); 188 } 189 None => { 190 // Parse the text format here specifically to add the `path` to 191 // the error message if there's a syntax error. 192 #[cfg(feature = "wat")] 193 let bytes = wat::parse_bytes(bytes).map_err(|mut e| { 194 e.set_path(path); 195 e 196 })?; 197 let _ = path; 198 if wasmparser::Parser::is_component(&bytes) { 199 #[cfg(feature = "component-model")] 200 { 201 self.ensure_allow_components()?; 202 RunTarget::Component(Component::new(engine, &bytes)?) 203 } 204 #[cfg(not(feature = "component-model"))] 205 { 206 bail!("support for components was not enabled at compile time"); 207 } 208 } else { 209 #[cfg(feature = "cranelift")] 210 return Ok(RunTarget::Core(Module::new(engine, &bytes)?)); 211 #[cfg(not(feature = "cranelift"))] 212 bail!("support for compiling modules was disabled at compile time"); 213 } 214 } 215 }) 216 } 217 } 218 219 #[derive(Clone, PartialEq)] 220 pub enum Profile { 221 Native(wasmtime::ProfilingStrategy), 222 Guest { path: String, interval: Duration }, 223 } 224 225 impl Profile { 226 /// Parse the `profile` argument to either the `run` or `serve` commands. 227 pub fn parse(s: &str) -> Result<Profile> { 228 let parts = s.split(',').collect::<Vec<_>>(); 229 match &parts[..] { 230 ["perfmap"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::PerfMap)), 231 ["jitdump"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::JitDump)), 232 ["vtune"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::VTune)), 233 ["guest"] => Ok(Profile::Guest { 234 path: "wasmtime-guest-profile.json".to_string(), 235 interval: Duration::from_millis(10), 236 }), 237 ["guest", path] => Ok(Profile::Guest { 238 path: path.to_string(), 239 interval: Duration::from_millis(10), 240 }), 241 ["guest", path, dur] => Ok(Profile::Guest { 242 path: path.to_string(), 243 interval: WasmtimeOptionValue::parse(Some(dur))?, 244 }), 245 _ => bail!("unknown profiling strategy: {s}"), 246 } 247 } 248 } 249