Skip to main content

wasmtime_wizer/
lib.rs

1//! Wizer: the WebAssembly pre-initializer!
2//!
3//! See the [`Wizer`] struct for details.
4
5#![deny(missing_docs)]
6#![cfg_attr(docsrs, feature(doc_cfg))]
7
8mod info;
9mod instrument;
10mod parse;
11mod rewrite;
12mod snapshot;
13
14#[cfg(feature = "wasmtime")]
15mod wasmtime;
16#[cfg(feature = "wasmtime")]
17pub use wasmtime::*;
18#[cfg(feature = "component-model")]
19mod component;
20#[cfg(feature = "component-model")]
21pub use component::*;
22#[cfg(not(feature = "rayon"))]
23mod rayoff;
24
25pub use crate::info::ModuleContext;
26pub use crate::snapshot::SnapshotVal;
27use ::wasmtime::{Result, bail, error::Context as _};
28use std::collections::{HashMap, HashSet};
29pub use wasmparser::ValType;
30
31const DEFAULT_KEEP_INIT_FUNC: bool = false;
32
33/// Wizer: the WebAssembly pre-initializer!
34///
35/// Don't wait for your Wasm module to initialize itself, pre-initialize it!
36/// Wizer instantiates your WebAssembly module, executes its initialization
37/// function, and then serializes the instance's initialized state out into a
38/// new WebAssembly module. Now you can use this new, pre-initialized
39/// WebAssembly module to hit the ground running, without making your users wait
40/// for that first-time set up code to complete.
41///
42/// ## Caveats
43///
44/// * The initialization function may not call any imported functions. Doing so
45///   will trigger a trap and `wizer` will exit.
46///
47/// * The Wasm module may not import globals, tables, or memories.
48///
49/// * Reference types are not supported yet. This is tricky because it would
50///   allow the Wasm module to mutate tables, and we would need to be able to
51///   snapshot the new table state, but funcrefs and externrefs don't have
52///   identity and aren't comparable in the Wasm spec, which makes snapshotting
53///   difficult.
54#[derive(Clone, Debug)]
55#[cfg_attr(feature = "clap", derive(clap::Parser))]
56pub struct Wizer {
57    /// The Wasm export name of the function that should be executed to
58    /// initialize the Wasm module.
59    ///
60    /// When used with components, this can be either the item name of a
61    /// function (e.g. `wasi:cli/run.run`) or a complete wave-encoded function
62    /// call (e.g. `your:package/iface.func("hello world")`)
63    #[cfg_attr(
64        feature = "clap",
65        arg(short = 'f', long, default_value = "wizer-initialize")
66    )]
67    init_func: String,
68
69    /// Any function renamings to perform.
70    ///
71    /// A renaming specification `dst=src` renames a function export `src` to
72    /// `dst`, overwriting any previous `dst` export.
73    ///
74    /// Multiple renamings can be specified. It is an error to specify more than
75    /// one source to rename to a destination name, or to specify more than one
76    /// renaming destination for one source.
77    ///
78    /// This option can be used, for example, to replace a `_start` entry point
79    /// in an initialized module with an alternate entry point.
80    ///
81    /// When module linking is enabled, these renames are only applied to the
82    /// outermost module.
83    #[cfg_attr(
84        feature = "clap",
85        arg(
86            short = 'r',
87            long = "rename-func",
88            alias = "func-rename",
89            value_name = "dst=src",
90            value_parser = parse_rename,
91        ),
92    )]
93    func_renames: Vec<(String, String)>,
94
95    /// After initialization, should the Wasm module still export the
96    /// initialization function?
97    ///
98    /// This is `false` by default, meaning that the initialization function is
99    /// no longer exported from the Wasm module.
100    #[cfg_attr(
101        feature = "clap",
102        arg(long, require_equals = true, value_name = "true|false")
103    )]
104    keep_init_func: Option<Option<bool>>,
105}
106
107#[cfg(feature = "clap")]
108fn parse_rename(s: &str) -> Result<(String, String)> {
109    let parts: Vec<&str> = s.splitn(2, '=').collect();
110    if parts.len() != 2 {
111        bail!("must contain exactly one equals character ('=')");
112    }
113    Ok((parts[0].into(), parts[1].into()))
114}
115
116#[derive(Default)]
117struct FuncRenames {
118    /// For a given export name that we encounter in the original module, a map
119    /// to a new name, if any, to emit in the output module.
120    rename_src_to_dst: HashMap<String, String>,
121    /// A set of export names that we ignore in the original module (because
122    /// they are overwritten by renamings).
123    rename_dsts: HashSet<String>,
124}
125
126impl FuncRenames {
127    fn parse(renames: &[(String, String)]) -> Result<FuncRenames> {
128        let mut ret = FuncRenames {
129            rename_src_to_dst: HashMap::new(),
130            rename_dsts: HashSet::new(),
131        };
132        if renames.is_empty() {
133            return Ok(ret);
134        }
135
136        for (dst, src) in renames {
137            if ret.rename_dsts.contains(dst) {
138                bail!("Duplicated function rename dst {dst}");
139            }
140            if ret.rename_src_to_dst.contains_key(src) {
141                bail!("Duplicated function rename src {src}");
142            }
143            ret.rename_dsts.insert(dst.clone());
144            ret.rename_src_to_dst.insert(src.clone(), dst.clone());
145        }
146
147        Ok(ret)
148    }
149}
150
151impl Wizer {
152    /// Construct a new `Wizer` builder.
153    pub fn new() -> Self {
154        Wizer {
155            init_func: "wizer-initialize".to_string(),
156            func_renames: vec![],
157            keep_init_func: None,
158        }
159    }
160
161    /// The export name of the initializer function.
162    ///
163    /// Defaults to `"wizer-initialize"`.
164    pub fn init_func(&mut self, init_func: impl Into<String>) -> &mut Self {
165        self.init_func = init_func.into();
166        self
167    }
168
169    /// Returns the initialization function that will be run for wizer.
170    pub fn get_init_func(&self) -> &str {
171        &self.init_func
172    }
173
174    /// Add a function rename to perform.
175    pub fn func_rename(&mut self, new_name: &str, old_name: &str) -> &mut Self {
176        self.func_renames
177            .push((new_name.to_string(), old_name.to_string()));
178        self
179    }
180
181    /// After initialization, should the Wasm module still export the
182    /// initialization function?
183    ///
184    /// This is `false` by default, meaning that the initialization function is
185    /// no longer exported from the Wasm module.
186    pub fn keep_init_func(&mut self, keep: bool) -> &mut Self {
187        self.keep_init_func = Some(Some(keep));
188        self
189    }
190
191    /// First half of [`Self::run`] which instruments the provided `wasm` and
192    /// produces a new wasm module which should be run by a runtime.
193    ///
194    /// After the returned wasm is executed the context returned here and the
195    /// state of the instance should be passed to [`Self::snapshot`].
196    pub fn instrument<'a>(&self, wasm: &'a [u8]) -> Result<(ModuleContext<'a>, Vec<u8>)> {
197        // Make sure we're given valid Wasm from the get go.
198        self.wasm_validate(&wasm)?;
199
200        let mut cx = parse::parse(wasm)?;
201
202        // When wizening core modules directly some imports aren't supported,
203        // so check for those here.
204        for import in cx.imports() {
205            match import.ty {
206                wasmparser::TypeRef::Global(_) => {
207                    bail!("imported globals are not supported")
208                }
209                wasmparser::TypeRef::Table(_) => {
210                    bail!("imported tables are not supported")
211                }
212                wasmparser::TypeRef::Memory(_) => {
213                    bail!("imported memories are not supported")
214                }
215                wasmparser::TypeRef::Func(_) => {}
216                wasmparser::TypeRef::FuncExact(_) => {}
217                wasmparser::TypeRef::Tag(_) => {}
218            }
219        }
220
221        let instrumented_wasm = instrument::instrument(&mut cx);
222        self.debug_assert_valid_wasm(&instrumented_wasm, "instrumented module");
223
224        Ok((cx, instrumented_wasm))
225    }
226
227    /// Second half of [`Self::run`] which takes the [`ModuleContext`] returned
228    /// by [`Self::instrument`] and the state of the `instance` after it has
229    /// possibly executed its initialization function.
230    ///
231    /// This returns a new WebAssembly binary which has all state
232    /// pre-initialized.
233    pub async fn snapshot(
234        &self,
235        cx: &ModuleContext<'_>,
236        instance: &mut impl InstanceState,
237    ) -> Result<Vec<u8>> {
238        // Parse rename spec.
239        let renames = FuncRenames::parse(&self.func_renames)?;
240
241        let snapshot = snapshot::snapshot(cx, instance).await;
242        let rewritten_wasm = self.rewrite(cx, &snapshot, &renames, true);
243
244        self.debug_assert_valid_wasm(&rewritten_wasm, "rewritten module");
245
246        Ok(rewritten_wasm)
247    }
248
249    fn debug_assert_valid_wasm(&self, wasm: &[u8], context: &str) {
250        if !cfg!(debug_assertions) {
251            return;
252        }
253        if let Err(error) = self.wasm_validate(&wasm) {
254            #[cfg(feature = "wasmprinter")]
255            let wat = wasmprinter::print_bytes(&wasm)
256                .unwrap_or_else(|e| format!("Disassembling to WAT failed: {e}"));
257            #[cfg(not(feature = "wasmprinter"))]
258            let wat = "`wasmprinter` cargo feature is not enabled".to_string();
259
260            let wat = if wat.len() > 16 * 1024 {
261                std::fs::write("invalid.wat", wat).expect("writing to invalid.wat");
262                "written to invalid.wat"
263            } else {
264                &wat
265            };
266            panic!("{context} is not valid wasm: {error:?}\n\nWAT:\n{wat}");
267        }
268    }
269
270    fn wasm_validate(&self, wasm: &[u8]) -> Result<()> {
271        log::debug!("Validating input Wasm");
272
273        wasmparser::Validator::new_with_features(wasmparser::WasmFeatures::all())
274            .validate_all(wasm)
275            .context("wasm validation failed")?;
276
277        for payload in wasmparser::Parser::new(0).parse_all(wasm) {
278            match payload? {
279                wasmparser::Payload::CodeSectionEntry(code) => {
280                    let mut ops = code.get_operators_reader()?;
281                    while !ops.eof() {
282                        match ops.read()? {
283                            // Table mutations aren't allowed as wizer has no
284                            // way to record a snapshot of a table at this time.
285                            // The only table mutations allowed are those from
286                            // active element segments which can be
287                            // deterministically replayed, so disallow all other
288                            // forms of mutating a table.
289                            //
290                            // Ideally Wizer could take a snapshot of a table
291                            // post-instantiation and then ensure that after
292                            // running initialization the table didn't get
293                            // mutated, allowing these instructions, but that's
294                            // also not possible at this time.
295                            wasmparser::Operator::TableCopy { .. } => {
296                                bail!("unsupported `table.copy` instruction")
297                            }
298                            wasmparser::Operator::TableInit { .. } => {
299                                bail!("unsupported `table.init` instruction")
300                            }
301                            wasmparser::Operator::TableSet { .. } => {
302                                bail!("unsupported `table.set` instruction")
303                            }
304                            wasmparser::Operator::TableGrow { .. } => {
305                                bail!("unsupported `table.grow` instruction")
306                            }
307                            wasmparser::Operator::TableFill { .. } => {
308                                bail!("unsupported `table.fill` instruction")
309                            }
310
311                            // Wizer has no way of dynamically determining which
312                            // element or data segments were dropped during
313                            // execution so instead disallow these instructions
314                            // entirely. Like above it'd be nice to allow them
315                            // but just forbid their execution during the
316                            // initialization function, but that can't be done
317                            // easily at this time.
318                            wasmparser::Operator::ElemDrop { .. } => {
319                                bail!("unsupported `elem.drop` instruction")
320                            }
321                            wasmparser::Operator::DataDrop { .. } => {
322                                bail!("unsupported `data.drop` instruction")
323                            }
324
325                            // Wizer can't snapshot GC references, so disallow
326                            // any mutation of GC references. This prevents, for
327                            // example, reading something from a table and then
328                            // mutating it.
329                            wasmparser::Operator::StructSet { .. } => {
330                                bail!("unsupported `struct.set` instruction")
331                            }
332                            wasmparser::Operator::ArraySet { .. } => {
333                                bail!("unsupported `array.set` instruction")
334                            }
335                            wasmparser::Operator::ArrayFill { .. } => {
336                                bail!("unsupported `array.fill` instruction")
337                            }
338                            wasmparser::Operator::ArrayCopy { .. } => {
339                                bail!("unsupported `array.copy` instruction")
340                            }
341                            wasmparser::Operator::ArrayInitData { .. } => {
342                                bail!("unsupported `array.init_data` instruction")
343                            }
344                            wasmparser::Operator::ArrayInitElem { .. } => {
345                                bail!("unsupported `array.init_elem` instruction")
346                            }
347
348                            _ => continue,
349                        }
350                    }
351                }
352                wasmparser::Payload::GlobalSection(globals) => {
353                    for g in globals {
354                        let g = g?.ty;
355                        if !g.mutable {
356                            continue;
357                        }
358                        match g.content_type {
359                            wasmparser::ValType::I32
360                            | wasmparser::ValType::I64
361                            | wasmparser::ValType::F32
362                            | wasmparser::ValType::F64
363                            | wasmparser::ValType::V128 => {}
364                            wasmparser::ValType::Ref(_) => {
365                                bail!("unsupported mutable global containing a reference type")
366                            }
367                        }
368                    }
369                }
370                _ => {}
371            }
372        }
373
374        Ok(())
375    }
376
377    fn get_keep_init_func(&self) -> bool {
378        match self.keep_init_func {
379            Some(keep) => keep.unwrap_or(true),
380            None => DEFAULT_KEEP_INIT_FUNC,
381        }
382    }
383}
384
385/// Abstract ability to load state from a WebAssembly instance after it's been
386/// instantiated and some exports have run.
387pub trait InstanceState {
388    /// Loads the global specified by `name`, returning a `SnapshotVal`.
389    ///
390    /// # Panics
391    ///
392    /// This function panics if `name` isn't an exported global or if the type
393    /// of the global doesn't fit in `SnapshotVal`.
394    fn global_get(
395        &mut self,
396        name: &str,
397        type_hint: ValType,
398    ) -> impl Future<Output = SnapshotVal> + Send;
399
400    /// Loads the contents of the memory specified by `name`, returning the
401    /// entier contents as a `Vec<u8>`.
402    ///
403    /// # Panics
404    ///
405    /// This function panics if `name` isn't an exported memory.
406    fn memory_contents(
407        &mut self,
408        name: &str,
409        contents: impl FnOnce(&[u8]) + Send,
410    ) -> impl Future<Output = ()> + Send;
411}