xref: /wasmtime-44.0.1/src/common.rs (revision f9f8a4df)
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