wasm_snip/
lib.rs

1/*!
2
3`wasm-snip` replaces a WebAssembly function's body with an `unreachable`.
4
5Maybe you know that some function will never be called at runtime, but the
6compiler can't prove that at compile time? Snip it! All the functions it
7transitively called — which weren't called by anything else and therefore
8could also never be called at runtime — will get removed too.
9
10Very helpful when shrinking the size of WebAssembly binaries!
11
12This functionality relies on the "name" section being present in the `.wasm`
13file, so build with debug symbols:
14
15```toml
16[profile.release]
17debug = true
18```
19
20* [Executable](#executable)
21* [Library](#library)
22* [License](#license)
23* [Contributing](#contributing)
24
25## Executable
26
27To install the `wasm-snip` executable, run
28
29```text
30$ cargo install wasm-snip
31```
32
33You can use `wasm-snip` to remove the `annoying_space_waster`
34function from `input.wasm` and put the new binary in `output.wasm` like this:
35
36```text
37$ wasm-snip input.wasm -o output.wasm annoying_space_waster
38```
39
40For information on using the `wasm-snip` executable, run
41
42```text
43$ wasm-snip --help
44```
45
46And you'll get the most up-to-date help text, like:
47
48```text
49Replace a wasm function with an `unreachable`.
50
51USAGE:
52wasm-snip [FLAGS] [OPTIONS] <input> [--] [function]...
53
54FLAGS:
55-h, --help                    Prints help information
56--snip-rust-fmt-code          Snip Rust's `std::fmt` and `core::fmt` code.
57--snip-rust-panicking-code    Snip Rust's `std::panicking` and `core::panicking` code.
58-V, --version                 Prints version information
59
60OPTIONS:
61-o, --output <output>         The path to write the output wasm file to. Defaults to stdout.
62-p, --pattern <pattern>...    Snip any function that matches the given regular expression.
63
64ARGS:
65<input>          The input wasm file containing the function(s) to snip.
66<function>...    The specific function(s) to snip. These must match exactly. Use the -p flag for fuzzy matching.
67```
68
69## Library
70
71To use `wasm-snip` as a library, add this to your `Cargo.toml`:
72
73```toml
74[dependencies.wasm-snip]
75# Do not build the executable.
76default-features = false
77```
78
79See [docs.rs/wasm-snip][docs] for API documentation.
80
81[docs]: https://docs.rs/wasm-snip
82
83## License
84
85Licensed under either of
86
87 * [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
88
89 * [MIT license](http://opensource.org/licenses/MIT)
90
91at your option.
92
93## Contributing
94
95See
96[CONTRIBUTING.md](https://github.com/rustwasm/wasm-snip/blob/master/CONTRIBUTING.md)
97for hacking.
98
99Unless you explicitly state otherwise, any contribution intentionally submitted
100for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
101dual licensed as above, without any additional terms or conditions.
102
103 */
104
105#![deny(missing_docs)]
106#![deny(missing_debug_implementations)]
107
108use failure::ResultExt;
109use rayon::prelude::*;
110use std::collections::HashMap;
111use std::collections::HashSet;
112use std::path;
113use walrus::ir::VisitorMut;
114
115/// Input configuration.
116#[derive(Clone, Debug)]
117pub enum Input {
118    /// The input `.wasm` file that should have its function snipped.
119    File(path::PathBuf),
120    /// The input WebAssembly blob that should have its function snipped.
121    Buffer(Vec<u8>),
122    // TODO: Support Walrus module directly.
123    // Module(walrus::Module),
124}
125
126impl Default for Input {
127    fn default() -> Self {
128        Input::File(path::PathBuf::default())
129    }
130}
131
132/// Options for controlling which functions in what `.wasm` file should be
133/// snipped.
134#[derive(Clone, Debug, Default)]
135pub struct Options {
136    /// The functions that should be snipped from the `.wasm` file.
137    pub functions: Vec<String>,
138
139    /// The regex patterns whose matches should be snipped from the `.wasm`
140    /// file.
141    pub patterns: Vec<String>,
142
143    /// Should Rust `std::fmt` and `core::fmt` functions be snipped?
144    pub snip_rust_fmt_code: bool,
145
146    /// Should Rust `std::panicking` and `core::panicking` functions be snipped?
147    pub snip_rust_panicking_code: bool,
148
149    /// Should we skip generating [the "producers" custom
150    /// section](https://github.com/WebAssembly/tool-conventions/blob/master/ProducersSection.md)?
151    pub skip_producers_section: bool,
152}
153
154/// Snip the functions from the input file described by the options.
155pub fn snip(module: &mut walrus::Module, options: Options) -> Result<(), failure::Error> {
156    if !options.skip_producers_section {
157        module
158            .producers
159            .add_processed_by("wasm-snip", env!("CARGO_PKG_VERSION"));
160    }
161
162    let names: HashSet<String> = options.functions.iter().cloned().collect();
163    let re_set = build_regex_set(options).context("failed to compile regex")?;
164    let to_snip = find_functions_to_snip(&module, &names, &re_set);
165
166    replace_calls_with_unreachable(module, &to_snip);
167    unexport_snipped_functions(module, &to_snip);
168    unimport_snipped_functions(module, &to_snip);
169    snip_table_elements(module, &to_snip);
170    delete_functions_to_snip(module, &to_snip);
171    walrus::passes::gc::run(module);
172
173    Ok(())
174}
175
176fn build_regex_set(mut options: Options) -> Result<regex::RegexSet, failure::Error> {
177    // Snip the Rust `fmt` code, if requested.
178    if options.snip_rust_fmt_code {
179        // Mangled symbols.
180        options.patterns.push(".*4core3fmt.*".into());
181        options.patterns.push(".*3std3fmt.*".into());
182
183        // Mangled in impl.
184        options.patterns.push(r#".*core\.\.fmt\.\..*"#.into());
185        options.patterns.push(r#".*std\.\.fmt\.\..*"#.into());
186
187        // Demangled symbols.
188        options.patterns.push(".*core::fmt::.*".into());
189        options.patterns.push(".*std::fmt::.*".into());
190    }
191
192    // Snip the Rust `panicking` code, if requested.
193    if options.snip_rust_panicking_code {
194        // Mangled symbols.
195        options.patterns.push(".*4core9panicking.*".into());
196        options.patterns.push(".*3std9panicking.*".into());
197
198        // Mangled in impl.
199        options.patterns.push(r#".*core\.\.panicking\.\..*"#.into());
200        options.patterns.push(r#".*std\.\.panicking\.\..*"#.into());
201
202        // Demangled symbols.
203        options.patterns.push(".*core::panicking::.*".into());
204        options.patterns.push(".*std::panicking::.*".into());
205    }
206
207    Ok(regex::RegexSet::new(options.patterns)?)
208}
209
210fn find_functions_to_snip(
211    module: &walrus::Module,
212    names: &HashSet<String>,
213    re_set: &regex::RegexSet,
214) -> HashSet<walrus::FunctionId> {
215    module
216        .funcs
217        .par_iter()
218        .filter_map(|f| {
219            f.name.as_ref().and_then(|name| {
220                if names.contains(name) || re_set.is_match(name) {
221                    Some(f.id())
222                } else {
223                    None
224                }
225            })
226        })
227        .collect()
228}
229
230fn delete_functions_to_snip(module: &mut walrus::Module, to_snip: &HashSet<walrus::FunctionId>) {
231    for f in to_snip.iter().cloned() {
232        module.funcs.delete(f);
233    }
234}
235
236fn replace_calls_with_unreachable(
237    module: &mut walrus::Module,
238    to_snip: &HashSet<walrus::FunctionId>,
239) {
240    struct Replacer<'a> {
241        to_snip: &'a HashSet<walrus::FunctionId>,
242    }
243
244    impl Replacer<'_> {
245        fn should_snip_call(&self, instr: &walrus::ir::Instr) -> bool {
246            if let walrus::ir::Instr::Call(walrus::ir::Call { func }) = instr {
247                if self.to_snip.contains(func) {
248                    return true;
249                }
250            }
251            false
252        }
253    }
254
255    impl VisitorMut for Replacer<'_> {
256        fn visit_instr_mut(&mut self, instr: &mut walrus::ir::Instr) {
257            if self.should_snip_call(instr) {
258                *instr = walrus::ir::Unreachable {}.into();
259            }
260        }
261    }
262
263    module.funcs.par_iter_local_mut().for_each(|(id, func)| {
264        // Don't bother transforming functions that we are snipping.
265        if to_snip.contains(&id) {
266            return;
267        }
268
269        let entry = func.entry_block();
270        walrus::ir::dfs_pre_order_mut(&mut Replacer { to_snip }, func, entry);
271    });
272}
273
274fn unexport_snipped_functions(module: &mut walrus::Module, to_snip: &HashSet<walrus::FunctionId>) {
275    let exports_to_snip: HashSet<walrus::ExportId> = module
276        .exports
277        .iter()
278        .filter_map(|e| match e.item {
279            walrus::ExportItem::Function(f) if to_snip.contains(&f) => Some(e.id()),
280            _ => None,
281        })
282        .collect();
283
284    for e in exports_to_snip {
285        module.exports.delete(e);
286    }
287}
288
289fn unimport_snipped_functions(module: &mut walrus::Module, to_snip: &HashSet<walrus::FunctionId>) {
290    let imports_to_snip: HashSet<walrus::ImportId> = module
291        .imports
292        .iter()
293        .filter_map(|i| match i.kind {
294            walrus::ImportKind::Function(f) if to_snip.contains(&f) => Some(i.id()),
295            _ => None,
296        })
297        .collect();
298
299    for i in imports_to_snip {
300        module.imports.delete(i);
301    }
302}
303
304fn snip_table_elements(module: &mut walrus::Module, to_snip: &HashSet<walrus::FunctionId>) {
305    let mut unreachable_funcs: HashMap<walrus::TypeId, walrus::FunctionId> = Default::default();
306
307    let make_unreachable_func = |ty: walrus::TypeId,
308                                 types: &mut walrus::ModuleTypes,
309                                 locals: &mut walrus::ModuleLocals,
310                                 funcs: &mut walrus::ModuleFunctions|
311     -> walrus::FunctionId {
312        let ty = types.get(ty);
313        let params = ty.params().to_vec();
314        let locals: Vec<_> = params.iter().map(|ty| locals.add(*ty)).collect();
315        let results = ty.results().to_vec();
316        let mut builder = walrus::FunctionBuilder::new(types, &params, &results);
317        builder.func_body().unreachable();
318        builder.finish(locals, funcs)
319    };
320
321    for t in module.tables.iter_mut() {
322        if let walrus::TableKind::Function(ref mut ft) = t.kind {
323            let types = &mut module.types;
324            let locals = &mut module.locals;
325            let funcs = &mut module.funcs;
326
327            ft.elements
328                .iter_mut()
329                .flat_map(|el| el)
330                .filter(|f| to_snip.contains(f))
331                .for_each(|el| {
332                    let ty = funcs.get(*el).ty();
333                    *el = *unreachable_funcs
334                        .entry(ty)
335                        .or_insert_with(|| make_unreachable_func(ty, types, locals, funcs));
336                });
337
338            ft.relative_elements
339                .iter_mut()
340                .flat_map(|(_, elems)| elems.iter_mut().filter(|f| to_snip.contains(f)))
341                .for_each(|el| {
342                    let ty = funcs.get(*el).ty();
343                    *el = *unreachable_funcs
344                        .entry(ty)
345                        .or_insert_with(|| make_unreachable_func(ty, types, locals, funcs));
346                });
347        }
348    }
349}